Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Refactor config into virtual modules
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

This patch moves the config files into a new config folder, this allows the new config package to detect it and based on a priority list and compute a final config. The final config is provided to the app via virtual modules that are created on build time and provided to the app during run time.

We define also a new NODE_CONFIG_ENV env variable which is used by the config package to overwrite certain parts of the config for e.g. testing. This allows us also to clean up any config changes we do in the test suites and have everything in the same place, aka the config dir.

Also since this new env var removes the need for window.PLAYWRIGHT and window.VITEST, I went the extra step and was able to remove all the injections into the window object we did, which is just cleaner. Added additionally a custom-environment-variables.json mapping file which defines env vars for each of the config options. Some are pretty straight forward, others require to serialize the values into a json string to be parsed.

The error behavior is pretty gracefully and in all cases always fallback to our default.json file instead of throwing build or run time errors.

check check-visual check-unit-test check-httpd-api-unit-test check-e2e check-build

👉 Preview 👉 Workflow runs 👉 Branch on GitHub

40 files changed +266 -308 ce8d6f6d 15e258da
modified .gitignore
@@ -1,6 +1,7 @@
/build/
node_modules/
NOTES
+
config/local*

# KaTeX files
*.min.css
modified README.md
@@ -40,6 +40,24 @@ There are several ways to deploy the UI publicly. Here are two common options:
1. Fork this repository to create your own version
2. Configure your Vercel account to deploy the forked repository

+
## Configuration
+

+
There's two ways to configure the UI:
+

+
**Create a `local.json` config file**
+

+
1. Copy [default.json][def] to a new file in the same folder called
+
   `local.json`.
+
2. Modify the properties in `local.json` to your preference.
+

+
**Environment variables**
+

+
1. Check [custom-environment-variables.json][env] for all available environment
+
   variables.
+
2. Set the desired environment variables when building the UI.
+

+
> For advanced configuration options, have a look at the [`node-config`][nco]
+
> package.

## Contributing

@@ -63,8 +81,11 @@ The UI is distributed under the terms of GPLv3. See [LICENSE][lic] for details.

[app]: https://app.radicle.xyz
[con]: ./CONTRIBUTING.md
+
[def]: ./config/default.json
+
[env]: ./config/custom-environment-variables.json
[iss]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/issues
[lic]: ./LICENSE
+
[nco]: https://github.com/node-config/node-config/wiki/Configuration-Files
[nod]: https://nodejs.org
[npm]: https://www.npmjs.com
[pat]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/patches
added config/custom-environment-variables.json
@@ -0,0 +1,24 @@
+
{
+
  "nodes": {
+
    "fallbackPublicExplorer": "FALLBACK_PUBLIC_EXPLORER",
+
    "apiVersion": "API_VERSION",
+
    "defaultHttpdPort": "DEFAULT_HTTPD_PORT",
+
    "defaultLocalHttpdPort": "DEFAULT_LOCAL_HTTPD_PORT",
+
    "defaultHttpdHostname": "DEFAULT_HTTPD_HOSTNAME",
+
    "defaultHttpdScheme": "DEFAULT_HTTPD_SCHEME",
+
    "defaultNodePort": "DEFAULT_NODE_PORT",
+
    "pinned": {
+
      "__name": "PINNED_NODES",
+
      "__format": "json"
+
    }
+
  },
+
  "supportWebsite": "SUPPORT_WEBSITE",
+
  "reactions": {
+
    "__name": "REACTIONS",
+
    "__format": "json"
+
  },
+
  "fallbackPreferredSeed": {
+
    "__name": "FALLBACK_PREFERRED_SEED",
+
    "__format": "json"
+
  }
+
}
added config/default.json
@@ -0,0 +1,27 @@
+
{
+
  "nodes": {
+
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
+
    "apiVersion": "0.1.0",
+
    "defaultHttpdPort": 443,
+
    "defaultLocalHttpdPort": 8080,
+
    "defaultHttpdHostname": "seed.radicle.garden",
+
    "defaultHttpdScheme": "https",
+
    "defaultNodePort": 8776,
+
    "pinned": [
+
      {
+
        "baseUrl": {
+
          "hostname": "seed.radicle.xyz",
+
          "port": 443,
+
          "scheme": "https"
+
        }
+
      }
+
    ]
+
  },
+
  "supportWebsite": "https://radicle.zulipchat.com",
+
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
+
  "fallbackPreferredSeed": {
+
    "hostname": "seed.radicle.garden",
+
    "port": 443,
+
    "scheme": "https"
+
  }
+
}
added config/production.json
@@ -0,0 +1 @@
+
default.json

\ No newline at end of file
added config/test.json
@@ -0,0 +1,17 @@
+
{
+
  "nodes": {
+
    "defaultHttpdPort": 8081,
+
    "defaultLocalHttpdPort": 8081,
+
    "defaultHttpdHostname": "127.0.0.1",
+
    "defaultHttpdScheme": "http",
+
    "pinned": [
+
      {
+
        "baseUrl": {
+
          "hostname": "127.0.0.1",
+
          "port": 8081,
+
          "scheme": "http"
+
        }
+
      }
+
    ]
+
  }
+
}
modified httpd-client/lib/fetcher.ts
@@ -3,7 +3,7 @@

import type { ZodIssue, ZodType, TypeOf } from "zod";

-
import { config } from "@app/lib/config";
+
import config from "virtual:config";
import { compare } from "compare-versions";

export interface BaseUrl {
modified httpd-client/vite.config.ts
@@ -1,7 +1,14 @@
-
import { defineConfig } from "vite";
+
import nodeConfig from "config";
import path from "node:path";
+
import virtual from "vite-plugin-virtual";
+
import { defineConfig } from "vite";

export default defineConfig({
+
  plugins: [
+
    virtual({
+
      "virtual:config": nodeConfig.util.toObject(),
+
    }),
+
  ],
  test: {
    environment: "happy-dom",
    include: ["httpd-client/tests/*.test.ts"],
added module.d.ts
@@ -0,0 +1,19 @@
+
declare module "virtual:*" {
+
  const config: {
+
    nodes: {
+
      apiVersion: string;
+
      fallbackPublicExplorer: string;
+
      defaultHttpdPort: number;
+
      defaultHttpdHostname: string;
+
      defaultLocalHttpdPort: number;
+
      defaultNodePort: number;
+
      defaultHttpdScheme: string;
+
      pinned: { baseUrl: BaseUrl }[];
+
    };
+
    reactions: string[];
+
    supportWebsite: string;
+
    fallbackPreferredSeed: BaseUrl;
+
  };
+

+
  export default config;
+
}
modified package-lock.json
@@ -31,19 +31,19 @@
      },
      "devDependencies": {
        "@playwright/test": "^1.42.1",
-
        "@sinonjs/fake-timers": "^11.2.2",
        "@sveltejs/vite-plugin-svelte": "^3.0.2",
        "@tsconfig/svelte": "^5.0.3",
+
        "@types/config": "^3.3.4",
        "@types/dompurify": "^3.0.5",
        "@types/katex": "^0.16.7",
        "@types/lodash": "^4.17.0",
        "@types/md5": "^2.3.5",
        "@types/node": "^20.11.30",
        "@types/sinon": "^17.0.3",
-
        "@types/sinonjs__fake-timers": "^8.1.5",
        "@types/wait-on": "^5.3.4",
        "@typescript-eslint/eslint-plugin": "^7.4.0",
        "chalk": "^5.3.0",
+
        "config": "^3.3.11",
        "eslint": "^8.57.0",
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-no-only-tests": "^3.1.0",
@@ -57,6 +57,7 @@
        "svelte-check": "^3.6.8",
        "typescript": "^5.4.3",
        "vite": "^5.2.6",
+
        "vite-plugin-virtual": "^0.3.0",
        "vitest": "^1.4.0",
        "wait-on": "^7.2.0"
      },
@@ -1022,6 +1023,12 @@
      "integrity": "sha512-Ms0t9K0oxioSb0lrZ5NRysx0nE/KsojYOG+db9v6wSaU/+P37vc0WRmh1QE1c8IAtTniD4yEhffGQuTKF8uaPw==",
      "dev": true
    },
+
    "node_modules/@types/config": {
+
      "version": "3.3.4",
+
      "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.4.tgz",
+
      "integrity": "sha512-qFiTLnWy+TdPSMIXFHP+87lFXFRM4SXjRS+CSB66+56TrpLNw003y1sh7DGaaC1NGesxgKoT5FDy6dyA1Xju/g==",
+
      "dev": true
+
    },
    "node_modules/@types/dompurify": {
      "version": "3.0.5",
      "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
@@ -1896,6 +1903,18 @@
      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
      "dev": true
    },
+
    "node_modules/config": {
+
      "version": "3.3.11",
+
      "resolved": "https://registry.npmjs.org/config/-/config-3.3.11.tgz",
+
      "integrity": "sha512-Dhn63ZoWCW5EMg4P0Sl/XNsj/7RLiUIA1x1npCy+m2cRwRHzLnt3UtYtxRDMZW/6oOMdWhCzaGYkOcajGgrAOA==",
+
      "dev": true,
+
      "dependencies": {
+
        "json5": "^2.2.3"
+
      },
+
      "engines": {
+
        "node": ">= 10.0.0"
+
      }
+
    },
    "node_modules/cross-spawn": {
      "version": "7.0.3",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3161,6 +3180,18 @@
      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
      "dev": true
    },
+
    "node_modules/json5": {
+
      "version": "2.2.3",
+
      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+
      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+
      "dev": true,
+
      "bin": {
+
        "json5": "lib/cli.js"
+
      },
+
      "engines": {
+
        "node": ">=6"
+
      }
+
    },
    "node_modules/jsonc-parser": {
      "version": "3.2.1",
      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
@@ -5019,6 +5050,15 @@
        "url": "https://opencollective.com/vitest"
      }
    },
+
    "node_modules/vite-plugin-virtual": {
+
      "version": "0.3.0",
+
      "resolved": "https://registry.npmjs.org/vite-plugin-virtual/-/vite-plugin-virtual-0.3.0.tgz",
+
      "integrity": "sha512-TOtrWw6jKrJNXfxhGRUiQzfAP1gRkYkVzMkJNjHUJ8idLuxf8eeeDKZKZHhdeYfaCc/87rv+KvWE2iCy1QInWA==",
+
      "dev": true,
+
      "peerDependencies": {
+
        "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
+
      }
+
    },
    "node_modules/vite/node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
modified package.json
@@ -8,8 +8,8 @@
    "check": "scripts/check",
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
    "test:unit": "TZ='UTC' vitest run",
-
    "test:e2e": "TZ='UTC' playwright test",
-
    "test:httpd-api:unit": "TZ='UTC' vitest run --config httpd-client/vite.config.ts --reporter verbose"
+
    "test:e2e": "NODE_CONFIG_ENV='test' TZ='UTC' playwright test",
+
    "test:httpd-api:unit": "NODE_CONFIG_ENV='test' TZ='UTC' vitest run --config httpd-client/vite.config.ts --reporter verbose"
  },
  "type": "module",
  "engines": {
@@ -17,19 +17,19 @@
  },
  "devDependencies": {
    "@playwright/test": "^1.42.1",
-
    "@sinonjs/fake-timers": "^11.2.2",
    "@sveltejs/vite-plugin-svelte": "^3.0.2",
    "@tsconfig/svelte": "^5.0.3",
+
    "@types/config": "^3.3.4",
    "@types/dompurify": "^3.0.5",
    "@types/katex": "^0.16.7",
    "@types/lodash": "^4.17.0",
    "@types/md5": "^2.3.5",
    "@types/node": "^20.11.30",
    "@types/sinon": "^17.0.3",
-
    "@types/sinonjs__fake-timers": "^8.1.5",
    "@types/wait-on": "^5.3.4",
    "@typescript-eslint/eslint-plugin": "^7.4.0",
    "chalk": "^5.3.0",
+
    "config": "^3.3.11",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-no-only-tests": "^3.1.0",
@@ -43,6 +43,7 @@
    "svelte-check": "^3.6.8",
    "typescript": "^5.4.3",
    "vite": "^5.2.6",
+
    "vite-plugin-virtual": "^0.3.0",
    "vitest": "^1.4.0",
    "wait-on": "^7.2.0"
  },
modified playwright.config.ts
@@ -4,7 +4,6 @@ import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = {
  testDir: "./tests/e2e",
  outputDir: "./tests/artifacts",
-
  testIgnore: "hashRouter.spec.ts",
  timeout: 30_000,
  expect: {
    timeout: 8000,
modified src/App.svelte
@@ -31,7 +31,7 @@

  void httpd.initialize().finally(() => void router.loadFromLocation());

-
  if (!window.VITEST && !window.PLAYWRIGHT && import.meta.env.PROD) {
+
  if (import.meta.env.PROD) {
    const plausible = Plausible({ domain: "app.radicle.xyz" });

    plausible.enableAutoPageviews();
modified src/App/Settings.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import { api, changeHttpdPort } from "@app/lib/httpd";
-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";
  import {
    codeFont,
    codeFonts,
modified src/components/ErrorMessage.svelte
@@ -2,7 +2,7 @@
  import type { ComponentProps } from "svelte";
  import type { ErrorParam } from "@app/lib/router/definitions";

-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";
  import Command from "./Command.svelte";
  import ExternalLink from "./ExternalLink.svelte";
  import Icon from "./Icon.svelte";
modified src/components/ReactionSelector.svelte
@@ -3,7 +3,7 @@

  import { createEventDispatcher } from "svelte";

-
  import config from "@app/config.json";
+
  import config from "virtual:config";

  import IconButton from "./IconButton.svelte";
  import IconSmall from "./IconSmall.svelte";
deleted src/config.json
@@ -1,27 +0,0 @@
-
{
-
  "nodes": {
-
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
-
    "apiVersion": "0.1.0",
-
    "defaultHttpdPort": 443,
-
    "defaultLocalHttpdPort": 8080,
-
    "defaultHttpdHostname": "seed.radicle.garden",
-
    "defaultHttpdScheme": "https",
-
    "defaultNodePort": 8776,
-
    "pinned": [
-
      {
-
        "baseUrl": {
-
          "hostname": "seed.radicle.xyz",
-
          "port": 443,
-
          "scheme": "https"
-
        }
-
      }
-
    ]
-
  },
-
  "supportWebsite": "https://radicle.zulipchat.com",
-
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
-
  "fallbackPreferredSeed": {
-
    "hostname": "seed.radicle.garden",
-
    "port": 443,
-
    "scheme": "https"
-
  }
-
}
deleted src/global.d.ts
@@ -1,25 +0,0 @@
-
/* eslint-disable @typescript-eslint/naming-convention */
-
import type { Config } from "@app/lib/config";
-
import type { FakeTimers } from "@sinonjs/fake-timers";
-

-
declare global {
-
  interface Window {
-
    // Defined in vite.config.ts and are available in all environments except
-
    // production.
-
    VITEST: boolean;
-
    PLAYWRIGHT: boolean;
-

-
    // APP_CONFIG is set from within Playwright tests at runtime.
-
    // To better understand how it works together, have a look at:
-
    //   tests/support/fixtures.ts
-
    //   src/config.ts
-
    APP_CONFIG: Config;
-
    // eslint-disable-next-line @typescript-eslint/ban-types
-
    initializeTestStubs?: Function;
-
    e2eTestStubs: {
-
      FakeTimers: FakeTimers;
-
    };
-
  }
-
}
-

-
export {};
modified src/index.ts
@@ -1,12 +1,3 @@
-
import * as FakeTimers from "@sinonjs/fake-timers";
-

-
if (window.PLAYWRIGHT && window.initializeTestStubs !== undefined) {
-
  window.e2eTestStubs = {
-
    FakeTimers: FakeTimers,
-
  };
-
  window.initializeTestStubs();
-
}
-

import App from "@app/App.svelte";

const app = new App({
deleted src/lib/config.ts
@@ -1,48 +0,0 @@
-
import type { BaseUrl } from "@httpd-client";
-

-
import configJson from "@app/config.json";
-

-
export interface Config {
-
  nodes: {
-
    apiVersion: string;
-
    fallbackPublicExplorer: string;
-
    defaultHttpdPort: number;
-
    defaultHttpdHostname: string;
-
    defaultLocalHttpdPort: number;
-
    defaultNodePort: number;
-
    defaultHttpdScheme: string;
-
    pinned: { baseUrl: BaseUrl }[];
-
  };
-
  supportWebsite: string;
-
  fallbackPreferredSeed: BaseUrl;
-
}
-

-
function getConfig(): Config {
-
  if (window.VITEST) {
-
    return {
-
      nodes: {
-
        fallbackPublicExplorer: "https://app.radicle.xyz/nodes/$host/$rid$path",
-
        apiVersion: "0.1.0",
-
        defaultHttpdHostname: "127.0.0.1",
-
        defaultHttpdPort: 8081,
-
        defaultLocalHttpdPort: 8081,
-
        defaultHttpdScheme: "http",
-
        defaultNodePort: 8776,
-
        pinned: [],
-
      },
-
      supportWebsite: "https://radicle.zulipchat.com",
-
      fallbackPreferredSeed: {
-
        hostname: "seed.radicle.garden",
-
        port: 443,
-
        scheme: "https",
-
      },
-
    };
-
  } else if (window.PLAYWRIGHT) {
-
    return window.APP_CONFIG;
-
  } else {
-
    // In dev and production environments we use data from config.json.
-
    return configJson;
-
  }
-
}
-

-
export const config = getConfig();
modified src/lib/httpd.ts
@@ -4,7 +4,7 @@ import { get, writable } from "svelte/store";
import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";

import { HttpdClient } from "@httpd-client";
-
import { config } from "@app/lib/config";
+
import config from "virtual:config";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { experimental } from "./appearance";

modified src/lib/router.ts
@@ -5,7 +5,7 @@ import { get, writable } from "svelte/store";

import * as mutexExecutor from "@app/lib/mutexExecutor";
import * as utils from "@app/lib/utils";
-
import { config } from "@app/lib/config";
+
import config from "virtual:config";
import {
  projectRouteToPath,
  projectTitle,
modified src/lib/seeds.ts
@@ -5,7 +5,7 @@ import { writable, derived, get } from "svelte/store";
import { number, string, object } from "zod";

import { api, httpdStore, type HttpdState } from "./httpd";
-
import { config } from "./config";
+
import config from "virtual:config";

const preferredSeedSchema = object({
  hostname: string(),
modified src/modals/ErrorModal.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";

  import Command from "@app/components/Command.svelte";
  import ExternalLink from "@app/components/ExternalLink.svelte";
modified src/views/nodes/router.ts
@@ -2,7 +2,7 @@ import type { BaseUrl, NodeStats, Policy, Scope } from "@httpd-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";

import { HttpdClient } from "@httpd-client";
-
import { config } from "@app/lib/config";
+
import config from "virtual:config";
import { baseUrlToString } from "@app/lib/utils";
import { handleError } from "@app/views/nodes/error";

modified src/views/projects/Header/CloneButton.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { BaseUrl } from "@httpd-client";

-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";
  import { parseRepositoryId } from "@app/lib/utils";

  import Button from "@app/components/Button.svelte";
modified src/views/projects/Header/ShareButton.svelte
@@ -5,7 +5,7 @@

  import { activeUnloadedRouteStore, routeToPath } from "@app/lib/router";
  import { api } from "@app/lib/httpd";
-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";
  import { formatPublicExplorer } from "@app/lib/utils";
  import { queryProject } from "@app/lib/projects";

modified src/views/projects/Share.svelte
@@ -4,7 +4,7 @@
  import debounce from "lodash/debounce";
  import { api, httpdStore } from "@app/lib/httpd";
  import { isLocal, toClipboard } from "@app/lib/utils";
-
  import { config } from "@app/lib/config";
+
  import config from "virtual:config";

  import Button from "@app/components/Button.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
modified tests/e2e/landingPage.spec.ts
@@ -1,12 +1,7 @@
-
import { appConfigWithFixture, expect, test } from "@tests/support/fixtures.js";
-

-
test.use({
-
  customAppConfig: true,
-
});
+
import { expect, test } from "@tests/support/fixtures.js";

test("show pinned projects", async ({ page }) => {
  await page.addInitScript(() => localStorage.setItem("experimental", "true"));
-
  await page.addInitScript(appConfigWithFixture);
  await page.goto("/");
  await expect(page.getByText("Local projects")).toBeVisible();

modified tests/e2e/project/commit.spec.ts
@@ -6,6 +6,7 @@ import {
  sourceBrowsingUrl,
  test,
} from "@tests/support/fixtures.js";
+
import sinon from "sinon";

const commitUrl = `${sourceBrowsingUrl}/commits/${bobHead}`;

@@ -21,13 +22,11 @@ test("navigation from commit list", async ({ page }) => {

test("relative timestamps", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("December 21 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("December 21 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });
  await page.goto(commitUrl);
  await expect(
modified tests/e2e/project/commits.spec.ts
@@ -7,6 +7,7 @@ import {
  test,
} from "@tests/support/fixtures.js";
import { createProject } from "@tests/support/project";
+
import sinon from "sinon";

test("peer and branch switching", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);
@@ -102,13 +103,11 @@ test("expand commit message", async ({ page }) => {

test("relative timestamps", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("December 21 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("December 21 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(sourceBrowsingUrl);
modified tests/e2e/router.ts
@@ -1,7 +1,6 @@
import {
  aliceMainHead,
  aliceRemote,
-
  appConfigWithFixture,
  expect,
  sourceBrowsingUrl,
  test,
@@ -13,8 +12,6 @@ import {
} from "@tests/support/router.js";

test("navigate between landing and project page", async ({ page }) => {
-
  await page.addInitScript(appConfigWithFixture);
-

  await page.goto("/");
  await expect(page).toHaveURL("/");

modified tests/support/fixtures.ts
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type * as Stream from "node:stream";
+

import * as Fs from "node:fs/promises";
import * as Path from "node:path";
import assert from "node:assert";
+
import { fileURLToPath } from "node:url";
import { test as base, expect } from "@playwright/test";

import * as Process from "./process.js";
@@ -22,17 +24,22 @@ const fixturesDir = Path.resolve(supportDir, "..", "./fixtures");
export const test = base.extend<{
  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
  forAllTests: void;
-
  customAppConfig: boolean;
  stateDir: string;
  peerManager: PeerManager;
  authenticatedPeer: RadiclePeer;
  outputLog: Stream.Writable;
}>({
-
  customAppConfig: [false, { option: true }],
-

  forAllTests: [
-
    async ({ customAppConfig, outputLog, page }, use) => {
+
    async ({ outputLog, page }, use) => {
      const browserLabel = logLabel.logPrefix("browser");
+
      let sinonPath = fileURLToPath(import.meta.resolve("sinon"));
+
      // The exports in sinon-esm.js mess up our test pipeline
+
      if (sinonPath.endsWith("-esm.js")) {
+
        sinonPath = sinonPath.replace("-esm", "");
+
      }
+
      await page.addInitScript({
+
        path: sinonPath,
+
      });
      page.on("console", msg => {
        // Ignore common console logs that we don't care about.
        if (
@@ -68,41 +75,6 @@ export const test = base.extend<{
        });
      }

-
      if (!customAppConfig) {
-
        // Remember: `page.addInitScript()` is run in the browser which
-
        // is completely isolated from the test environment, so we don't have
-
        // access to any variables that we have in the test.
-
        await page.addInitScript(() => {
-
          window.APP_CONFIG = {
-
            nodes: {
-
              fallbackPublicExplorer:
-
                "https://app.radicle.xyz/nodes/$host/$rid$path",
-
              apiVersion: "0.1.0",
-
              defaultHttpdPort: 8081,
-
              defaultHttpdHostname: "127.0.0.1",
-
              defaultLocalHttpdPort: 8081,
-
              defaultHttpdScheme: "http",
-
              defaultNodePort: 8776,
-
              pinned: [
-
                {
-
                  baseUrl: {
-
                    hostname: "127.0.0.1",
-
                    port: 8081,
-
                    scheme: "http",
-
                  },
-
                },
-
              ],
-
            },
-
            supportWebsite: "https://radicle.zulipchat.com",
-
            fallbackPreferredSeed: {
-
              hostname: "seed.radicle.garden",
-
              port: 443,
-
              scheme: "https",
-
            },
-
          };
-
        });
-
      }
-

      const playwrightLabel = logLabel.logPrefix("playwright");

      function isLocalhost(url: URL) {
@@ -211,35 +183,6 @@ function log(text: string, label: string, outputLog: Stream.Writable) {
  }
}

-
export function appConfigWithFixture(defaultLocalHttpdPort = 8081) {
-
  window.APP_CONFIG = {
-
    nodes: {
-
      fallbackPublicExplorer: "https://app.radicle.xyz/nodes/$host/$rid$path",
-
      apiVersion: "0.1.0",
-
      defaultHttpdPort: 8081,
-
      defaultHttpdHostname: "127.0.0.1",
-
      defaultLocalHttpdPort,
-
      defaultHttpdScheme: "http",
-
      defaultNodePort: 8776,
-
      pinned: [
-
        {
-
          baseUrl: {
-
            hostname: "127.0.0.1",
-
            port: 8081,
-
            scheme: "http",
-
          },
-
        },
-
      ],
-
    },
-
    supportWebsite: "https://radicle.zulipchat.com",
-
    fallbackPreferredSeed: {
-
      hostname: "seed.radicle.garden",
-
      port: 443,
-
      scheme: "https",
-
    },
-
  };
-
}
-

export async function createSourceBrowsingFixture(
  peerManager: PeerManager,
  palm: RadiclePeer,
modified tests/visual/desktop/cob.spec.ts
@@ -1,14 +1,13 @@
import { test, expect, cobUrl } from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test.beforeEach(async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });
});

modified tests/visual/desktop/landingPage.spec.ts
@@ -1,49 +1,38 @@
-
import { test, expect, appConfigWithFixture } from "@tests/support/fixtures.js";
-

-
test.use({
-
  customAppConfig: true,
-
});
+
import { test, expect } from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test("pinned projects", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

-
  await page.addInitScript(appConfigWithFixture);
  await page.goto("/", { waitUntil: "networkidle" });
  await expect(page).toHaveScreenshot();
});

-
test("load error", async ({ page }) => {
+
test("load projects error", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.route(
-
    ({ pathname }) =>
-
      pathname === "/api/v1/projects/rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir",
+
    ({ pathname }) => pathname === "/api/v1/projects",
    route => route.fulfill({ status: 500 }),
  );

-
  await page.addInitScript(appConfigWithFixture, 8090);
  await page.goto("/", { waitUntil: "networkidle" });
  await expect(page).toHaveScreenshot();
});

test("response parse error", async ({ page }) => {
-
  await page.addInitScript(appConfigWithFixture);
  await page.route("*/**/v1/projects*", route => {
    return route.fulfill({
      json: [{ name: 1337 }],
@@ -54,7 +43,6 @@ test("response parse error", async ({ page }) => {
});

test("response error", async ({ page }) => {
-
  await page.addInitScript(appConfigWithFixture);
  await page.route("*/**/v1/projects*", route => {
    return route.fulfill({
      status: 500,
modified tests/visual/desktop/node.spec.ts
@@ -1,14 +1,13 @@
import { test, expect } from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test("node page", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto("/nodes/radicle.local", { waitUntil: "networkidle" });
modified tests/visual/desktop/project.spec.ts
@@ -7,6 +7,7 @@ import {
  markdownUrl,
  sourceBrowsingRid,
} from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test("source page", async ({ page }) => {
  await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
@@ -15,13 +16,11 @@ test("source page", async ({ page }) => {

test("history page", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(
@@ -36,13 +35,11 @@ test("history page", async ({ page }) => {

test("commit page", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(
@@ -56,13 +53,11 @@ test("commit page", async ({ page }) => {

test("diff selection", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(`${cobUrl}/patches`);
modified tests/visual/mobile/cob.spec.ts
@@ -1,14 +1,13 @@
import { test, expect, cobUrl } from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test.beforeEach(async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });
});

modified tests/visual/mobile/project.spec.ts
@@ -4,6 +4,7 @@ import {
  sourceBrowsingUrl,
  test,
} from "@tests/support/fixtures.js";
+
import sinon from "sinon";

test("source tree page", async ({ page }) => {
  await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
@@ -12,13 +13,11 @@ test("source tree page", async ({ page }) => {

test("commits page", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(
@@ -33,13 +32,11 @@ test("commits page", async ({ page }) => {

test("commit page", async ({ page }) => {
  await page.addInitScript(() => {
-
    window.initializeTestStubs = () => {
-
      window.e2eTestStubs.FakeTimers.install({
-
        now: new Date("November 24 2022 12:00:00").valueOf(),
-
        shouldClearNativeTimers: true,
-
        shouldAdvanceTime: false,
-
      });
-
    };
+
    sinon.useFakeTimers({
+
      now: new Date("November 24 2022 12:00:00").valueOf(),
+
      shouldClearNativeTimers: true,
+
      shouldAdvanceTime: false,
+
    });
  });

  await page.goto(
modified vite.config.ts
@@ -1,4 +1,6 @@
+
import config from "config";
import path from "node:path";
+
import virtual from "vite-plugin-virtual";
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";

@@ -9,6 +11,9 @@ export default defineConfig({
    reporters: "verbose",
  },
  plugins: [
+
    virtual({
+
      "virtual:config": config.util.toObject(),
+
    }),
    svelte({
      // Reference: https://github.com/sveltejs/vite-plugin-svelte/issues/270#issuecomment-1033190138
      dynamicCompileOptions({ filename }) {
@@ -56,9 +61,4 @@ export default defineConfig({
      },
    },
  },
-

-
  define: {
-
    VITEST: process.env.VITEST !== undefined,
-
    PLAYWRIGHT: process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined,
-
  },
});