refactor: ♻️ Make auth store require less null checks

This commit is contained in:
Jesse Wierzbinski 2026-01-09 22:35:46 +01:00
parent 68e23a818a
commit b23ed66401
No known key found for this signature in database
32 changed files with 111 additions and 124 deletions

View file

@ -56,13 +56,13 @@ useSeoMeta({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} · Versia` : "Versia";
},
title: computed(() => authStore.instance?.title ?? ""),
ogImage: computed(() => authStore.instance?.banner?.url),
twitterTitle: computed(() => authStore.instance?.title ?? ""),
title: computed(() => authStore.instanceOptional?.title ?? ""),
ogImage: computed(() => authStore.instanceOptional?.banner?.url),
twitterTitle: computed(() => authStore.instanceOptional?.title ?? ""),
twitterDescription: computed(() =>
convert(description.value?.content ?? ""),
),
twitterImage: computed(() => authStore.instance?.banner?.url),
twitterImage: computed(() => authStore.instanceOptional?.banner?.url),
description: computed(() => convert(description.value?.content ?? "")),
ogDescription: computed(() => convert(description.value?.content ?? "")),
ogSiteName: "Versia",

View file

@ -141,7 +141,7 @@ const composerKey = props.relation
const store = useComposerStore(composerKey)();
const authStore = useAuthStore();
const charactersLeft = computed(() => {
const max = authStore.instance?.configuration.statuses.max_characters ?? 0;
const max = authStore.instance.configuration.statuses.max_characters;
return max - store.rawContent.length;
});

View file

@ -1,6 +1,6 @@
<template>
<Command
class="rounded border shadow-md min-w-[200px] h-fit not-prose"
class="rounded border shadow-md min-w-50 h-fit not-prose"
:selected-value="emojis[selectedIndex]?.id"
>
<CommandList>
@ -14,7 +14,7 @@
class="scroll-m-10"
>
<img
class="h-[1lh] align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out"
class="h-lh align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out"
:src="emoji.url"
:title="emoji.shortcode"
>

View file

@ -126,11 +126,9 @@ export const emojiSuggestion = {
return [];
}
const emojis = authStore.emojis;
return go(
query,
emojis
authStore.emojis
.filter((emoji) => emoji.shortcode.includes(query))
.map((emoji) => ({
key: emoji.shortcode,

View file

@ -80,7 +80,7 @@ const isValid = ref(false);
:open="isOpen"
@update:open="isOpen = false"
>
<AlertDialogContent class="sm:max-w-[425px] flex flex-col">
<AlertDialogContent class="sm:max-w-106.25 flex flex-col">
<AlertDialogHeader>
<AlertDialogTitle>{{ modalOptions.title }}</AlertDialogTitle>
<AlertDialogDescription v-if="modalOptions.message">

View file

@ -44,14 +44,13 @@
<script lang="ts" setup>
import type { CustomEmoji, Status } from "@versia/client/schemas";
import { Ellipsis, Heart, Quote, Repeat, Reply, Smile } from "lucide-vue-next";
import { Heart, Quote, Repeat, Reply, Smile } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import * as m from "~~/paraglide/messages.js";
import { getLocale } from "~~/paraglide/runtime";
import { confirmModalService } from "../modals/composable";
import ActionButton from "./action-button.vue";
import Menu from "./menu.vue";
import type { UnicodeEmoji } from "./reactions/picker/emoji";
import Picker from "./reactions/picker/index.vue";

View file

@ -136,8 +136,8 @@ const _delete = async () => {
{{ m.tense_quick_cod_favor() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !authorIsMe" />
<DropdownMenuGroup v-if="authStore.isSignedIn && !authorIsMe">
<DropdownMenuSeparator v-if="authorIsMe" />
<DropdownMenuGroup v-if="authorIsMe">
<DropdownMenuItem as="button" :disabled="true">
<Flag />
{{ m.great_few_jaguar_rise() }}

View file

@ -32,9 +32,9 @@ const copy = (data: string) => {
const authStore = useAuthStore();
const data: [string, string | VNode][] = [
["User ID", authStore.account?.id ?? ""],
["Instance domain", authStore.instance?.domain ?? ""],
["Instance version", authStore.instance?.versia_version ?? ""],
["User ID", authStore.account.id],
["Instance domain", authStore.instance.domain],
["Instance version", authStore.instance.versia_version ?? ""],
["Client ID", authStore.application?.client_id ?? ""],
[
"Client secret",
@ -55,7 +55,7 @@ const data: [string, string | VNode][] = [
class="font-sans"
size="sm"
// @ts-expect-error missing onClick types
onClick={() => copy(authStore.token?.access_token ?? "")}
onClick={() => copy(authStore.token.access_token)}
>
Click to copy
</Button>,

View file

@ -76,12 +76,12 @@ useListen("preferences:open", () => {
</script>
<template>
<Dialog v-model:open="open" v-if="authStore.isSignedIn">
<Dialog v-model:open="open">
<DialogContent
class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden"
>
<Tabs
class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full"
class="md:grid-cols-[auto_minmax(0,1fr)] grid! gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full"
orientation="vertical"
:default-value="pages[0]"
>
@ -92,8 +92,8 @@ useListen("preferences:open", () => {
class="grid gap-3 items-center grid-cols-[auto_minmax(0,1fr)]"
>
<Avatar
:name="authStore.account!.display_name || authStore.account!.username"
:src="authStore.account!.avatar"
:name="authStore.account.display_name || authStore.account.username"
:src="authStore.account.avatar"
/>
<DialogTitle>Preferences</DialogTitle>
</div>
@ -101,7 +101,7 @@ useListen("preferences:open", () => {
Make changes to your preferences here.
</DialogDescription>
<TabsList
class="md:grid md:grid-cols-1 w-full h-fit *:justify-start !justify-start"
class="md:grid md:grid-cols-1 w-full h-fit *:justify-start justify-start!"
>
<TabsTrigger
v-for="page in pages"

View file

@ -37,10 +37,6 @@ const canEdit =
authStore.permissions.includes(RolePermission.ManageEmojis);
const deleteAll = async () => {
if (!authStore.isSignedIn) {
return;
}
const { confirmed } = await confirmModalService.confirm({
title: m.tense_quick_cod_favor(),
message: m.next_hour_jurgen_sprout({

View file

@ -50,10 +50,6 @@ const { emoji } = defineProps<{
const authStore = useAuthStore();
const editName = async () => {
if (!authStore.isSignedIn) {
return;
}
const result = await confirmModalService.confirm({
title: m.slimy_awful_florian_sail(),
defaultValue: emoji.shortcode,
@ -83,10 +79,6 @@ const editName = async () => {
};
const _delete = async () => {
if (!authStore.isSignedIn) {
return;
}
const { confirmed } = await confirmModalService.confirm({
title: m.tense_quick_cod_favor(),
message: m.honest_factual_carp_aspire(),

View file

@ -98,7 +98,7 @@ const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
src={row.getValue("url")}
alt={`:${row.getValue("shortcode")}:`}
title={row.getValue("shortcode")}
class="h-[1lh] align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out"
class="h-lh align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out"
/>
),
},
@ -108,7 +108,7 @@ const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
return (
<Button
variant="link"
class="!p-0 !h-auto"
class="p-0! h-auto!"
// @ts-expect-error types don't include onClick
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
@ -135,7 +135,7 @@ const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
return (
<Button
variant="link"
class="!p-0 !h-auto"
class="p-0! h-auto!"
// @ts-expect-error types don't include onClick
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
@ -164,7 +164,7 @@ const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
return (
<Button
variant="link"
class="!p-0 !h-auto"
class="p-0! h-auto!"
// @ts-expect-error types don't include onClick
onClick={() => {
const filter = column.getFilterValue();

View file

@ -203,25 +203,21 @@ const formSchema = toTypedSchema(
.refine(
(v) =>
v.size <=
(authStore.instance?.configuration.emojis
.emoji_size_limit ?? Number.POSITIVE_INFINITY),
authStore.instance.configuration.emojis.emoji_size_limit,
m.orange_weird_parakeet_hug({
count:
authStore.instance?.configuration.emojis
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
count: authStore.instance.configuration.emojis
.emoji_size_limit,
}),
),
shortcode: z
.string()
.min(1)
.max(
authStore.instance?.configuration.emojis
.max_shortcode_characters ?? Number.POSITIVE_INFINITY,
authStore.instance.configuration.emojis
.max_shortcode_characters,
m.solid_inclusive_owl_hug({
count:
authStore.instance?.configuration.emojis
.max_shortcode_characters ??
Number.POSITIVE_INFINITY,
count: authStore.instance.configuration.emojis
.max_shortcode_characters,
}),
)
.regex(emojiValidator),
@ -238,13 +234,11 @@ const formSchema = toTypedSchema(
alt: z
.string()
.max(
authStore.instance?.configuration.emojis
.max_description_characters ?? Number.POSITIVE_INFINITY,
authStore.instance.configuration.emojis
.max_description_characters,
m.key_ago_hound_emerge({
count:
authStore.instance?.configuration.emojis
.max_description_characters ??
Number.POSITIVE_INFINITY,
count: authStore.instance.configuration.emojis
.max_description_characters,
}),
)
.optional(),
@ -255,10 +249,6 @@ const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
});
const submit = handleSubmit(async (values) => {
if (!authStore.isSignedIn) {
return;
}
const id = toast.loading(m.factual_gray_mouse_believe());
try {

View file

@ -38,7 +38,7 @@
<FormField v-slot="{ setValue }" name="avatar">
<TextInput :title="m.safe_icy_bulldog_quell()">
<ImageUploader
v-model:image="authStore.account!.avatar"
v-model:image="authStore.account.avatar"
@submit-file="(file) => setValue(file)"
@submit-url="(url) => setValue(url)"
/>
@ -143,10 +143,6 @@ const dirty = computed(() => form.meta.value.dirty);
const submitting = ref(false);
const authStore = useAuthStore();
if (!(authStore.instance && authStore.account)) {
throw new Error("Not signed in.");
}
const schema = formSchema(authStore.instance);
const form = useForm({
@ -167,7 +163,7 @@ const form = useForm({
});
const save = form.handleSubmit(async (values) => {
if (submitting.value || !authStore.account) {
if (submitting.value) {
return;
}
@ -198,7 +194,7 @@ const save = form.handleSubmit(async (values) => {
: values.discoverable,
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
fields_attributes: values.fields.every((field) =>
authStore.account?.source?.fields?.some(
authStore.account.source?.fields?.some(
(f) => f.name === field.name && f.value === field.value,
),
)

View file

@ -45,8 +45,8 @@
{{ m.active_trite_lark_inspire() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
<DropdownMenuSeparator v-if="!isMe" />
<DropdownMenuGroup v-if="!isMe">
<DropdownMenuItem as="button" @click="muteUser(account.id)">
<VolumeX />
{{ m.spare_wild_mole_intend() }}
@ -63,8 +63,8 @@
{{ m.slow_chunky_chipmunk_hush() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
<DropdownMenuSeparator v-if="!isMe" />
<DropdownMenuGroup v-if="!isMe">
<DropdownMenuItem as="button" :disabled="true">
<Flag />
{{ m.great_few_jaguar_rise() }}
@ -104,7 +104,7 @@ const { account } = defineProps<{
}>();
const authStore = useAuthStore();
const isMe = authStore.account?.id === account.id;
const isMe = authStore.accountOptional?.id === account.id;
const { copy } = useClipboard();
const copyText = (text: string) => {

View file

@ -42,7 +42,7 @@ const roles = account.roles.filter((r) => r.visible);
// Get user handle in username@instance format
const handle = account.acct.includes("@")
? account.acct
: `${account.acct}@${authStore.instance?.domain ?? window.location.host}`;
: `${account.acct}@${authStore.instance.domain}`;
const isDeveloper = config.DEVELOPER_HANDLES.includes(handle);
</script>

View file

@ -2,7 +2,7 @@
<Button
variant="secondary"
:disabled="isLoading || relationship?.requested"
v-if="!isMe && authStore.isSignedIn"
v-if="!isMe"
@click="relationship?.following ? unfollow() : follow()"
>
<Loader v-if="isLoading" class="animate-spin" />
@ -31,7 +31,7 @@ const { account } = defineProps<{
const { relationship, isLoading } = useRelationship(account.id);
const authStore = useAuthStore();
const isMe = authStore.account?.id === account.id;
const isMe = authStore.accountOptional?.id === account.id;
const follow = async () => {
if (preferences.confirm_actions.value.includes("follow")) {

View file

@ -23,7 +23,7 @@
/>
<Button
data-switch
v-if="authStore.identity?.id !== identity.id"
v-if="authStore.identityOptional?.id !== identity.id"
@click="authStore.setActiveIdentity(identity.id)"
variant="outline"
>

View file

@ -26,10 +26,7 @@ const authStore = useAuthStore();
<SidebarMenu class="gap-3">
<SidebarMenuItem>
<AccountManager>
<SidebarMenuButton
v-if="authStore.account && authStore.instance"
size="lg"
>
<SidebarMenuButton v-if="authStore.isSignedIn" size="lg">
<TinyCard
:account="authStore.account"
:domain="authStore.instance.domain"

View file

@ -11,13 +11,10 @@ const authStore = useAuthStore();
<template>
<SidebarHeader>
<SidebarMenu>
<SidebarMenu v-if="authStore.isSignedIn">
<SidebarMenuItem>
<NuxtLink href="/">
<InstanceSmallCard
v-if="authStore.instance"
:instance="authStore.instance"
/>
<InstanceSmallCard :instance="authStore.instance" />
</NuxtLink>
</SidebarMenuItem>
</SidebarMenu>

View file

@ -29,7 +29,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class=" cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>

View file

@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>

View file

@ -25,7 +25,7 @@ const forwardedProps = useForwardProps(delegatedProps);
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8',
props.class,
)"
>

View file

@ -12,7 +12,7 @@ const props = defineProps<{
data-slot="table-cell"
:class="
cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-0.5',
props.class,
)
"

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<th
data-slot="table-head"
:class="cn('text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', props.class)"
:class="cn('text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-0.5', props.class)"
>
<slot />
</th>

View file

@ -3,7 +3,7 @@ import * as m from "~~/paraglide/messages.js";
export const useCacheRefresh = () => {
const authStore = useAuthStore();
const { identity } = storeToRefs(authStore);
const { identityOptional } = storeToRefs(authStore);
authStore.client.getInstance().then((res) => {
authStore.updateActiveIdentity({
@ -13,7 +13,7 @@ export const useCacheRefresh = () => {
// Refresh custom emojis and instance data and me on every reload
watch(
identity,
identityOptional,
async (oldIdentity, newIdentity) => {
if (newIdentity && newIdentity.id !== oldIdentity?.id) {
console.info("Refreshing emoji, instance and account cache");

View file

@ -1,4 +1,4 @@
import type { Client, Output } from "@versia/client";
import type { Output } from "@versia/client";
import type { Notification, Status } from "@versia/client/schemas";
import { useIntervalFn } from "@vueuse/core";
import type { z } from "zod";
@ -17,7 +17,7 @@ export function useTimeline<
const hasReachedEnd = ref(false);
const error = ref<Error | null>(null);
const authStore = useAuthStore();
const { identity } = storeToRefs(authStore);
const { identityOptional } = storeToRefs(authStore);
const nextMaxId = ref<string | undefined>(undefined);
const prevMinId = ref<string | undefined>(undefined);
@ -101,7 +101,7 @@ export function useTimeline<
pause();
});
watch(identity, (newIdentity, oldIdentity) => {
watch(identityOptional, (newIdentity, oldIdentity) => {
if (newIdentity?.id !== oldIdentity?.id) {
// Reload timeline when identity changes
items.value = [];

View file

@ -31,7 +31,6 @@ const route = useRoute();
watch(
() => route.path,
async () => {
console.log(route.meta.requiresAuth && !authStore.isSignedIn);
if (route.meta.requiresAuth && !authStore.isSignedIn) {
window.location.href = "/";
}

View file

@ -38,34 +38,55 @@ export const useAuthStore = defineStore("auth", {
applications: {},
}),
getters: {
identity(state): Identity | null {
return state.activeIdentityId
? state.identities.find(
(id) => id.id === state.activeIdentityId,
) || null
: null;
identity(state): Identity {
if (state.activeIdentityId === null) {
throw new Error("This functionality requires authentication.");
}
const identity = state.identities.find(
(id) => id.id === state.activeIdentityId,
);
if (!identity) {
throw new Error("This functionality requires authentication.");
}
return identity;
},
emojis(): z.infer<typeof CustomEmoji>[] {
return this.identity?.emojis || [];
},
instance(): z.infer<typeof Instance> | null {
return this.identity?.instance || null;
},
account(): z.infer<typeof Account> | null {
return this.identity?.account || null;
},
application(): z.infer<typeof CredentialApplication> | null {
if (!this.identity) {
identityOptional(state): Identity | null {
if (state.activeIdentityId === null) {
return null;
}
return (
state.identities.find(
(id) => id.id === state.activeIdentityId,
) || null
);
},
emojis(): z.infer<typeof CustomEmoji>[] {
return this.identity.emojis;
},
instance(): z.infer<typeof Instance> {
return this.identity.instance;
},
instanceOptional(): z.infer<typeof Instance> | null {
return this.identityOptional?.instance ?? null;
},
account(): z.infer<typeof Account> {
return this.identity.account;
},
accountOptional(): z.infer<typeof Account> | null {
return this.identityOptional?.account ?? null;
},
application(): z.infer<typeof CredentialApplication> | null {
return this.applications[this.identity.instance.domain] || null;
},
token(): z.infer<typeof Token> | null {
return this.identity?.token || null;
token(): z.infer<typeof Token> {
return this.identity.token;
},
permissions(): RolePermission[] {
const roles = this.account?.roles ?? [];
const roles = this.account.roles;
return roles
.flatMap((r) => r.permissions)
@ -76,11 +97,11 @@ export const useAuthStore = defineStore("auth", {
},
client(): Client {
const apiHost = window.location.origin;
const domain = this.identity?.instance.domain;
const domain = this.identityOptional?.instance.domain;
return new Client(
domain ? new URL(`https://${domain}`) : new URL(apiHost),
this.identity?.token.access_token ?? undefined,
this.identityOptional?.token.access_token ?? undefined,
{
globalCatch: (error) => {
toast.error(

View file

@ -41,7 +41,7 @@ export const calculateMentionsFromReply = (
// Deduplicate mentions
.filter((men, i, a) => a.indexOf(men) === i)
// Remove self
.filter((men) => men.id !== authStore.identity?.account.id);
.filter((men) => men.id !== authStore.identity.account.id);
if (peopleToMention.length === 0) {
return "";
@ -72,8 +72,8 @@ export const useComposerStore = (key: ComposerStateKey) =>
isOverCharacterLimit(): boolean {
const authStore = useAuthStore();
const characterLimit =
authStore.identity?.instance.configuration.statuses
.max_characters ?? 0;
authStore.identity.instance.configuration.statuses
.max_characters;
return this.characterCount > characterLimit;
},

View file

@ -3,7 +3,7 @@ export const wrapUrl = (path: string) => {
return new URL(
path,
authStore.instance
authStore.instanceOptional
? `https://${authStore.instance.domain}`
: window.location.origin,
).toString();

View file

@ -90,7 +90,9 @@
"noUselessElse": "error",
"noCommonJs": "error"
},
"nursery": {}
"nursery": {
"noUnnecessaryConditions": "error"
}
}
},
"formatter": {