feat: fix panel persist over server rendering, etc

This commit is contained in:
Khairul Hidayat 2024-02-22 23:05:02 +07:00
parent 2c9faa332a
commit efe51a9b5e
27 changed files with 962 additions and 150 deletions

View File

@ -50,6 +50,7 @@ const CodeEditor = (props: Props) => {
parser,
plugins,
cursorOffset: cursor,
printWidth: 64,
}
);

View File

@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";

View File

@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";

View File

@ -1,24 +1,82 @@
"use client";
import { GripVertical } from "lucide-react";
import { createContext, forwardRef, useContext } from "react";
import * as ResizablePrimitive from "react-resizable-panels";
import cookieJs from "cookiejs";
import { cn } from "~/lib/utils";
import { usePageContext } from "~/renderer/context";
import { useDebounce } from "~/hooks/useDebounce";
const ResizableContext = createContext<{ initialSize: number[] }>(null!);
const ResizablePanelGroup = ({
className,
autoSaveId,
direction,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
);
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => {
const { cookies } = usePageContext();
const [debouncePersistLayout] = useDebounce((sizes: number[]) => {
if (autoSaveId && typeof window !== "undefined") {
cookieJs.set(panelKey, JSON.stringify(sizes));
}
}, 500);
const ResizablePanel = ResizablePrimitive.Panel;
const panelKey = ["panel", direction, autoSaveId].join(":");
let initialSize: number[] = [];
if (autoSaveId && cookies && cookies[panelKey]) {
initialSize = JSON.parse(cookies[panelKey]) || [];
}
const onLayout = (sizes: number[]) => {
if (props.onLayout) {
props.onLayout(sizes);
}
debouncePersistLayout(sizes);
};
return (
<ResizableContext.Provider value={{ initialSize }}>
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
direction={direction}
onLayout={onLayout}
/>
</ResizableContext.Provider>
);
};
type ResizablePanelProps = React.ComponentProps<
typeof ResizablePrimitive.Panel
> & {
panelId: number;
};
const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
const { panelId, defaultSize, ...restProps } = props;
const ctx = useContext(ResizableContext);
let initialSize = defaultSize;
if (panelId != null) {
const size = ctx?.initialSize[panelId];
if (size != null) {
initialSize = size;
}
}
return (
<ResizablePrimitive.Panel
ref={ref}
defaultSize={initialSize}
{...restProps}
/>
);
});
const ResizableHandle = ({
withHandle,

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -21,6 +21,7 @@
"license": "ISC",
"devDependencies": {
"@swc/cli": "^0.3.9",
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"@types/nprogress": "^0.2.3",
@ -29,7 +30,6 @@
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.0.1",
"drizzle-kit": "^0.20.14",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
@ -58,13 +58,17 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"console-feed": "^3.5.0",
"cookie-parser": "^1.4.6",
"cookiejs": "^2.1.3",
"copy-to-clipboard": "^3.3.3",
"cssnano": "^6.0.3",
"drizzle-orm": "^0.29.3",
"drizzle-zod": "^0.5.1",
"express": "^4.18.2",
"lucide-react": "^0.331.0",
"mime": "^4.0.1",
"nprogress": "^0.2.0",
"postcss": "^8",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -8,7 +8,6 @@ import { usePortrait } from "~/hooks/usePortrait";
import Editor from "./components/editor";
import ProjectContext from "./context/project";
import { cn } from "~/lib/utils";
import { withClientOnly } from "~/renderer/client-only";
import { useParams, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts";
@ -29,6 +28,7 @@ const ViewProjectPage = () => {
className={cn("w-full !h-dvh bg-slate-600", !isCompact ? "md:p-4" : "")}
>
<ResizablePanel
panelId={0}
defaultSize={isPortrait ? 50 : 60}
collapsible
collapsedSize={0}
@ -45,6 +45,7 @@ const ViewProjectPage = () => {
}
/>
<ResizablePanel
panelId={1}
defaultSize={isPortrait ? 50 : 40}
collapsible
collapsedSize={0}
@ -57,4 +58,4 @@ const ViewProjectPage = () => {
);
};
export default withClientOnly(ViewProjectPage);
export default ViewProjectPage;

View File

@ -0,0 +1,12 @@
import { PageContext } from "vike/types";
import trpcServer from "~/server/api/trpc/trpc";
export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx);
const pinnedFiles = await trpc.file.getAll({ isPinned: true });
const files = await trpc.file.getAll();
return { files, pinnedFiles };
};
export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -9,7 +9,6 @@ import FileViewer from "./file-viewer";
import trpc from "~/lib/trpc";
import EditorContext from "../context/editor";
import type { FileSchema } from "~/server/db/schema/file";
import { usePortrait } from "~/hooks/usePortrait";
import Panel from "~/components/ui/panel";
import { previewStore } from "../stores/web-preview";
import { useProjectContext } from "../context/project";
@ -19,27 +18,26 @@ import useCommandKey from "~/hooks/useCommandKey";
import { Button } from "~/components/ui/button";
import { FaCompress, FaCompressArrowsAlt } from "react-icons/fa";
import ConsoleLogger from "./console-logger";
import { useData } from "~/renderer/hooks";
import { Data } from "../+data";
const Editor = () => {
const isPortrait = usePortrait();
const { pinnedFiles } = useData<Data>();
const trpcUtils = trpc.useUtils();
const [isMounted, setMounted] = useState(false);
const project = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [curTabIdx, setCurTabIdx] = useState(0);
const [curOpenFiles, setOpenFiles] = useState<number[]>([]);
const pinnedFiles = trpc.file.getAll.useQuery(
{ isPinned: true },
{ enabled: !isMounted }
const [curOpenFiles, setOpenFiles] = useState<number[]>(
pinnedFiles.map((i) => i.id)
);
const openedFilesData = trpc.file.getAll.useQuery(
{ id: curOpenFiles },
{ enabled: curOpenFiles.length > 0 }
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles }
);
const [openedFiles, setOpenedFiles] = useState<any[]>([]);
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles);
const deleteFile = trpc.file.delete.useMutation({
onSuccess: (file) => {
@ -53,10 +51,6 @@ const Editor = () => {
},
});
useEffect(() => {
setMounted(true);
}, []);
const toggleSidebar = useCallback(() => {
const sidebar = sidebarPanel.current;
if (!sidebar) {
@ -74,15 +68,19 @@ const Editor = () => {
useCommandKey("b", toggleSidebar);
useEffect(() => {
if (!pinnedFiles.data?.length || curOpenFiles.length > 0) {
if (!pinnedFiles?.length || curOpenFiles.length > 0) {
return;
}
pinnedFiles.data.forEach((file) => {
pinnedFiles.forEach((file) => {
onOpenFile(file.id, false);
});
return () => {
setOpenFiles([]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pinnedFiles.data]);
}, [pinnedFiles]);
useEffect(() => {
if (openedFilesData.data) {
@ -170,7 +168,8 @@ const Editor = () => {
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
<ResizablePanel
ref={sidebarPanel}
defaultSize={isPortrait ? 0 : 25}
panelId={0}
defaultSize={25}
minSize={10}
collapsible
collapsedSize={0}
@ -183,12 +182,9 @@ const Editor = () => {
<ResizableHandle className="bg-slate-900" />
<ResizablePanel defaultSize={isPortrait ? 100 : 75}>
<ResizablePanelGroup
autoCapitalize="code-editor"
direction="vertical"
>
<ResizablePanel defaultSize={isPortrait ? 100 : 80} minSize={20}>
<ResizablePanel panelId={1} defaultSize={75}>
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
<ResizablePanel panelId={0} defaultSize={80} minSize={20}>
<Tabs
tabs={openFileList}
current={curTabIdx}
@ -197,19 +193,16 @@ const Editor = () => {
/>
</ResizablePanel>
{!isPortrait ? (
<>
<ResizableHandle />
<ResizablePanel
defaultSize={20}
collapsible
collapsedSize={0}
minSize={10}
>
<ConsoleLogger />
</ResizablePanel>
</>
) : null}
<ResizableHandle />
<ResizablePanel
panelId={1}
defaultSize={20}
collapsible
collapsedSize={0}
minSize={10}
>
<ConsoleLogger />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>

View File

@ -24,12 +24,17 @@ import {
import { cn, getUrl } from "~/lib/utils";
import FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard";
import { useParams } from "~/renderer/hooks";
import { useData, useParams } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner";
import { Data } from "../+data";
const FileListing = () => {
const pageData = useData<Data>();
const { onOpenFile, onFileChanged } = useEditorContext();
const createFileDlg = useDisclose<CreateFileSchema>();
const files = trpc.file.getAll.useQuery();
const files = trpc.file.getAll.useQuery(undefined, {
initialData: pageData.files,
});
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
@ -62,11 +67,17 @@ const FileListing = () => {
</DropdownMenu>
</div>
<div className="flex flex-col items-stretch flex-1 overflow-y-auto">
{fileList.map((file) => (
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
))}
</div>
{files.isLoading ? (
<div className="flex-1 flex items-center justify-center">
<Spinner />
</div>
) : (
<div className="flex flex-col items-stretch flex-1 overflow-y-auto">
{fileList.map((file) => (
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
))}
</div>
)}
<CreateFileDialog
disclose={createFileDlg}

View File

@ -4,6 +4,10 @@ import { getFileExt } from "~/lib/utils";
import React from "react";
import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "~/lib/trpc";
import { useData } from "~/renderer/hooks";
import { Data } from "../+data";
import ClientOnly from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner";
type Props = {
id: number;
@ -11,7 +15,12 @@ type Props = {
};
const FileViewer = ({ id, onFileContentChange }: Props) => {
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id);
const { pinnedFiles } = useData<Data>();
const initialData = pinnedFiles.find((i) => i.id === id);
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
initialData,
});
const updateFileContent = trpc.file.update.useMutation({
onSuccess: () => {
if (onFileContentChange) onFileContentChange();
@ -20,8 +29,9 @@ const FileViewer = ({ id, onFileContentChange }: Props) => {
});
if (isLoading) {
return <p>Loading...</p>;
return <LoadingLayout />;
}
if (!data || data.isDirectory) {
return <p>File not found.</p>;
}
@ -32,16 +42,36 @@ const FileViewer = ({ id, onFileContentChange }: Props) => {
const ext = getFileExt(filename);
return (
<CodeEditor
lang={ext}
value={data?.content || ""}
formatOnSave
onChange={(val) => updateFileContent.mutate({ id, content: val })}
/>
<ClientOnly fallback={<SSRCodeEditor value={data?.content} />}>
<CodeEditor
lang={ext}
value={data?.content || ""}
formatOnSave
onChange={(val) => updateFileContent.mutate({ id, content: val })}
/>
</ClientOnly>
);
}
return null;
};
const LoadingLayout = () => {
return (
<div className="w-full h-full flex items-center justify-center">
<Spinner />
</div>
);
};
const SSRCodeEditor = ({ value }: { value?: string | null }) => {
return (
<textarea
className="w-full h-full py-3 pl-11 pr-2 overflow-x-auto text-nowrap font-mono text-sm md:text-[16px] md:leading-[22px] bg-[#1a1b26] text-[#787c99]"
value={value || ""}
readOnly
/>
);
};
export default FileViewer;

View File

@ -31,6 +31,7 @@ const WebPreview = ({ url }: WebPreviewProps) => {
return (
<PanelComponent className="h-full flex flex-col bg-slate-800">
<div className="h-10 flex items-center">
<p className="flex-1 truncate text-xs uppercase pl-4">Preview</p>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
@ -38,18 +39,6 @@ const WebPreview = ({ url }: WebPreviewProps) => {
>
<FaRedo />
</Button>
<Input
className="flex-1 dark:bg-gray-900 dark:hover:bg-gray-950 h-8 rounded-full"
value={url || ""}
readOnly
/>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={() => {}}
>
<FaEllipsisV />
</Button>
</div>
{url != null ? (

536
pnpm-lock.yaml generated
View File

@ -71,9 +71,18 @@ dependencies:
console-feed:
specifier: ^3.5.0
version: 3.5.0(jquery@3.7.1)(react-dom@18.2.0)(react@18.2.0)
cookie-parser:
specifier: ^1.4.6
version: 1.4.6
cookiejs:
specifier: ^2.1.3
version: 2.1.3
copy-to-clipboard:
specifier: ^3.3.3
version: 3.3.3
cssnano:
specifier: ^6.0.3
version: 6.0.3(postcss@8.4.35)
drizzle-orm:
specifier: ^0.29.3
version: 0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
@ -92,6 +101,9 @@ dependencies:
nprogress:
specifier: ^0.2.0
version: 0.2.0
postcss:
specifier: ^8
version: 8.4.35
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -133,6 +145,9 @@ devDependencies:
'@swc/cli':
specifier: ^0.3.9
version: 0.3.9(@swc/core@1.4.2)
'@types/cookie-parser':
specifier: ^1.4.6
version: 1.4.6
'@types/express':
specifier: ^4.17.21
version: 4.17.21
@ -157,9 +172,6 @@ devDependencies:
drizzle-kit:
specifier: ^0.20.14
version: 0.20.14
postcss:
specifier: ^8
version: 8.4.35
tailwindcss:
specifier: ^3.3.0
version: 3.4.1
@ -2145,6 +2157,11 @@ packages:
resolution: {integrity: sha512-HfBVYUShvktA6M78jrCsRLyeE6l2NdaPJxKg095h4vk0bgWVfz0MgEOCQR/hjGdw0EFLEVcZANhqTZaHEzr36w==}
dev: false
/@trysound/sax@0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
dev: false
/@types/babel__core@7.20.5:
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
dependencies:
@ -2196,6 +2213,12 @@ packages:
'@types/node': 20.11.19
dev: true
/@types/cookie-parser@1.4.6:
resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==}
dependencies:
'@types/express': 4.17.21
dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@ -2608,6 +2631,10 @@ packages:
- supports-color
dev: false
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@ -2635,7 +2662,6 @@ packages:
electron-to-chromium: 1.4.678
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
dev: true
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -2700,9 +2726,17 @@ packages:
engines: {node: '>=14.16'}
dev: true
/caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001588
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
dev: false
/caniuse-lite@1.0.30001588:
resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==}
dev: true
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@ -2809,6 +2843,10 @@ packages:
hasBin: true
dev: false
/colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
dev: false
/commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@ -2816,7 +2854,6 @@ packages:
/commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
dev: true
/commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
@ -2866,15 +2903,32 @@ packages:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
/cookie-parser@1.4.6:
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
engines: {node: '>= 0.8.0'}
dependencies:
cookie: 0.4.1
cookie-signature: 1.0.6
dev: false
/cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false
/cookie@0.4.1:
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
engines: {node: '>= 0.6'}
dev: false
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/cookiejs@2.1.3:
resolution: {integrity: sha512-pA/nRQVka2eTXm1/Dq8pNt1PN+e1PJNItah0vL15qwpet81/tUfrAp8e0iiVM8WEAzDcTGK5/1hDyR6BdBZMVg==}
dev: false
/copy-anything@3.0.5:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
@ -2919,11 +2973,116 @@ packages:
shebang-command: 2.0.0
which: 2.0.2
/css-declaration-sorter@7.1.1(postcss@8.4.35):
resolution: {integrity: sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==}
engines: {node: ^14 || ^16 || >=18}
peerDependencies:
postcss: ^8.0.9
dependencies:
postcss: 8.4.35
dev: false
/css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
dev: false
/css-tree@2.2.1:
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
dependencies:
mdn-data: 2.0.28
source-map-js: 1.0.2
dev: false
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
dependencies:
mdn-data: 2.0.30
source-map-js: 1.0.2
dev: false
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: false
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
/cssnano-preset-default@6.0.3(postcss@8.4.35):
resolution: {integrity: sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
css-declaration-sorter: 7.1.1(postcss@8.4.35)
cssnano-utils: 4.0.1(postcss@8.4.35)
postcss: 8.4.35
postcss-calc: 9.0.1(postcss@8.4.35)
postcss-colormin: 6.0.2(postcss@8.4.35)
postcss-convert-values: 6.0.2(postcss@8.4.35)
postcss-discard-comments: 6.0.1(postcss@8.4.35)
postcss-discard-duplicates: 6.0.1(postcss@8.4.35)
postcss-discard-empty: 6.0.1(postcss@8.4.35)
postcss-discard-overridden: 6.0.1(postcss@8.4.35)
postcss-merge-longhand: 6.0.2(postcss@8.4.35)
postcss-merge-rules: 6.0.3(postcss@8.4.35)
postcss-minify-font-values: 6.0.1(postcss@8.4.35)
postcss-minify-gradients: 6.0.1(postcss@8.4.35)
postcss-minify-params: 6.0.2(postcss@8.4.35)
postcss-minify-selectors: 6.0.2(postcss@8.4.35)
postcss-normalize-charset: 6.0.1(postcss@8.4.35)
postcss-normalize-display-values: 6.0.1(postcss@8.4.35)
postcss-normalize-positions: 6.0.1(postcss@8.4.35)
postcss-normalize-repeat-style: 6.0.1(postcss@8.4.35)
postcss-normalize-string: 6.0.1(postcss@8.4.35)
postcss-normalize-timing-functions: 6.0.1(postcss@8.4.35)
postcss-normalize-unicode: 6.0.2(postcss@8.4.35)
postcss-normalize-url: 6.0.1(postcss@8.4.35)
postcss-normalize-whitespace: 6.0.1(postcss@8.4.35)
postcss-ordered-values: 6.0.1(postcss@8.4.35)
postcss-reduce-initial: 6.0.2(postcss@8.4.35)
postcss-reduce-transforms: 6.0.1(postcss@8.4.35)
postcss-svgo: 6.0.2(postcss@8.4.35)
postcss-unique-selectors: 6.0.2(postcss@8.4.35)
dev: false
/cssnano-utils@4.0.1(postcss@8.4.35):
resolution: {integrity: sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/cssnano@6.0.3(postcss@8.4.35):
resolution: {integrity: sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
cssnano-preset-default: 6.0.3(postcss@8.4.35)
lilconfig: 3.1.1
postcss: 8.4.35
dev: false
/csso@5.0.5:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
dependencies:
css-tree: 2.2.1
dev: false
/csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
dev: false
@ -3020,6 +3179,33 @@ packages:
/dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dreamopt@0.8.0:
resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==}
engines: {node: '>=0.4.0'}
@ -3144,7 +3330,6 @@ packages:
/electron-to-chromium@1.4.678:
resolution: {integrity: sha512-NbdGC2p0O5Q5iVhLEsNBSfytaw7wbEFJlIvaF71wi6QDtLAph5/rVogjyOpf/QggJIt8hNK3KdwNJnc2bzckbw==}
dev: true
/emmet@2.4.6:
resolution: {integrity: sha512-dJfbdY/hfeTyf/Ef7Y7ubLYzkBvPQ912wPaeVYpAxvFxkEBf/+hJu4H6vhAvFN6HlxqedlfVn2x1S44FfQ97pg==}
@ -3182,6 +3367,11 @@ packages:
dependencies:
once: 1.4.0
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/env-paths@3.0.0:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -3317,7 +3507,6 @@ packages:
/escalade@3.1.2:
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
engines: {node: '>=6'}
dev: true
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@ -3982,10 +4171,18 @@ packages:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: false
/lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: false
/lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
dev: true
/lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -4042,6 +4239,14 @@ packages:
semver: 6.3.1
dev: false
/mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
dev: false
/mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: false
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@ -4269,7 +4474,6 @@ packages:
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true
/nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
@ -4320,6 +4524,12 @@ packages:
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
dev: false
/nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: false
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@ -4449,6 +4659,77 @@ packages:
nice-napi: 1.0.2
dev: true
/postcss-calc@9.0.1(postcss@8.4.35):
resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.2.2
dependencies:
postcss: 8.4.35
postcss-selector-parser: 6.0.15
postcss-value-parser: 4.2.0
dev: false
/postcss-colormin@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
caniuse-api: 3.0.0
colord: 2.9.3
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-convert-values@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-discard-comments@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/postcss-discard-duplicates@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/postcss-discard-empty@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/postcss-discard-overridden@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/postcss-import@15.1.0(postcss@8.4.35):
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@ -4485,6 +4766,74 @@ packages:
postcss: 8.4.35
yaml: 2.3.4
/postcss-merge-longhand@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
stylehacks: 6.0.2(postcss@8.4.35)
dev: false
/postcss-merge-rules@6.0.3(postcss@8.4.35):
resolution: {integrity: sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
caniuse-api: 3.0.0
cssnano-utils: 4.0.1(postcss@8.4.35)
postcss: 8.4.35
postcss-selector-parser: 6.0.15
dev: false
/postcss-minify-font-values@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-minify-gradients@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
colord: 2.9.3
cssnano-utils: 4.0.1(postcss@8.4.35)
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-minify-params@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
cssnano-utils: 4.0.1(postcss@8.4.35)
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-minify-selectors@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-selector-parser: 6.0.15
dev: false
/postcss-nested@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
engines: {node: '>=12.0'}
@ -4494,6 +4843,128 @@ packages:
postcss: 8.4.35
postcss-selector-parser: 6.0.15
/postcss-normalize-charset@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
dev: false
/postcss-normalize-display-values@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-positions@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-repeat-style@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-string@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-timing-functions@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-unicode@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-url@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-normalize-whitespace@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-ordered-values@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
cssnano-utils: 4.0.1(postcss@8.4.35)
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-reduce-initial@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
caniuse-api: 3.0.0
postcss: 8.4.35
dev: false
/postcss-reduce-transforms@6.0.1(postcss@8.4.35):
resolution: {integrity: sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
dev: false
/postcss-selector-parser@6.0.15:
resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==}
engines: {node: '>=4'}
@ -4501,6 +4972,27 @@ packages:
cssesc: 3.0.0
util-deprecate: 1.0.2
/postcss-svgo@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==}
engines: {node: ^14 || ^16 || >= 18}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-value-parser: 4.2.0
svgo: 3.2.0
dev: false
/postcss-unique-selectors@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
postcss: 8.4.35
postcss-selector-parser: 6.0.15
dev: false
/postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@ -5080,6 +5572,17 @@ packages:
resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==}
dev: false
/stylehacks@6.0.2(postcss@8.4.35):
resolution: {integrity: sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==}
engines: {node: ^14 || ^16 || >=18.0}
peerDependencies:
postcss: ^8.4.31
dependencies:
browserslist: 4.23.0
postcss: 8.4.35
postcss-selector-parser: 6.0.15
dev: false
/sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@ -5110,6 +5613,20 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/svgo@3.2.0:
resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
'@trysound/sax': 0.2.0
commander: 7.2.0
css-select: 5.1.0
css-tree: 2.3.1
css-what: 6.1.0
csso: 5.0.5
picocolors: 1.0.0
dev: false
/tailwind-merge@2.2.1:
resolution: {integrity: sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==}
dependencies:
@ -5310,7 +5827,6 @@ packages:
browserslist: 4.23.0
escalade: 3.1.2
picocolors: 1.0.0
dev: true
/use-callback-ref@1.3.1(@types/react@18.2.57)(react@18.2.0):
resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==}

View File

@ -2,7 +2,7 @@ import type { Config } from "vike/types";
export default {
clientRouting: true,
passToClient: ["routeParams"],
passToClient: ["routeParams", "cookies"],
meta: {
title: {
env: { server: true, client: true },

View File

@ -1,4 +1,5 @@
//
import type { Request } from "express";
declare global {
namespace Vike {
interface PageContext {
@ -12,6 +13,8 @@ declare global {
description?: string;
};
abortReason?: string;
req: Request;
cookies: Record<string, string>;
}
}
}

View File

@ -1,16 +1,12 @@
import { type Request, type Response, Router } from "express";
import { getFileExt } from "~/lib/utils";
import db from "~/server/db";
import { file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
import { serveHtml } from "./serve-html";
import { Mime } from "mime/lite";
import standardTypes from "mime/types/standard.js";
import otherTypes from "mime/types/other.js";
import { serveJs } from "./serve-js";
import { type Request, type Response, Router } from "express";
const mime = new Mime(standardTypes, otherTypes);
mime.define({ "text/javascript": ["jsx", "tsx"] }, true);
import { getMimeType } from "~/server/lib/mime";
import { postcss } from "./postcss";
const get = async (req: Request, res: Response) => {
const { slug, ...pathParams } = req.params as any;
@ -35,10 +31,11 @@ const get = async (req: Request, res: Response) => {
content = await serveJs(fileData);
}
res.setHeader(
"Content-Type",
mime.getType(fileData.filename) || "application/octet-stream"
);
if (["css"].includes(ext)) {
content = await postcss(fileData);
}
res.setHeader("Content-Type", getMimeType(fileData.filename));
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

View File

@ -0,0 +1,34 @@
import postcssPlugin from "postcss";
import tailwindcss from "tailwindcss";
import cssnano from "cssnano";
import { fileExists, getProjectDir } from "~/server/lib/utils";
import { FileSchema } from "~/server/db/schema/file";
import { unpackProject } from "~/server/lib/unpack-project";
export const postcss = async (fileData: FileSchema) => {
const content = fileData.content || "";
const projectDir = getProjectDir();
if (!fileExists(projectDir)) {
return content;
}
try {
await unpackProject({ ext: "ts,tsx,js,jsx,html" });
const result = await postcssPlugin([
tailwindcss({
content: [projectDir + "/**/*.{ts,tsx,js,jsx,html}"],
}),
cssnano({
preset: ["default", { discardComments: { removeAll: true } }],
}),
]).process(content, {
from: undefined,
});
return result.css;
} catch (err) {
return content;
}
};

View File

@ -2,6 +2,8 @@ import db from "~/server/db";
import { FileSchema, file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
const preventHtmlDirectAccess = `<script>if (window === window.parent) {window.location.href = '/';}</script>`;
export const serveHtml = async (fileData: FileSchema) => {
const layout = await db.query.file.findFirst({
where: and(
@ -14,15 +16,16 @@ export const serveHtml = async (fileData: FileSchema) => {
});
let content = fileData.content || "";
if (!layout?.content) {
return content;
if (layout?.content != null) {
content = layout.content.replace("{CONTENT}", content);
}
content = layout.content.replace("{CONTENT}", content);
const bodyOpeningTagIdx = content.indexOf("<body");
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
const injectScripts = [
'<script src="/js/hook-console.js"></script>',
preventHtmlDirectAccess,
];
const importMaps = [
{ name: "react", url: "https://esm.sh/react@18.2.0" },
@ -45,7 +48,5 @@ export const serveHtml = async (fileData: FileSchema) => {
content.substring(firstScriptTagIdx);
}
// content = content.replace(/ {2}|\r\n|\n|\r/gm, "");
return content;
};

View File

@ -1,9 +1,6 @@
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import { Request } from "express";
export const createContext = async ({
req,
res,
}: CreateExpressContextOptions) => {
export const createContext = async ({ req }: { req: Request }) => {
return {};
};

View File

@ -1,7 +1,12 @@
import { createContext } from "./context";
import { appRouter } from "../../routers/_app";
import { createCallerFactory } from ".";
import { PageContext } from "vike/types";
const trpcServer = createCallerFactory(appRouter)(createContext);
const trpcServer = async (ctx: PageContext) => {
const createCaller = createCallerFactory(appRouter);
const context = await createContext({ req: ctx.req });
return createCaller(context);
};
export default trpcServer;

View File

@ -12,6 +12,52 @@ const main = async () => {
})
.returning();
// await db
// .insert(file)
// .values([
// {
// userId: adminUser.id,
// path: "index.html",
// filename: "index.html",
// content: '<p class="text-lg text-red-500">Hello world!</p>',
// },
// {
// userId: adminUser.id,
// path: "styles.css",
// filename: "styles.css",
// content: "body { padding: 16px; }",
// },
// {
// userId: adminUser.id,
// path: "script.js",
// filename: "script.js",
// content: "console.log('hello world!');",
// },
// {
// userId: adminUser.id,
// path: "_layout.html",
// filename: "_layout.html",
// content: `<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
// <title>Document</title>
// <link rel="stylesheet" href="styles.css">
// <script src="https://cdn.tailwindcss.com"></script>
// </head>
// <body>
// {CONTENT}
// <script src="script.js" type="module" defer></script>
// </body>
// </html>`,
// },
// ])
// .execute();
// react template
await db
.insert(file)
.values([
@ -19,40 +65,69 @@ const main = async () => {
userId: adminUser.id,
path: "index.html",
filename: "index.html",
content: '<p class="text-lg text-red-500">Hello world!</p>',
content: `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>React + Tailwind Template</title>
<link rel="stylesheet" href="globals.css" />
</head>
<body>
<div id="app"></div>
<script src="index.jsx" type="module" defer></script>
</body>
</html>
`,
},
{
userId: adminUser.id,
path: "styles.css",
filename: "styles.css",
content: "body { padding: 16px; }",
},
{
userId: adminUser.id,
path: "script.js",
filename: "script.js",
content: "console.log('hello world!');",
},
{
userId: adminUser.id,
path: "_layout.html",
filename: "_layout.html",
content: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
path: "globals.css",
filename: "globals.css",
content: `@tailwind base;
@tailwind components;
@tailwind utilities;
<link rel="stylesheet" href="styles.css">
body {
@apply p-4;
}
`,
},
{
userId: adminUser.id,
path: "index.jsx",
filename: "index.jsx",
content: `import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
{CONTENT}
<script src="script.js" type="module" defer></script>
</body>
</html>`,
const root = createRoot(document.getElementById("app"));
root.render(<App />);
`,
},
{
userId: adminUser.id,
path: "App.jsx",
filename: "App.jsx",
isPinned: true,
content: `import React from "react";
const App = () => {
return (
<div>
<h1 class="text-xl font-medium text-blue-500">
React + Tailwind Template!
</h1>
<p>Open App.jsx to edit this text.</p>
</div>
);
};
export default App;
`,
},
])
.execute();

View File

@ -1,6 +1,7 @@
import express from "express";
import { renderPage } from "vike/server";
import { IS_DEV } from "./lib/consts";
import cookieParser from "cookie-parser";
import api from "./api";
async function createServer() {
@ -22,11 +23,13 @@ async function createServer() {
app.use(express.static(root + "/dist/client"));
}
app.use(cookieParser());
app.use("/api", api);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
const pageContext = {};
const pageContext = { req, cookies: req.cookies };
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
const { httpResponse } = ctx;

11
server/lib/mime.ts Normal file
View File

@ -0,0 +1,11 @@
import { Mime } from "mime/lite";
import standardTypes from "mime/types/standard.js";
import otherTypes from "mime/types/other.js";
const mime = new Mime(standardTypes, otherTypes);
mime.define({ "text/javascript": ["jsx", "tsx"] }, true);
export const getMimeType = (
ext: string,
defaultMime: string = "application/octet-stream"
) => mime.getType(ext) || defaultMime;

View File

@ -0,0 +1,50 @@
import fs from "node:fs/promises";
import path from "node:path";
import { and, eq, isNull } from "drizzle-orm";
import db from "../db";
import { file } from "../db/schema/file";
import { fileExists, getProjectDir } from "./utils";
import { getFileExt } from "~/lib/utils";
type UnpackProjectOptions = {
ext: string;
};
export const unpackProject = async (
opt: Partial<UnpackProjectOptions> = {}
) => {
const files = await db.query.file.findMany({
where: and(
eq(file.isDirectory, false),
eq(file.isFile, false),
isNull(file.deletedAt)
),
});
const projectDir = getProjectDir();
if (!fileExists(projectDir)) {
await fs.mkdir(projectDir, { recursive: true });
}
for (const file of files) {
const ext = getFileExt(file.filename);
// skip file if not in included extension list
if (opt.ext && opt.ext.length > 0 && !opt.ext.split(",").includes(ext)) {
continue;
}
const fpath = path.resolve(
projectDir,
file.path.replace(/(\.{2})(\/+)/g, "")
);
const dir = path.dirname(fpath);
if (!fileExists(dir)) {
await fs.mkdir(dir, { recursive: true });
}
await fs.writeFile(fpath, file.content || "");
}
return projectDir;
};

15
server/lib/utils.ts Normal file
View File

@ -0,0 +1,15 @@
import fs from "node:fs";
import path from "node:path";
export const fileExists = (path: string) => {
try {
fs.accessSync(path, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
};
export const getProjectDir = () => {
return path.resolve(process.cwd(), "storage/tmp/project1");
};

View File

@ -21,7 +21,7 @@ const fileRouter = router({
opt?.isPinned ? eq(file.isPinned, true) : undefined
),
orderBy: [desc(file.isDirectory), asc(file.filename)],
columns: { content: false },
columns: !file.isPinned ? { content: true } : undefined,
});
return files;

View File

@ -9,6 +9,11 @@ const config = {
"~": path.resolve("./"),
},
},
server: {
watch: {
ignored: [path.resolve(__dirname, "storage/**")],
},
},
};
export default config;