Better API code structure
This commit is contained in:
parent
4f3488c427
commit
6fdeee1269
12 changed files with 265 additions and 227 deletions
|
@ -87,7 +87,7 @@ const { prefilledInstance } = Astro.props;
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getUrlDomain, normalizeURL } from "@scripts/util";
|
import { getUrlDomain, normalizeURL } from "@lib/url";
|
||||||
import { $popularInstances } from "@stores/popular-instances";
|
import { $popularInstances } from "@stores/popular-instances";
|
||||||
import { $savedInstances, save } from "@stores/saved-instances";
|
import { $savedInstances, save } from "@stores/saved-instances";
|
||||||
|
|
||||||
|
|
60
src/lib/instance.ts
Normal file
60
src/lib/instance.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { supportedProjects } from "./project";
|
||||||
|
|
||||||
|
interface Instance {
|
||||||
|
domain: string;
|
||||||
|
score: number;
|
||||||
|
active_users_monthly: number;
|
||||||
|
total_users: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstancesForProject = async (
|
||||||
|
project: keyof typeof supportedProjects,
|
||||||
|
): Promise<Instance[]> => {
|
||||||
|
let instances: Instance[];
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.fediverse.observer/", {
|
||||||
|
headers: {
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en;q=1.0",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
referrer: "https://api.fediverse.observer/",
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `{nodes(status:"UP",softwarename:"${project}"){domain score active_users_monthly total_users}}`,
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
instances = json.data.nodes;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not fetch instances for "${project}"`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances.filter(
|
||||||
|
(instance) =>
|
||||||
|
instance.score > 90 &&
|
||||||
|
// sanity check for some spammy-looking instances
|
||||||
|
instance.total_users >= instance.active_users_monthly,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPopularInstanceDomains = async (): Promise<string[]> => {
|
||||||
|
const instancesPerProject = await Promise.all(
|
||||||
|
Object.keys(supportedProjects).map((project) =>
|
||||||
|
getInstancesForProject(project),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const instances = instancesPerProject.flat();
|
||||||
|
instances.sort((a, b) => b.active_users_monthly - a.active_users_monthly);
|
||||||
|
|
||||||
|
return instances.slice(0, 200).map((instance) => instance.domain);
|
||||||
|
};
|
60
src/lib/nodeinfo.ts
Normal file
60
src/lib/nodeinfo.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { normalizeURL } from "@lib/url";
|
||||||
|
|
||||||
|
interface NodeInfoList {
|
||||||
|
links: {
|
||||||
|
rel: string;
|
||||||
|
href: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeInfo {
|
||||||
|
software: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSoftwareName = async (
|
||||||
|
domain: string,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const nodeInfoListUrl = new URL(
|
||||||
|
"/.well-known/nodeinfo",
|
||||||
|
normalizeURL(domain),
|
||||||
|
);
|
||||||
|
|
||||||
|
let nodeInfoList: NodeInfoList;
|
||||||
|
try {
|
||||||
|
const nodeInfoListResponse = await fetch(nodeInfoListUrl);
|
||||||
|
nodeInfoList = await nodeInfoListResponse.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Could not fetch '.well-known/nodeinfo':", error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of nodeInfoList.links) {
|
||||||
|
if (
|
||||||
|
/^http:\/\/nodeinfo\.diaspora\.software\/ns\/schema\/(1\.0|1\.1|2\.0|2\.1)/.test(
|
||||||
|
link.rel,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const nodeInfoResponse = await fetch(link.href);
|
||||||
|
const nodeInfo = (await nodeInfoResponse.json()) as NodeInfo;
|
||||||
|
|
||||||
|
return nodeInfo.software.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not found
|
||||||
|
console.warn("No NodeInfo found for domain:", domain);
|
||||||
|
return undefined;
|
||||||
|
};
|
64
src/lib/project.ts
Normal file
64
src/lib/project.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ProjectPublishConfig {
|
||||||
|
endpoint: string;
|
||||||
|
params: {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mastodonConfig: ProjectPublishConfig = {
|
||||||
|
endpoint: "share",
|
||||||
|
params: {
|
||||||
|
text: "text",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const misskeyConfig: ProjectPublishConfig = {
|
||||||
|
endpoint: "share",
|
||||||
|
params: {
|
||||||
|
text: "text",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of the supported fediverse projects.
|
||||||
|
*
|
||||||
|
* The keys of this mapping can be used as keys for the fediverse.observer API,
|
||||||
|
* icon names, etc.
|
||||||
|
*/
|
||||||
|
export const supportedProjects: Record<string, ProjectPublishConfig> = {
|
||||||
|
calckey: misskeyConfig,
|
||||||
|
fedibird: mastodonConfig,
|
||||||
|
firefish: misskeyConfig,
|
||||||
|
foundkey: misskeyConfig,
|
||||||
|
friendica: {
|
||||||
|
endpoint: "compose",
|
||||||
|
params: {
|
||||||
|
text: "body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
glitchcafe: mastodonConfig,
|
||||||
|
gnusocial: {
|
||||||
|
endpoint: "notice/new",
|
||||||
|
params: {
|
||||||
|
text: "status_textarea",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hometown: mastodonConfig,
|
||||||
|
hubzilla: {
|
||||||
|
endpoint: "rpost",
|
||||||
|
params: {
|
||||||
|
text: "body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mastodon: mastodonConfig,
|
||||||
|
meisskey: misskeyConfig,
|
||||||
|
misskey: misskeyConfig,
|
||||||
|
};
|
30
src/lib/response.ts
Normal file
30
src/lib/response.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const json = (
|
||||||
|
body: unknown,
|
||||||
|
status: number = 200,
|
||||||
|
headers: Record<string, string> = {},
|
||||||
|
) => {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const error = (message: string, status: number = 400) => {
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,130 +6,38 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getSoftwareName } from "@lib/nodeinfo";
|
||||||
|
import { ProjectPublishConfig, supportedProjects } from "@lib/project";
|
||||||
|
import { error, json } from "@lib/response";
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { FediverseProject } from "@scripts/constants";
|
|
||||||
import { normalizeURL } from "@scripts/util";
|
|
||||||
|
|
||||||
interface FediverseProjectData {
|
export type Detection = {
|
||||||
publishEndpoint: string;
|
domain: string;
|
||||||
params: {
|
project: keyof typeof supportedProjects;
|
||||||
text: string;
|
} & ProjectPublishConfig;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mastodonSettings = {
|
|
||||||
publishEndpoint: "share",
|
|
||||||
params: {
|
|
||||||
text: "text",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const misskeySettings = {
|
|
||||||
publishEndpoint: "share",
|
|
||||||
params: {
|
|
||||||
text: "text",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const PROJECTS: Map<FediverseProject, FediverseProjectData> = new Map()
|
|
||||||
.set(FediverseProject.Mastodon, mastodonSettings)
|
|
||||||
.set(FediverseProject.Fedibird, mastodonSettings)
|
|
||||||
.set(FediverseProject.GlitchCafe, mastodonSettings)
|
|
||||||
.set(FediverseProject.Hometown, mastodonSettings)
|
|
||||||
.set(FediverseProject.Misskey, misskeySettings)
|
|
||||||
.set(FediverseProject.Calckey, misskeySettings)
|
|
||||||
.set(FediverseProject.Firefish, misskeySettings)
|
|
||||||
.set(FediverseProject.FoundKey, misskeySettings)
|
|
||||||
.set(FediverseProject.Meisskey, misskeySettings)
|
|
||||||
.set(FediverseProject.GNUSocial, {
|
|
||||||
publishEndpoint: "/notice/new",
|
|
||||||
params: {
|
|
||||||
text: "status_textarea",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.set(FediverseProject.Friendica, {
|
|
||||||
publishEndpoint: "compose",
|
|
||||||
params: {
|
|
||||||
text: "body",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.set(FediverseProject.Hubzilla, {
|
|
||||||
publishEndpoint: "rpost",
|
|
||||||
params: {
|
|
||||||
text: "body",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NodeInfoList {
|
|
||||||
links: {
|
|
||||||
rel: string;
|
|
||||||
href: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeInfo {
|
|
||||||
[key: string]: unknown;
|
|
||||||
software: {
|
|
||||||
[key: string]: unknown;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type NonEmptyArray<T> = [T, ...T[]];
|
|
||||||
function isNotEmpty<T>(array: T[]): array is NonEmptyArray<T> {
|
|
||||||
return array.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkNodeInfo = async (domain: string): Promise<FediverseProject> => {
|
|
||||||
const nodeInfoListUrl = new URL(
|
|
||||||
"/.well-known/nodeinfo",
|
|
||||||
normalizeURL(domain),
|
|
||||||
);
|
|
||||||
const nodeInfoListResponse = await fetch(nodeInfoListUrl);
|
|
||||||
const nodeInfoList = (await nodeInfoListResponse.json()) as NodeInfoList;
|
|
||||||
|
|
||||||
if (isNotEmpty(nodeInfoList.links)) {
|
|
||||||
const nodeInfoUrl = nodeInfoList.links[0].href;
|
|
||||||
const nodeInfoResponse = await fetch(nodeInfoUrl);
|
|
||||||
const nodeInfo = (await nodeInfoResponse.json()) as NodeInfo;
|
|
||||||
|
|
||||||
return nodeInfo.software.name as FediverseProject;
|
|
||||||
} else {
|
|
||||||
throw new Error(`No nodeinfo found for ${domain}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const get: APIRoute = async ({ params }) => {
|
export const get: APIRoute = async ({ params }) => {
|
||||||
const domain = params.domain as string;
|
const domain = params.domain as string;
|
||||||
|
|
||||||
try {
|
const softwareName = await getSoftwareName(domain);
|
||||||
const projectId = await checkNodeInfo(domain);
|
if (softwareName === undefined) {
|
||||||
|
return error("Could not detect software; NodeInfo not present.");
|
||||||
if (!PROJECTS.has(projectId)) {
|
|
||||||
throw new Error(`Unexpected project ID: ${projectId}`);
|
|
||||||
}
|
}
|
||||||
const projectData = PROJECTS.get(projectId) as FediverseProjectData;
|
|
||||||
return new Response(
|
if (!(softwareName in supportedProjects)) {
|
||||||
JSON.stringify({
|
return error(`"${softwareName}" is not supported yet.`);
|
||||||
host: domain,
|
}
|
||||||
project: projectId,
|
|
||||||
publishEndpoint: projectData.publishEndpoint,
|
const publishConfig = supportedProjects[softwareName] as ProjectPublishConfig;
|
||||||
params: projectData.params,
|
return json(
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
status: 200,
|
domain,
|
||||||
headers: {
|
project: softwareName,
|
||||||
"Cache-Control": "public, s-maxage=86400, max-age=604800",
|
...publishConfig,
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
},
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Cache-Control": "public, s-maxage=86400, max-age=604800",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch {
|
|
||||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
|
||||||
status: 404,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,70 +7,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { FediverseProject } from "@scripts/constants";
|
import { getPopularInstanceDomains } from "@lib/instance";
|
||||||
|
import { json } from "@lib/response";
|
||||||
interface ProjectInstance {
|
|
||||||
domain: string;
|
|
||||||
score: number;
|
|
||||||
active_users_monthly: number;
|
|
||||||
total_users: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROJECTS = Object.values(FediverseProject);
|
|
||||||
|
|
||||||
export const fetchInstances = async (
|
|
||||||
projectId: string,
|
|
||||||
): Promise<ProjectInstance[]> => {
|
|
||||||
const response = await fetch("https://api.fediverse.observer/", {
|
|
||||||
headers: {
|
|
||||||
Accept: "*/*",
|
|
||||||
"Accept-Language": "en;q=1.0",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
referrer: "https://api.fediverse.observer/",
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: `{nodes(status:"UP",softwarename:"${projectId}"){domain score active_users_monthly total_users}}`,
|
|
||||||
}),
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
const json = await response.json();
|
|
||||||
const instances: ProjectInstance[] = json.data.nodes;
|
|
||||||
return instances.filter(
|
|
||||||
(instance) =>
|
|
||||||
instance.score > 90 &&
|
|
||||||
instance.total_users >= instance.active_users_monthly,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const get: APIRoute = async () => {
|
export const get: APIRoute = async () => {
|
||||||
try {
|
const popularInstanceDomains = await getPopularInstanceDomains();
|
||||||
const response = await Promise.all(
|
|
||||||
PROJECTS.map((projectId) => fetchInstances(projectId)),
|
|
||||||
);
|
|
||||||
const instances = response.flat();
|
|
||||||
instances.sort((a, b) => {
|
|
||||||
return b.active_users_monthly - a.active_users_monthly;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(
|
return json(popularInstanceDomains, 200, {
|
||||||
JSON.stringify(
|
"Cache-Control":
|
||||||
instances
|
popularInstanceDomains.length > 0
|
||||||
.slice(0, 200)
|
? "public, s-maxage=86400, max-age=604800"
|
||||||
.map((instance: ProjectInstance) => instance.domain),
|
: "public, s-maxage=60, max-age=3600",
|
||||||
),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, s-maxage=86400, max-age=604800",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Could not fetch instances:", error);
|
|
||||||
return new Response(JSON.stringify([]), {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getUrlDomain } from "@scripts/util";
|
import { getUrlDomain } from "@lib/url";
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
|
import type { Detection } from "./detect/[domain]";
|
||||||
|
import { error } from "@lib/response";
|
||||||
|
|
||||||
export const post: APIRoute = async ({ redirect, request, url }) => {
|
export const post: APIRoute = async ({ redirect, request, url }) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
@ -16,18 +18,14 @@ export const post: APIRoute = async ({ redirect, request, url }) => {
|
||||||
const instanceHost =
|
const instanceHost =
|
||||||
getUrlDomain(formData.get("instance") as string) || "mastodon.social";
|
getUrlDomain(formData.get("instance") as string) || "mastodon.social";
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(new URL(`/api/detect/${instanceHost}`, url));
|
const response = await fetch(new URL(`/api/detect/${instanceHost}`, url));
|
||||||
const { host, publishEndpoint, params } = await response.json();
|
const json = await response.json();
|
||||||
const publishUrl = new URL(publishEndpoint, `https://${host}/`);
|
if (json.error) {
|
||||||
|
return error(json.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { domain, endpoint, params } = json as Detection;
|
||||||
|
const publishUrl = new URL(endpoint, `https://${domain}/`);
|
||||||
publishUrl.search = new URLSearchParams([[params.text, text]]).toString();
|
publishUrl.search = new URLSearchParams([[params.text, text]]).toString();
|
||||||
return redirect(publishUrl.toString(), 303);
|
return redirect(publishUrl.toString(), 303);
|
||||||
} catch {
|
|
||||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
|
||||||
status: 400,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*!
|
|
||||||
* This file is part of Share₂Fedi
|
|
||||||
* https://github.com/kytta/share2fedi
|
|
||||||
*
|
|
||||||
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration of the supported fediverse projects.
|
|
||||||
*
|
|
||||||
* The values of this enum are used as the keys for the fediverse.observer API,
|
|
||||||
* as the icon names, etc.
|
|
||||||
*/
|
|
||||||
export enum FediverseProject {
|
|
||||||
Calckey = "calckey",
|
|
||||||
GlitchCafe = "glitchcafe",
|
|
||||||
Fedibird = "fedibird",
|
|
||||||
Firefish = "firefish",
|
|
||||||
FoundKey = "foundkey",
|
|
||||||
Friendica = "friendica",
|
|
||||||
GNUSocial = "gnusocial",
|
|
||||||
Hometown = "hometown",
|
|
||||||
Hubzilla = "hubzilla",
|
|
||||||
Mastodon = "mastodon",
|
|
||||||
Meisskey = "meisskey",
|
|
||||||
Misskey = "misskey",
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { persistentAtom } from "@nanostores/persistent";
|
import { persistentAtom } from "@nanostores/persistent";
|
||||||
import { getUrlDomain } from "@scripts/util";
|
import { getUrlDomain } from "@lib/url";
|
||||||
import { action, onMount } from "nanostores";
|
import { action, onMount } from "nanostores";
|
||||||
|
|
||||||
const OLD_LOCAL_STORAGE_KEY = "recentInstances";
|
const OLD_LOCAL_STORAGE_KEY = "recentInstances";
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"@i18n/*": ["src/i18n/*"],
|
"@i18n/*": ["src/i18n/*"],
|
||||||
"@layouts/*": ["src/layouts/*"],
|
"@layouts/*": ["src/layouts/*"],
|
||||||
"@pages/*": ["src/pages/*"],
|
"@pages/*": ["src/pages/*"],
|
||||||
"@scripts/*": ["src/scripts/*"],
|
"@lib/*": ["src/lib/*"],
|
||||||
"@stores/*": ["src/stores/*"],
|
"@stores/*": ["src/stores/*"],
|
||||||
"@styles/*": ["src/styles/*"]
|
"@styles/*": ["src/styles/*"]
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue