microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.5.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/debugger/scriptImporter.ts

204lines · modepreview

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

import { logger } from "vscode-debugadapter";
import { ensurePackagerRunning } from "../common/packagerStatus";
import path = require("path");
import { Request } from "../common/node/request";
import { SourceMapUtil } from "./sourceMap";
import url = require("url");
import * as semver from "semver";
import { ProjectVersionHelper, RNPackageVersions } from "../common/projectVersionHelper";
import { ErrorHelper } from "../common/error/errorHelper";
import { InternalErrorCode } from "../common/error/internalErrorCode";
import { FileSystem } from "../common/node/fileSystem";

export interface DownloadedScript {
    contents: string;
    filepath: string;
}

interface IStrictUrl extends url.Url {
    pathname: string;
    href: string;
}

export class ScriptImporter {
    public static DEBUGGER_WORKER_FILE_BASENAME = "debuggerWorker";
    public static DEBUGGER_WORKER_FILENAME = ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME + ".js";
    private packagerAddress: string;
    private packagerPort: number;
    private sourcesStoragePath: string;
    private packagerRemoteRoot?: string;
    private packagerLocalRoot?: string;
    private sourceMapUtil: SourceMapUtil;

    constructor(
        packagerAddress: string,
        packagerPort: number,
        sourcesStoragePath: string,
        packagerRemoteRoot?: string,
        packagerLocalRoot?: string,
    ) {
        this.packagerAddress = packagerAddress;
        this.packagerPort = packagerPort;
        this.sourcesStoragePath = sourcesStoragePath;
        this.packagerRemoteRoot = packagerRemoteRoot;
        this.packagerLocalRoot = packagerLocalRoot;
        this.sourceMapUtil = new SourceMapUtil();
    }

    public downloadAppScript(
        scriptUrlString: string,
        projectRootPath: string,
    ): Promise<DownloadedScript> {
        const parsedScriptUrl = url.parse(scriptUrlString);
        const overriddenScriptUrlString =
            parsedScriptUrl.hostname === "localhost"
                ? this.overridePackagerPort(scriptUrlString)
                : scriptUrlString;
        // We'll get the source code, and store it locally to have a better debugging experience
        return Request.request(overriddenScriptUrlString, true).then(scriptBody => {
            return ProjectVersionHelper.getReactNativeVersions(projectRootPath).then(rnVersions => {
                // unfortunatelly Metro Bundler is broken in RN 0.54.x versions, so use this workaround unless it will be fixed
                // https://github.com/facebook/metro/issues/147
                // https://github.com/microsoft/vscode-react-native/issues/660
                if (
                    ProjectVersionHelper.getRNVersionsWithBrokenMetroBundler().indexOf(
                        rnVersions.reactNativeVersion,
                    ) >= 0
                ) {
                    let noSourceMappingUrlGenerated =
                        scriptBody.match(/sourceMappingURL=/g) === null;
                    if (noSourceMappingUrlGenerated) {
                        let sourceMapPathUrl = overriddenScriptUrlString.replace("bundle", "map");
                        scriptBody = this.sourceMapUtil.appendSourceMapPaths(
                            scriptBody,
                            sourceMapPathUrl,
                        );
                    }
                }

                // Extract sourceMappingURL from body
                let scriptUrl = <IStrictUrl>url.parse(overriddenScriptUrlString); // scriptUrl = "http://localhost:8081/index.ios.bundle?platform=ios&dev=true"
                let sourceMappingUrl = this.sourceMapUtil.getSourceMapURL(scriptUrl, scriptBody); // sourceMappingUrl = "http://localhost:8081/index.ios.map?platform=ios&dev=true"

                let waitForSourceMapping: Promise<void> = Promise.resolve();
                if (sourceMappingUrl) {
                    /* handle source map - request it and store it locally */
                    waitForSourceMapping = this.writeAppSourceMap(sourceMappingUrl, scriptUrl).then(
                        () => {
                            scriptBody = this.sourceMapUtil.updateScriptPaths(
                                scriptBody,
                                <IStrictUrl>sourceMappingUrl,
                            );
                            if (semver.gte(rnVersions.reactNativeVersion, "0.61.0")) {
                                scriptBody = this.sourceMapUtil.removeSourceURL(scriptBody);
                            }
                        },
                    );
                }

                return waitForSourceMapping
                    .then(() => this.writeAppScript(scriptBody, scriptUrl))
                    .then((scriptFilePath: string) => {
                        logger.verbose(
                            `Script ${overriddenScriptUrlString} downloaded to ${scriptFilePath}`,
                        );
                        return { contents: scriptBody, filepath: scriptFilePath };
                    });
            });
        });
    }

    public downloadDebuggerWorker(
        sourcesStoragePath: string,
        projectRootPath: string,
        debuggerWorkerUrlPath?: string,
    ): Promise<void> {
        const errPackagerNotRunning = ErrorHelper.getInternalError(
            InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort,
            this.packagerPort,
        );
        return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
            .then(() => {
                return ProjectVersionHelper.getReactNativeVersions(projectRootPath);
            })
            .then((rnVersions: RNPackageVersions) => {
                let debuggerWorkerURL = this.prepareDebuggerWorkerURL(
                    rnVersions.reactNativeVersion,
                    debuggerWorkerUrlPath,
                );
                let debuggerWorkerLocalPath = path.join(
                    sourcesStoragePath,
                    ScriptImporter.DEBUGGER_WORKER_FILENAME,
                );
                logger.verbose(
                    "About to download: " + debuggerWorkerURL + " to: " + debuggerWorkerLocalPath,
                );

                return Request.request(debuggerWorkerURL, true).then((body: string) => {
                    return new FileSystem().writeFile(debuggerWorkerLocalPath, body);
                });
            });
    }

    public prepareDebuggerWorkerURL(rnVersion: string, debuggerWorkerUrlPath?: string): string {
        let debuggerWorkerURL: string;
        // It can be empty string
        if (debuggerWorkerUrlPath !== undefined) {
            debuggerWorkerURL = `http://${this.packagerAddress}:${this.packagerPort}/${debuggerWorkerUrlPath}${ScriptImporter.DEBUGGER_WORKER_FILENAME}`;
        } else {
            let newPackager = "";
            if (
                !semver.valid(
                    rnVersion,
                ) /*Custom RN implementations should support new packager*/ ||
                semver.gte(rnVersion, "0.50.0")
            ) {
                newPackager = "debugger-ui/";
            }
            debuggerWorkerURL = `http://${this.packagerAddress}:${this.packagerPort}/${newPackager}${ScriptImporter.DEBUGGER_WORKER_FILENAME}`;
        }
        return debuggerWorkerURL;
    }

    /**
     * Writes the script file to the project temporary location.
     */
    private writeAppScript(scriptBody: string, scriptUrl: IStrictUrl): Promise<string> {
        let scriptFilePath = path.join(this.sourcesStoragePath, path.basename(scriptUrl.pathname)); // scriptFilePath = "$TMPDIR/index.ios.bundle"
        return new FileSystem().writeFile(scriptFilePath, scriptBody).then(() => scriptFilePath);
    }

    /**
     * Writes the source map file to the project temporary location.
     */
    private writeAppSourceMap(sourceMapUrl: IStrictUrl, scriptUrl: IStrictUrl): Promise<void> {
        return Request.request(sourceMapUrl.href, true).then((sourceMapBody: string) => {
            let sourceMappingLocalPath = path.join(
                this.sourcesStoragePath,
                path.basename(sourceMapUrl.pathname),
            ); // sourceMappingLocalPath = "$TMPDIR/index.ios.map"
            let scriptFileRelativePath = path.basename(scriptUrl.pathname); // scriptFileRelativePath = "index.ios.bundle"
            let updatedContent = this.sourceMapUtil.updateSourceMapFile(
                sourceMapBody,
                scriptFileRelativePath,
                this.sourcesStoragePath,
                this.packagerRemoteRoot,
                this.packagerLocalRoot,
            );
            return new FileSystem().writeFile(sourceMappingLocalPath, updatedContent);
        });
    }

    /**
     * Changes the port of the url to be the one configured as this.packagerPort
     */
    private overridePackagerPort(urlToOverride: string): string {
        let components = url.parse(urlToOverride);
        components.port = this.packagerPort.toString();
        delete components.host; // We delete the host, if not the port change will be ignored
        return url.format(components);
    }
}