Merge pull request 'Rebranding features' (#636) from mmalmi/snort:main into main
continuous-integration/drone/push Build is failing Details

Reviewed-on: #636
This commit is contained in:
Kieran 2023-09-27 10:35:45 +00:00
commit a780b9554d
66 changed files with 168 additions and 81 deletions

View File

@ -0,0 +1 @@
Choose config with NODE_CONFIG_ENV: `NODE_CONFIG_ENV=iris yarn start`

View File

@ -0,0 +1,7 @@
{
"appName": "Snort",
"appNameCapitalized": "Snort",
"appTitle": "Snort - Nostr",
"nip05Domain": "snort.social",
"favicon": "public/favicon.ico"
}

View File

@ -0,0 +1,7 @@
{
"appName": "iris",
"appNameCapitalized": "Iris",
"appTitle": "iris",
"nip05Domain": "iris.to",
"favicon": "public/iris.ico"
}

View File

@ -88,6 +88,7 @@
"@webpack-cli/generators": "^3.0.4",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"babel-loader": "^9.1.3",
"config": "^3.3.9",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",

View File

@ -11,7 +11,7 @@
<link rel="preconnect" href="https://imgproxy.snort.social" />
<link rel="apple-touch-icon" href="/nostrich_512.png" />
<link rel="manifest" href="/manifest.json" />
<title>Snort - Nostr</title>
<title><%= htmlWebpackPlugin.options.templateParameters.appTitle %></title>
</head>
<body>
<div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -6,6 +6,7 @@ interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
disabled?: boolean;
onClick(e: React.MouseEvent): Promise<void> | void;
children?: React.ReactNode;
ref?: React.Ref<HTMLButtonElement>;
}
export default function AsyncButton(props: AsyncButtonProps) {
@ -28,7 +29,7 @@ export default function AsyncButton(props: AsyncButtonProps) {
}
return (
<button className="spinner-button" type="button" disabled={loading || props.disabled} {...props} onClick={handle}>
<button ref={props.ref} className="spinner-button" type="button" disabled={loading || props.disabled} {...props} onClick={handle}>
<span style={{ visibility: loading ? "hidden" : "visible" }}>{props.children}</span>
{loading && (
<span className="spinner-wrapper">

View File

@ -1,7 +1,7 @@
import "./BadgeList.css";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { TaggedNostrEvent } from "@snort/system";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";

View File

@ -1,5 +1,5 @@
import { useState, useMemo, ChangeEvent } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey, TaggedNostrEvent } from "@snort/system";
import Note from "Element/Note";

View File

@ -6,7 +6,7 @@ import DM from "Element/DM";
import useLogin from "Hooks/useLogin";
import WriteMessage from "Element/WriteMessage";
import { Chat, createEmptyChatObject, useChatSystem } from "chat";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { ChatParticipantProfile } from "./ChatParticipant";
export default function DmWindow({ id }: { id: string }) {

View File

@ -1,5 +1,5 @@
import "./FollowButton.css";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import useEventPublisher from "Hooks/useEventPublisher";

View File

@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import useEventPublisher from "Hooks/useEventPublisher";

View File

@ -1,7 +1,7 @@
import "./Following.css";
import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
export function FollowingMark({ pubkey }: { pubkey: string }) {
const { follows } = useLogin(s => ({ follows: s.follows }));

View File

@ -0,0 +1,22 @@
import { useState, useEffect, FC, ComponentProps } from 'react';
import { useIntl, FormattedMessage } from 'react-intl';
type ExtendedProps = ComponentProps<typeof FormattedMessage>;
const ExtendedFormattedMessage: FC<ExtendedProps> = (props) => {
const { id, defaultMessage, values} = props;
const { formatMessage } = useIntl();
const [processedMessage, setProcessedMessage] = useState<string | null>(null);
useEffect(() => {
const translatedMessage = formatMessage({ id, defaultMessage }, values);
if (typeof translatedMessage === 'string') {
setProcessedMessage(translatedMessage.replace('Snort', process.env.APP_NAME_CAPITALIZED || 'Snort'));
}
}, [id, defaultMessage, values, formatMessage]);
return <>{processedMessage}</>;
};
export default ExtendedFormattedMessage;

View File

@ -1,5 +1,5 @@
import { NostrEvent, NostrLink } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link } from "react-router-dom";
import { findTag } from "SnortUtils";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useInView } from "react-intersection-observer";
import messages from "./messages";

View File

@ -4,7 +4,7 @@ const Logo = () => {
const navigate = useNavigate();
return (
<h1 className="logo" onClick={() => navigate("/")}>
Snort
{process.env.APP_NAME}
</h1>
);
};

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate } from "react-router-dom";
import { logout } from "Login";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Magnet } from "SnortUtils";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag } from "SnortUtils";

View File

@ -7,7 +7,7 @@ import Note from "Element/Note";
import { getDisplayName } from "Element/ProfileImage";
import { eventLink, hexToBech32 } from "SnortUtils";
import useModeration from "Hooks/useModeration";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import Icon from "Icons/Icon";
import { useUserProfile } from "@snort/system-react";
import { useInView } from "react-intersection-observer";

View File

@ -1,6 +1,6 @@
import "./NoteToSelf.css";
import { Link, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { profileLink } from "SnortUtils";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import useLogin from "Hooks/useLogin";
import "./PinPrompt.css";
import { ReactNode, useState } from "react";
import {ReactNode, useRef, useState} from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { unwrap } from "@snort/shared";
import { EventPublisher, InvalidPinError, PinEncrypted, PinEncryptedPayload } from "@snort/system";
@ -23,6 +23,7 @@ export function PinPrompt({
const [pin, setPin] = useState("");
const [error, setError] = useState("");
const { formatMessage } = useIntl();
const submitButtonRef = useRef<HTMLButtonElement>(null);
async function submitPin() {
if (pin.length < 4) {
@ -55,29 +56,38 @@ export function PinPrompt({
return (
<Modal id="pin" onClose={() => onCancel()}>
<div className="flex-column g12">
<h2>
<FormattedMessage defaultMessage="Enter Pin" />
</h2>
{subTitle}
<input
type="number"
onChange={e => setPin(e.target.value)}
value={pin}
autoFocus={true}
maxLength={20}
minLength={4}
/>
{error && <b className="error">{error}</b>}
<div className="flex g8">
<button type="button" onClick={() => onCancel()}>
<FormattedMessage defaultMessage="Cancel" />
</button>
<AsyncButton type="button" onClick={() => submitPin()}>
<FormattedMessage defaultMessage="Submit" />
</AsyncButton>
<form
onSubmit={(e) => {
e.preventDefault();
if (submitButtonRef.current) {
submitButtonRef.current.click();
}
}}
>
<div className="flex-column g12">
<h2>
<FormattedMessage defaultMessage="Enter Pin" />
</h2>
{subTitle}
<input
type="number"
onChange={(e) => setPin(e.target.value)}
value={pin}
autoFocus={true}
maxLength={20}
minLength={4}
/>
{error && <b className="error">{error}</b>}
<div className="flex g8">
<button type="button" onClick={() => onCancel()}>
<FormattedMessage defaultMessage="Cancel" />
</button>
<AsyncButton ref={submitButtonRef} onClick={() => submitPin()} type="submit">
<FormattedMessage defaultMessage="Submit" />
</AsyncButton>
</div>
</div>
</div>
</form>
</Modal>
);
}

View File

@ -1,6 +1,6 @@
import useImgProxy from "Hooks/useImgProxy";
import React, { useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { getUrlHostname } from "SnortUtils";
interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {

View File

@ -1,5 +1,5 @@
import { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";

View File

@ -1,6 +1,6 @@
import "./Relay.css";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate } from "react-router-dom";
import { RelaySettings } from "@snort/system";
import { unixNowMs } from "@snort/shared";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { FileExtensionRegex } from "Const";
import Reveal from "Element/Reveal";

View File

@ -2,7 +2,7 @@ import "./RootTabs.css";
import { useState, ReactNode, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon";

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { HexKey, NostrPrefix } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import FollowListBase from "Element/FollowListBase";
import PageSpinner from "Element/PageSpinner";

View File

@ -1,5 +1,5 @@
import "./Timeline.css";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer";
import { TaggedNostrEvent, EventKind, u256 } from "@snort/system";

View File

@ -1,6 +1,6 @@
import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { TaggedNostrEvent, EventKind, u256, NostrEvent, NostrLink } from "@snort/system";
import { unixNow } from "@snort/shared";
import { SnortContext } from "@snort/system-react";

View File

@ -4,7 +4,7 @@ import { NostrEvent, NostrLink } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
const media = ev.tags.find(a => a[0] === "media");

View File

@ -86,6 +86,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
};
v.extraChats ??= [];
}
this.#loadIrisKeyIfExists();
}
getSessions() {
@ -217,6 +218,23 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return { ...s };
}
async #loadIrisKeyIfExists() {
try {
const irisKeyJSON = window.localStorage.getItem('iris.myKey');
if (irisKeyJSON) {
const irisKeyObj = JSON.parse(irisKeyJSON);
if (irisKeyObj.priv) {
const privateKey = await PinEncrypted.create(irisKeyObj.priv, '1234');
this.loginWithPrivateKey(privateKey);
window.localStorage.removeItem('iris.myKey');
}
}
} catch (e) {
console.error("Failed to load iris key", e);
}
}
#migrate() {
let didMigrate = false;

View File

@ -1,7 +1,7 @@
import "./Deck.css";
import { CSSProperties, createContext, useContext, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { NostrLink } from "@snort/system";
import { DeckNav } from "Element/Deck/Nav";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { HexKey } from "@snort/system";
import { ApiHost, KieranPubKey, SnortPubKey } from "Const";

View File

@ -1,6 +1,6 @@
import { db } from "Db";
import AsyncButton from "Element/AsyncButton";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useRouteError } from "react-router-dom";
const ErrorPage = () => {

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import Timeline from "Element/Timeline";
import useEventPublisher from "Hooks/useEventPublisher";

View File

@ -1,6 +1,6 @@
import { Link } from "react-router-dom";
import { KieranPubKey } from "Const";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { TLVEntryType, encodeTLVEntries, NostrPrefix } from "@snort/system";
import { bech32ToHex } from "SnortUtils";

View File

@ -209,7 +209,7 @@ function LogoHeader() {
return (
<Link to="/" className="logo">
<h1>Snort</h1>
<h1>{process.env.APP_NAME}</h1>
{currentSubscription && (
<small className="flex">
<Icon name="diamond" size={10} className="mr5" />

View File

@ -283,7 +283,7 @@ export default function LoginPage() {
<div>
<div className="login-container">
<h1 className="logo" onClick={() => navigate("/")}>
Snort
{process.env.APP_NAME}
</h1>
<h1 dir="auto">
<FormattedMessage defaultMessage="Login" description="Login header" />

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { ApiHost } from "Const";
import Nip5Service from "Element/Nip5Service";

View File

@ -1,6 +1,6 @@
import { NostrPrefix, tryParseNostrLink } from "@snort/system";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate, useParams } from "react-router-dom";
import Spinner from "Icons/Spinner";
@ -24,7 +24,7 @@ export default function NostrLinkHandler() {
}
} else {
try {
const pubkey = await getNip05PubKey(`${link}@snort.social`);
const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`);
if (pubkey) {
navigate(profileLink(pubkey));
}

View File

@ -1,6 +1,6 @@
import "./ProfilePage.css";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate, useParams } from "react-router-dom";
import {
encodeTLV,

View File

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Link, Outlet, RouteObject, useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { unixNow } from "@snort/shared";
import { NostrLink } from "@snort/system";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
import SettingsIndex from "Pages/settings/Root";
import Profile from "Pages/settings/Profile";

View File

@ -1,5 +1,5 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link } from "react-router-dom";
import ProfilePreview from "Element/ProfilePreview";

View File

@ -1,5 +1,5 @@
import "./Keys.css";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { encodeTLV, NostrPrefix, PinEncrypted } from "@snort/system";
import Copy from "Element/Copy";

View File

@ -3,7 +3,7 @@ import useLogin from "Hooks/useLogin";
import { setAppData } from "Login";
import { appendDedupe } from "SnortUtils";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
export function ModerationSettings() {
const login = useLogin();

View File

@ -1,6 +1,6 @@
import "./Profile.css";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate } from "react-router-dom";
import { mapEventToProfile } from "@snort/system";
import { useUserProfile } from "@snort/system-react";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import ProfilePreview from "Element/ProfilePreview";
import useRelayState from "Feed/RelayState";
import { useNavigate, useParams } from "react-router-dom";

View File

@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { unixNowMs } from "@snort/shared";
import { randomSample } from "SnortUtils";

View File

@ -1,6 +1,6 @@
import "./Root.css";
import { useEffect, useMemo } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import Icon from "Icons/Icon";
import { LoginStore, logout } from "Login";

View File

@ -1,6 +1,6 @@
import "./WalletSettings.css";
import LndLogo from "lnd-logo.png";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link, RouteObject, useNavigate } from "react-router-dom";
import BlueWallet from "Icons/BlueWallet";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link, useNavigate } from "react-router-dom";
import { ApiHost } from "Const";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
import ListHandles from "./ListHandles";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link, useNavigate } from "react-router-dom";
import PageSpinner from "Element/PageSpinner";

View File

@ -1,7 +1,7 @@
import "./index.css";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { RouteObject } from "react-router-dom";
import { formatShort } from "Number";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link } from "react-router-dom";
import { BaseUITask } from "Tasks";

View File

@ -1,4 +1,4 @@
import { FormattedMessage } from "react-intl";
import FormattedMessage from "Element/FormattedMessage";
import { Link } from "react-router-dom";
import { MetadataCache } from "@snort/system";
import { BaseUITask } from "Tasks";

View File

@ -9,6 +9,8 @@ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const IntlTsTransformer = require("@formatjs/ts-transformer");
const { DefinePlugin } = require('webpack');
const appConfig = require('config');
const isProduction = process.env.NODE_ENV == "production";
@ -47,8 +49,11 @@ const config = {
}),
new HtmlWebpackPlugin({
template: "public/index.html",
favicon: "public/favicon.ico",
favicon: appConfig.get('favicon'),
excludeChunks: ["pow", "bench"],
templateParameters: {
appTitle: appConfig.get('appTitle'),
},
}),
new HtmlWebpackPlugin({
filename: "bench.html",
@ -69,6 +74,11 @@ const config = {
swSrc: "./src/service-worker.ts",
})
: false,
new DefinePlugin({
"process.env.APP_NAME": JSON.stringify(appConfig.get('appName')),
"process.env.APP_NAME_CAPITALIZED": JSON.stringify(appConfig.get('appNameCapitalized')),
"process.env.NIP05_DOMAIN": JSON.stringify(appConfig.get('nip05Domain')),
}),
],
module: {
rules: [

View File

@ -2709,6 +2709,7 @@ __metadata:
"@webpack-cli/generators": ^3.0.4
"@webscopeio/react-textarea-autocomplete": ^4.9.2
babel-loader: ^9.1.3
config: ^3.3.9
copy-webpack-plugin: ^11.0.0
css-loader: ^6.7.3
css-minimizer-webpack-plugin: ^5.0.0
@ -5330,6 +5331,15 @@ __metadata:
languageName: node
linkType: hard
"config@npm:^3.3.9":
version: 3.3.9
resolution: "config@npm:3.3.9"
dependencies:
json5: ^2.2.3
checksum: 2c29e40be22274462769670a4b69fcbcad2d3049eb15030073e410d32c892ef29e0c879a3d68ef92ddd572c516e4f65a11bb6458f680a44ceb0f051bcd3d97ff
languageName: node
linkType: hard
"connect-history-api-fallback@npm:^2.0.0":
version: 2.0.0
resolution: "connect-history-api-fallback@npm:2.0.0"