Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Enable localhost connections in radicle-explorer
Rūdolfs Ošiņš committed 3 months ago
commit c72494f6208b371f5a2438d47dfdceb4693cfae1
parent 4c20d62
22 files changed +81 -113
modified config/custom-environment-variables.json
@@ -3,6 +3,7 @@
    "fallbackPublicExplorer": "FALLBACK_PUBLIC_EXPLORER",
    "requiredApiVersion": "REQUIRED_API_VERSION",
    "defaultHttpdPort": "DEFAULT_HTTPD_PORT",
+
    "defaultLocalHttpdPort": "DEFAULT_LOCAL_HTTPD_PORT",
    "defaultHttpdScheme": "DEFAULT_HTTPD_SCHEME"
  },
  "source": {
modified config/default.json
@@ -3,6 +3,7 @@
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
    "requiredApiVersion": "~0.18.0",
    "defaultHttpdPort": 443,
+
    "defaultLocalHttpdPort": 8080,
    "defaultHttpdScheme": "https"
  },
  "source": {
modified global.d.ts
@@ -3,6 +3,8 @@ import type { Config } from "./module.d.ts";
declare global {
  // eslint-disable-next-line no-var, @typescript-eslint/naming-convention
  var __CONFIG__: Config;
+
  // eslint-disable-next-line no-var, @typescript-eslint/naming-convention
+
  var __PLAYWRIGHT__: boolean;
}

export {};
modified http-client/tests/client.test.ts
@@ -1,11 +1,11 @@
import { describe, test } from "vitest";

import { HttpdClient } from "@http-client";
-
import { defaultHttpdPort } from "@tests/support/fixtures";
+
import config from "@app/lib/config";

const api = new HttpdClient({
  hostname: "127.0.0.1",
-
  port: defaultHttpdPort,
+
  port: config.nodes.defaultHttpdPort,
  scheme: "http",
});

modified http-client/tests/repo.test.ts
@@ -1,18 +1,18 @@
import { describe, test } from "vitest";

import { HttpdClient } from "@http-client";
+
import config from "@app/lib/config";
import {
  aliceMainHead,
  aliceRemote,
  cobRid,
-
  defaultHttpdPort,
  sourceBrowsingRid,
} from "@tests/support/fixtures.js";

describe("repo", () => {
  const api = new HttpdClient({
    hostname: "127.0.0.1",
-
    port: defaultHttpdPort,
+
    port: config.nodes.defaultHttpdPort,
    scheme: "http",
  });

modified src/lib/router.ts
@@ -145,37 +145,31 @@ export async function replace(newRoute: Route): Promise<void> {
  await navigate("replace", newRoute);
}

-
function extractBaseUrl(hostAndPort: string): BaseUrl {
-
  if (
-
    hostAndPort === "radicle.local" ||
-
    hostAndPort === `radicle.local:${config.nodes.defaultHttpdPort}` ||
-
    hostAndPort === "0.0.0.0" ||
-
    hostAndPort === `0.0.0.0:${config.nodes.defaultHttpdPort}` ||
-
    hostAndPort === "127.0.0.1" ||
-
    hostAndPort === `127.0.0.1:${config.nodes.defaultHttpdPort}`
-
  ) {
-
    return {
-
      hostname: "127.0.0.1",
-
      port: config.nodes.defaultHttpdPort,
-
      scheme: "http",
-
    };
-
  } else if (hostAndPort.includes(":")) {
-
    const [hostname, port] = hostAndPort.split(":");
-
    return {
-
      hostname,
-
      port: Number(port),
-
      scheme:
-
        utils.isLocal(hostname) || utils.isOnion(hostname)
-
          ? "http"
-
          : config.nodes.defaultHttpdScheme,
-
    };
+
export function extractBaseUrl(hostAndPort: string): BaseUrl {
+
  const [hostname, portString] = decodeURIComponent(hostAndPort).split(":");
+

+
  let port;
+

+
  if (portString !== undefined) {
+
    port = Number(portString);
+
  } else if (globalThis.__PLAYWRIGHT__ === true) {
+
    port = config.nodes.defaultHttpdPort;
  } else {
-
    return {
-
      hostname: hostAndPort,
-
      port: config.nodes.defaultHttpdPort,
-
      scheme: config.nodes.defaultHttpdScheme,
-
    };
+
    port = utils.isLocal(hostname)
+
      ? config.nodes.defaultLocalHttpdPort
+
      : config.nodes.defaultHttpdPort;
  }
+

+
  const scheme =
+
    utils.isLocal(hostname) || utils.isOnion(hostname)
+
      ? "http"
+
      : config.nodes.defaultHttpdScheme;
+

+
  return {
+
    hostname,
+
    port,
+
    scheme,
+
  };
}

function urlToRoute(url: URL): Route | null {
modified src/lib/utils.ts
@@ -205,11 +205,7 @@ export function isMarkdownPath(path: string): boolean {

// Check whether the given address is a localhost address.
export function isLocal(addr: string): boolean {
-
  return (
-
    addr.startsWith("127.0.0.1") ||
-
    addr.startsWith("0.0.0.0") ||
-
    addr.startsWith("radicle.local")
-
  );
+
  return addr.startsWith("127.0.0.1") || addr.startsWith("localhost");
}

// Check whether the given domain name is an onion domain name.
modified src/views/nodes/SeedSelector.svelte
@@ -13,7 +13,7 @@
    selectedSeed,
  } from "./SeedSelector";
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import { push } from "@app/lib/router";
+
  import { extractBaseUrl, push } from "@app/lib/router";

  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
@@ -45,11 +45,7 @@

  async function navigateToSeed() {
    loading = true;
-
    const seed = {
-
      hostname: seedAddressInput.trim(),
-
      port: config.nodes.defaultHttpdPort,
-
      scheme: config.nodes.defaultHttpdScheme,
-
    };
+
    const seed = extractBaseUrl(seedAddressInput.trim());
    validationMessage = await validateInput(seed);
    if (validationMessage === undefined) {
      closeFocused();
modified src/views/nodes/router.ts
@@ -4,7 +4,7 @@ import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
-
import { baseUrlToString, isLocal } from "@app/lib/utils";
+
import { baseUrlToString } from "@app/lib/utils";
import { handleError } from "@app/views/nodes/error";
import { unreachableError } from "@app/views/repos/error";
import { determineSeed } from "./SeedSelector";
@@ -53,17 +53,6 @@ export async function loadNodeRoute(

  const api = new HttpdClient(baseUrl);

-
  if (import.meta.env.PROD && isLocal(`${baseUrl.hostname}:${baseUrl.port}`)) {
-
    return {
-
      resource: "error",
-
      params: {
-
        icon: "device",
-
        title: "Local node browsing not supported",
-
        description: `You're trying to access a local node from your browser, we are currently working on a desktop app specific for this use case. Join our <strong>#desktop</strong> channel on <radicle-external-link href="${config.supportWebsite}">${config.supportWebsite}</radicle-external-link> for more information.`,
-
      },
-
    };
-
  }
-

  try {
    const [node, stats] = await Promise.all([api.getNode(), api.getStats()]);

modified src/views/repos/router.ts
@@ -29,7 +29,7 @@ import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { cached } from "@app/lib/cache";
import { handleError, unreachableError } from "@app/views/repos/error";
-
import { isLocal, unreachable } from "@app/lib/utils";
+
import { unreachable } from "@app/lib/utils";
import { nodePath } from "@app/views/nodes/router";

export const PATCHES_PER_PAGE = 10;
@@ -262,19 +262,6 @@ export async function loadRepoRoute(
  route: RepoRoute,
  previousLoaded: LoadedRoute,
): Promise<RepoLoadedRoute | ErrorRoute | NotFoundRoute> {
-
  if (
-
    import.meta.env.PROD &&
-
    isLocal(`${route.node.hostname}:${route.node.port}`)
-
  ) {
-
    return {
-
      resource: "error",
-
      params: {
-
        icon: "device",
-
        title: "Local node browsing not supported",
-
        description: `You're trying to access a repository on a local node from your browser, we are currently working on a desktop app specific for this use case. Join our <strong>#desktop</strong> channel on <radicle-external-link href="${config.supportWebsite}">${config.supportWebsite}</radicle-external-link> for more information.`,
-
      },
-
    };
-
  }
  const api = new HttpdClient(route.node);

  try {
modified src/views/users/router.ts
@@ -2,7 +2,6 @@ import type { BaseUrl, NodeIdentity, NodeStats } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";

import * as utils from "@app/lib/utils";
-
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { handleError } from "@app/views/nodes/error";
@@ -30,19 +29,6 @@ export async function loadUserRoute({
  did,
  baseUrl,
}: UserRoute): Promise<UserLoadedRoute | NotFoundRoute | ErrorRoute> {
-
  if (
-
    import.meta.env.PROD &&
-
    utils.isLocal(`${baseUrl.hostname}:${baseUrl.port}`)
-
  ) {
-
    return {
-
      resource: "error",
-
      params: {
-
        icon: "device",
-
        title: "Local node browsing not supported",
-
        description: `You're trying to access a local node from your browser, we are currently working on a desktop app specific for this use case. Join our <strong>#desktop</strong> channel on <radicle-external-link href="${config.supportWebsite}">${config.supportWebsite}</radicle-external-link> for more information.`,
-
      },
-
    };
-
  }
  const parsedDid = utils.parseNodeId(decodeURIComponent(did));
  if (!parsedDid) {
    return {
modified tests/build/smoke.spec.ts
@@ -5,7 +5,5 @@ test("exceptions in production build", async ({ page }) => {
  // Wait for scripts to finish executing, there might be exceptions that
  // happen after the page has been painted.
  await page.waitForTimeout(2000);
-
  await expect(
-
    page.getByText("Local node browsing not supported"),
-
  ).toBeVisible();
+
  await expect(page.getByText("Node not found")).toBeVisible();
});
modified tests/e2e/clipboard.spec.ts
@@ -47,7 +47,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
    await page.getByRole("button", { name: "Git" }).click();
    await page.getByText("git clone").locator(".clipboard").first().click();
    await expectClipboard(
-
      `git clone http://127.0.0.1/${sourceBrowsingRid.replace(
+
      `git clone http://localhost/${sourceBrowsingRid.replace(
        "rad:",
        "",
      )}.git source-browsing`,
modified tests/e2e/node.spec.ts
@@ -26,7 +26,7 @@ test("node metadata", async ({ page, peerManager }) => {
});

test("node repos", async ({ page }) => {
-
  await page.goto("/nodes/radicle.local");
+
  await page.goto("/nodes/localhost");
  const repo = page
    .locator(".repo-card", { hasText: "source-browsing" })
    .nth(0);
modified tests/e2e/router.ts
@@ -24,7 +24,7 @@ test("navigate between landing and repo page", async ({ page }) => {
});

test("navigation between node and repo pages", async ({ page }) => {
-
  await page.goto("/nodes/radicle.local");
+
  await page.goto("/nodes/localhost");

  const repo = page
    .locator(".repo-card", { hasText: "source-browsing" })
@@ -32,7 +32,7 @@ test("navigation between node and repo pages", async ({ page }) => {
  await repo.click();
  await expect(page).toHaveURL(sourceBrowsingUrl);

-
  await expectBackAndForwardNavigationWorks("/nodes/radicle.local", page);
+
  await expectBackAndForwardNavigationWorks("/nodes/localhost", page);
  await expectUrlPersistsReload(page);

  await page.getByRole("link", { name: "Local Node" }).click();
added tests/support/config.ts
@@ -0,0 +1,12 @@
+
// Test adapter for configuration in Node.js environment (Playwright tests).
+
//
+
// The app uses "virtual:config" (defined in vite.config.ts via
+
// vite-plugin-virtual), which is populated at build time with
+
// config.util.toObject() from the "config" npm package. Since Vite's virtual
+
// module system doesn't work in Node.js, this file provides the same
+
// configuration data by directly importing and converting the "config" package.
+
import nodeConfig from "config";
+

+
const config = nodeConfig.util.toObject();
+

+
export default config;
modified tests/support/fixtures.ts
@@ -30,6 +30,13 @@ export const test = base.extend<{
}>({
  forAllTests: [
    async ({ outputLog, page }, use) => {
+
      // Flag that tests are running so the app uses the test httpd port
+
      // (8081 from config.test.json) instead of the production default (8080)
+
      // for /nodes/localhost requests.
+
      await page.addInitScript(() => {
+
        globalThis.__PLAYWRIGHT__ = true;
+
      });
+

      const browserLabel = logLabel.logPrefix("browser");
      page.on("console", msg => {
        // Ignore common console logs that we don't care about.
@@ -602,11 +609,10 @@ export const shortBobHead = formatCommit(bobHead);
export const sourceBrowsingRid = "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir";
export const cobRid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe";
export const markdownRid = "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg";
-
export const sourceBrowsingUrl = `/nodes/127.0.0.1/${sourceBrowsingRid}`;
-
export const cobUrl = `/nodes/127.0.0.1/${cobRid}`;
-
export const markdownUrl = `/nodes/127.0.0.1/${markdownRid}`;
+
export const sourceBrowsingUrl = `/nodes/localhost/${sourceBrowsingRid}`;
+
export const cobUrl = `/nodes/localhost/${cobRid}`;
+
export const markdownUrl = `/nodes/localhost/${markdownRid}`;
export const shortNodeRemote = "z6MktU…1xB22S";
-
export const defaultHttpdPort = 8081;
export const gitOptions = {
  alice: {
    GIT_AUTHOR_NAME: "Alice Liddell",
modified tests/support/globalSetup.ts
@@ -12,9 +12,9 @@ import {
  createCobsFixture,
  createMarkdownFixture,
  createSourceBrowsingFixture,
-
  defaultHttpdPort,
  gitOptions,
} from "@tests/support/fixtures.js";
+
import config from "@tests/support/config.js";
import { createPeerManager } from "@tests/support/peerManager.js";

const heartwoodBinaryPath = Path.join(
@@ -87,7 +87,7 @@ export default async function globalSetup(): Promise<() => void> {
        alias: "palm",
      },
    });
-
    await palm.startHttpd(defaultHttpdPort);
+
    await palm.startHttpd(config.nodes.defaultHttpdPort);

    try {
      console.log("Creating source-browsing fixture");
@@ -108,7 +108,7 @@ export default async function globalSetup(): Promise<() => void> {
    }
    await palm.stopNode();
  } else {
-
    await palm.startHttpd(defaultHttpdPort);
+
    await palm.startHttpd(config.nodes.defaultHttpdPort);
  }

  return async () => {
modified tests/unit/router.test.ts
@@ -1,6 +1,6 @@
-
import { defaultHttpdPort } from "@tests/support/fixtures.js";
import { describe, expect, test } from "vitest";
import { testExports, type Route } from "@app/lib/router";
+
import config from "@tests/support/config.js";

// Defining the window.origin value, since vitest doesn't provide one.
window.origin = "http://localhost:3000";
@@ -217,7 +217,7 @@ describe("pathToRoute", () => {
        baseUrl: {
          hostname: "example.node.tld",
          scheme: "http",
-
          port: defaultHttpdPort,
+
          port: config.nodes.defaultHttpdPort,
        },
        repoPageIndex: 0,
      },
@@ -232,7 +232,7 @@ describe("pathToRoute", () => {
        node: {
          hostname: "example.node.tld",
          scheme: "http",
-
          port: defaultHttpdPort,
+
          port: config.nodes.defaultHttpdPort,
        },
        repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
        route: "",
@@ -248,7 +248,7 @@ describe("pathToRoute", () => {
        node: {
          hostname: "example.node.tld",
          scheme: "http",
-
          port: defaultHttpdPort,
+
          port: config.nodes.defaultHttpdPort,
        },
        repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
        route: "",
modified tests/visual/desktop/node.spec.ts
@@ -2,7 +2,7 @@ import { test, expect } from "@tests/support/fixtures.js";

test("node page", async ({ page }) => {
  await page.clock.setFixedTime(new Date("November 24 2022 12:00:00"));
-
  await page.goto("/nodes/radicle.local", { waitUntil: "networkidle" });
+
  await page.goto("/nodes/localhost", { waitUntil: "networkidle" });
  await expect(page).toHaveScreenshot();
});

@@ -38,7 +38,7 @@ test("response parse error", async ({ page }) => {
    });
  });

-
  await page.goto("/nodes/radicle.local", {
+
  await page.goto("/nodes/localhost", {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
@@ -51,7 +51,7 @@ test("response error", async ({ page }) => {
    });
  });

-
  await page.goto("/nodes/radicle.local", {
+
  await page.goto("/nodes/localhost", {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
modified tests/visual/desktop/user.spec.ts
@@ -7,14 +7,14 @@ import {

test("user page", async ({ page }) => {
  await page.clock.setFixedTime(new Date("November 24 2022 12:00:00"));
-
  await page.goto(`/nodes/radicle.local/users/${aliceRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${aliceRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
});

test("empty pinned repos", async ({ page }) => {
-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
@@ -27,7 +27,7 @@ test("response parse error", async ({ page }) => {
    });
  });

-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
@@ -40,7 +40,7 @@ test("response error", async ({ page }) => {
    });
  });

-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
modified tests/visual/mobile/user.spec.ts
@@ -7,14 +7,14 @@ import {

test("user page", async ({ page }) => {
  await page.clock.setFixedTime(new Date("November 24 2022 12:00:00"));
-
  await page.goto(`/nodes/radicle.local/users/${aliceRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${aliceRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
});

test("empty pinned repos", async ({ page }) => {
-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
@@ -27,7 +27,7 @@ test("response parse error", async ({ page }) => {
    });
  });

-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();
@@ -40,7 +40,7 @@ test("response error", async ({ page }) => {
    });
  });

-
  await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
+
  await page.goto(`/nodes/localhost/users/${bobRemote}`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot();