diff --git a/index.html b/index.html index dfa13bf2..41ef5b1b 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,16 @@
+ + diff --git a/package.json b/package.json index 653b2c27..a33f93bb 100644 --- a/package.json +++ b/package.json @@ -86,9 +86,11 @@ "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", + "cheerio": "^1.0.0-rc.12", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "glob": "^10.4.1", "jsdom": "^22.1.0", "pnpm": "^8.14.1", "postcss": "^8.4.31", @@ -96,6 +98,8 @@ "tsc-alias": "^1.8.8", "typescript": "^5.0.2", "vite": "^5.0.12", + "vite-plugin-external": "^4.3.1", + "vite-plugin-externalize-dependencies": "^0.12.0", "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^0.34.6" diff --git a/public/_headers b/public/_headers new file mode 100644 index 00000000..c776a479 --- /dev/null +++ b/public/_headers @@ -0,0 +1,2 @@ +/* + Access-Control-Allow-Origin: * diff --git a/src/components/utils.ts b/src/components/utils.ts index 940515c2..1489d71f 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -5,8 +5,3 @@ import { useReducer } from "react"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - -export function useForceUpdate() { - const [, forceUpdate] = useReducer((x) => x + 1, 0); - return forceUpdate; -} diff --git a/src/datatypes/bot/datatype.ts b/src/datatypes/bot/datatype.ts index 1780fe9f..7b8b7f1c 100644 --- a/src/datatypes/bot/datatype.ts +++ b/src/datatypes/bot/datatype.ts @@ -1,22 +1,17 @@ +import { MarkdownDatatype } from "@/datatypes/essay/datatype"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { ContactDoc, RegisteredContactDoc } from "@/os/explorer/account"; -import { MarkdownDatatype } from "@/datatypes/markdown/datatype"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; -import { type DataType } from "@/os/datatypes"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; -import { Bot } from "lucide-react"; import { EssayEditingBotDoc } from "./schema"; const BOT_AVATAR_URL = "automerge:uL1duhieqUV4qaeHGHX1dg8FnNy" as AutomergeUrl; -export const EssayEditingBotDatatype: DataType< +export const EssayEditingBotDatatype: DataTypeWitoutMetaData< EssayEditingBotDoc, never, never > = { - id: "bot", - name: "Bot", - isExperimental: true, - icon: Bot, init: (doc: any, repo: Repo) => { const contactHandle = repo.create(); const promptHandle = repo.create(); diff --git a/src/datatypes/bot/essayEditingBot.ts b/src/datatypes/bot/essayEditingBot.ts index 6ba52a7a..41ac7376 100644 --- a/src/datatypes/bot/essayEditingBot.ts +++ b/src/datatypes/bot/essayEditingBot.ts @@ -1,7 +1,7 @@ import { RegisteredContactDoc } from "@/os/explorer/account"; import { DEFAULT_MODEL, openaiClient } from "@/os/lib/llm"; import { createBranch } from "@/os/versionControl/branches"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; import { AutomergeUrl, DocHandle, Repo } from "@automerge/automerge-repo"; import { splice } from "@automerge/automerge/next"; import { EssayEditingBotDoc } from "./schema"; diff --git a/src/datatypes/bot/module.ts b/src/datatypes/bot/module.ts new file mode 100644 index 00000000..7cb64f04 --- /dev/null +++ b/src/datatypes/bot/module.ts @@ -0,0 +1,21 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; +import { Bot } from "lucide-react"; +import { EssayEditingBotDoc } from "./schema"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "bot", + name: "Bot", + icon: Bot, + isExperimental: true, + }, + + load: () => + import("./datatype").then( + ({ EssayEditingBotDatatype }) => EssayEditingBotDatatype + ), +}); diff --git a/src/datatypes/datagrid/datatype.ts b/src/datatypes/datagrid/datatype.ts index 93e7977b..cd094cf7 100644 --- a/src/datatypes/datagrid/datatype.ts +++ b/src/datatypes/datagrid/datatype.ts @@ -1,9 +1,8 @@ -import { DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; import { Annotation } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; import { pick } from "lodash"; -import { Sheet } from "lucide-react"; import { DataGridDoc, DataGridDocAnchor } from "./schema"; // When a copy of the document has been made, @@ -123,15 +122,11 @@ const patchesToAnnotations = ( }); }; -export const DataGridDatatype: DataType< +export const DataGridDatatype: DataTypeWitoutMetaData< DataGridDoc, DataGridDocAnchor, string > = { - id: "datagrid", - name: "Spreadsheet", - isExperimental: true, - icon: Sheet, init, getTitle, setTitle, diff --git a/src/datatypes/datagrid/module.ts b/src/datatypes/datagrid/module.ts new file mode 100644 index 00000000..1aa795cb --- /dev/null +++ b/src/datatypes/datagrid/module.ts @@ -0,0 +1,19 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; +import { Sheet } from "lucide-react"; +import { DataGridDoc, DataGridDocAnchor } from "./schema"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "datagrid", + name: "Spreadsheet", + icon: Sheet, + isExperimental: true, + }, + + load: () => + import("./datatype").then(({ DataGridDatatype }) => DataGridDatatype), +}); diff --git a/src/datatypes/markdown/datatype.ts b/src/datatypes/essay/datatype.ts similarity index 97% rename from src/datatypes/markdown/datatype.ts rename to src/datatypes/essay/datatype.ts index a651187d..cd0a7a77 100644 --- a/src/datatypes/markdown/datatype.ts +++ b/src/datatypes/essay/datatype.ts @@ -1,4 +1,4 @@ -import { DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { FileExportMethod } from "@/os/fileExports"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; import { @@ -12,9 +12,8 @@ import { } from "@/os/versionControl/utils"; import { next as A } from "@automerge/automerge"; import { Repo } from "@automerge/automerge-repo"; -import { Doc, splice } from "@automerge/automerge/next"; +import { splice } from "@automerge/automerge/next"; import { pick } from "lodash"; -import { Text } from "lucide-react"; import { AssetsDoc } from "../../tools/essay/assets"; import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; @@ -289,14 +288,11 @@ const fileExportMethods: FileExportMethod[] = [ }, ]; -export const MarkdownDatatype: DataType< +export const MarkdownDatatype: DataTypeWitoutMetaData< MarkdownDoc, MarkdownDocAnchor, string > = { - id: "essay", - name: "Essay", - icon: Text, init, getTitle, markCopy, diff --git a/src/datatypes/markdown/index.ts b/src/datatypes/essay/index.ts similarity index 100% rename from src/datatypes/markdown/index.ts rename to src/datatypes/essay/index.ts diff --git a/src/datatypes/essay/module.ts b/src/datatypes/essay/module.ts new file mode 100644 index 00000000..66fb7362 --- /dev/null +++ b/src/datatypes/essay/module.ts @@ -0,0 +1,18 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Text } from "lucide-react"; +import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; +import { Module } from "@/os/modules"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "essay", + name: "Essay", + icon: Text, + }, + + load: () => + import("./datatype").then(({ MarkdownDatatype }) => MarkdownDatatype), +}); diff --git a/src/datatypes/markdown/schema.ts b/src/datatypes/essay/schema.ts similarity index 100% rename from src/datatypes/markdown/schema.ts rename to src/datatypes/essay/schema.ts diff --git a/src/datatypes/markdown/utils.ts b/src/datatypes/essay/utils.ts similarity index 100% rename from src/datatypes/markdown/utils.ts rename to src/datatypes/essay/utils.ts diff --git a/src/datatypes/folder/datatype.ts b/src/datatypes/folder/datatype.ts index 1ce0350b..9e379f1a 100644 --- a/src/datatypes/folder/datatype.ts +++ b/src/datatypes/folder/datatype.ts @@ -1,5 +1,4 @@ -import { DataType } from "@/os/datatypes"; -import { FolderIcon } from "lucide-react"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { FolderDoc } from "."; export const init = (doc: any) => { @@ -22,10 +21,7 @@ export const setTitle = (doc: FolderDoc, title: string) => { doc.title = title; }; -export const FolderDatatype: DataType = { - id: "folder", - name: "Folder", - icon: FolderIcon, +export const FolderDatatype: DataTypeWitoutMetaData = { init, getTitle, setTitle, diff --git a/src/datatypes/folder/module.ts b/src/datatypes/folder/module.ts new file mode 100644 index 00000000..9a619162 --- /dev/null +++ b/src/datatypes/folder/module.ts @@ -0,0 +1,17 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Folder } from "lucide-react"; +import { FolderDoc } from "./schema"; +import { Module } from "@/os/modules"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "folder", + name: "Folder", + icon: Folder, + }, + + load: () => import("./datatype").then(({ FolderDatatype }) => FolderDatatype), +}); diff --git a/src/datatypes/kanban/datatype.ts b/src/datatypes/kanban/datatype.ts index 690603e0..bf9d777f 100644 --- a/src/datatypes/kanban/datatype.ts +++ b/src/datatypes/kanban/datatype.ts @@ -1,39 +1,15 @@ -import { DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData, useDataType } from "@/os/datatypes"; import { ChangeGroup } from "@/os/versionControl/groupChanges"; import { Annotation, - HasVersionControlMetadata, initVersionControlMetadata, } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; -import { KanbanSquare } from "lucide-react"; - -export type Lane = { - id: string; - title: string; - cardIds: string[]; -}; - -export type Card = { - id: string; - title: string; - description: string; - label: string; -}; - -export type KanbanBoardDocAnchor = - | { type: "card"; id: string } - | { type: "lane"; id: string }; - -export type KanbanBoardDoc = { - title: string; - lanes: Lane[]; - cards: Card[]; -} & HasVersionControlMetadata; +import { Card, KanbanBoardDoc, KanbanBoardDocAnchor, Lane } from "./schema"; // When a copy of the document has been made, // update the title so it's more clear which one is the copy vs original. -export const markCopy = () => { +const markCopy = () => { console.error("todo"); }; @@ -45,7 +21,7 @@ const setTitle = async (doc: KanbanBoardDoc, title: string) => { doc.title = title; }; -export const init = (doc: any) => { +const init = (doc: any) => { doc.title = "Untitled Board"; doc.lanes = []; doc.cards = []; @@ -240,15 +216,11 @@ const actions = { }, }; -export const KanbanBoardDatatype: DataType< +export const KanbanBoardDatatype: DataTypeWitoutMetaData< KanbanBoardDoc, KanbanBoardDocAnchor, undefined > = { - id: "kanban", - name: "Kanban Board", - isExperimental: true, - icon: KanbanSquare, init, getTitle, setTitle, diff --git a/src/datatypes/kanban/index.ts b/src/datatypes/kanban/index.ts index d0380db5..320a9929 100644 --- a/src/datatypes/kanban/index.ts +++ b/src/datatypes/kanban/index.ts @@ -1,4 +1,3 @@ -import { KanbanBoardDatatype } from "./datatype"; -export default KanbanBoardDatatype; - +export { KanbanBoardDatatype } from "./datatype"; +export * from "./schema"; export * from "./useDocumentWithActions"; diff --git a/src/datatypes/kanban/module.ts b/src/datatypes/kanban/module.ts new file mode 100644 index 00000000..79dbc3c8 --- /dev/null +++ b/src/datatypes/kanban/module.ts @@ -0,0 +1,19 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; +import { KanbanSquare } from "lucide-react"; +import { KanbanBoardDoc, KanbanBoardDocAnchor } from "./schema"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "kanban", + name: "Kanban Board", + icon: KanbanSquare, + isExperimental: true, + }, + + load: () => + import("./datatype").then(({ KanbanBoardDatatype }) => KanbanBoardDatatype), +}); diff --git a/src/datatypes/kanban/schema.ts b/src/datatypes/kanban/schema.ts new file mode 100644 index 00000000..267d2a65 --- /dev/null +++ b/src/datatypes/kanban/schema.ts @@ -0,0 +1,24 @@ +import { HasVersionControlMetadata } from "@/os/versionControl/schema"; + +export type Lane = { + id: string; + title: string; + cardIds: string[]; +}; + +export type Card = { + id: string; + title: string; + description: string; + label: string; +}; + +export type KanbanBoardDocAnchor = + | { type: "card"; id: string } + | { type: "lane"; id: string }; + +export type KanbanBoardDoc = { + title: string; + lanes: Lane[]; + cards: Card[]; +} & HasVersionControlMetadata; diff --git a/src/datatypes/kanban/useDocumentWithActions.ts b/src/datatypes/kanban/useDocumentWithActions.ts index 109fdb80..f47f26cc 100644 --- a/src/datatypes/kanban/useDocumentWithActions.ts +++ b/src/datatypes/kanban/useDocumentWithActions.ts @@ -1,7 +1,7 @@ import { AutomergeUrl } from "@automerge/automerge-repo"; import { useDocument, useHandle } from "@automerge/automerge-repo-react-hooks"; import { useMemo } from "react"; -import { DataType } from "../../os/datatypes"; +import { DataType, useDataType } from "../../os/datatypes"; /** Returns a doc with helper actions from a datatype definition. * @@ -14,13 +14,19 @@ import { DataType } from "../../os/datatypes"; */ export const useDocumentWithActions = ( docUrl: AutomergeUrl, - datatype: DataType + datatypeId: string ) => { + const dataType = useDataType(datatypeId); const [doc, changeDoc] = useDocument(docUrl); const handle = useHandle(docUrl); const actions = useMemo(() => { const result = {}; - for (const [key, value] of Object.entries(datatype.actions)) { + + if (!dataType) { + return; + } + + for (const [key, value] of Object.entries(dataType.actions)) { result[key] = (args: object) => { handle.change( (doc: D) => { @@ -36,6 +42,6 @@ export const useDocumentWithActions = ( }; } return result as Record void>; - }, [datatype, handle]); + }, [dataType, handle]); return [doc, changeDoc, actions] as const; }; diff --git a/src/datatypes/module/datatype.ts b/src/datatypes/module/datatype.ts new file mode 100644 index 00000000..1e41a4cc --- /dev/null +++ b/src/datatypes/module/datatype.ts @@ -0,0 +1,29 @@ +import { DataTypeWitoutMetaData } from "@/os/datatypes"; +import { ModuleDoc } from "./schema"; + +export const init = (doc: any) => { + doc.title = "Untitled Module"; + doc.docs = []; +}; + +// When a copy of the document has been made, +// update the title so it's more clear which one is the copy vs original. +// TODO: generalize this to a HasTitle schema? +export const markCopy = (doc: ModuleDoc) => { + doc.title = `Copy of ${doc.title}`; +}; + +export const getTitle = async (doc: any) => { + return doc.title; +}; + +export const setTitle = (doc: ModuleDoc, title: string) => { + doc.title = title; +}; + +export const ModuleDataType: DataTypeWitoutMetaData = { + init, + getTitle, + setTitle, + markCopy, +}; diff --git a/src/datatypes/module/index.ts b/src/datatypes/module/index.ts new file mode 100644 index 00000000..945d97c2 --- /dev/null +++ b/src/datatypes/module/index.ts @@ -0,0 +1,4 @@ +import { ModuleDataType } from "./datatype"; +export default ModuleDataType; + +export * from "./schema"; diff --git a/src/datatypes/module/module.ts b/src/datatypes/module/module.ts new file mode 100644 index 00000000..787a9979 --- /dev/null +++ b/src/datatypes/module/module.ts @@ -0,0 +1,18 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Package } from "lucide-react"; +import { ModuleDoc } from "./schema"; +import { Module } from "@/os/modules"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "module", + name: "Module", + icon: Package, + isExperimental: true, + }, + + load: () => import("./datatype").then(({ ModuleDataType }) => ModuleDataType), +}); diff --git a/src/datatypes/module/schema.ts b/src/datatypes/module/schema.ts new file mode 100644 index 00000000..509fc834 --- /dev/null +++ b/src/datatypes/module/schema.ts @@ -0,0 +1,6 @@ +import { AutomergeUrl } from "@automerge/automerge-repo"; + +export type ModuleDoc = { + title: string; + url: AutomergeUrl; +}; diff --git a/src/datatypes/tldraw/datatype.ts b/src/datatypes/tldraw/datatype.ts index 08b99f74..58bc73bf 100644 --- a/src/datatypes/tldraw/datatype.ts +++ b/src/datatypes/tldraw/datatype.ts @@ -1,16 +1,20 @@ -import { next as A } from "@automerge/automerge"; -import { DataType } from "@/os/datatypes"; -import { init as tldrawinit } from "automerge-tldraw"; -import { PenLine } from "lucide-react"; -import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; -import { pick } from "lodash"; -import { TLShape, TLShapeId, createTLStore } from "@tldraw/tldraw"; import { Annotation, initVersionControlMetadata, } from "@/os/versionControl/schema"; -import { defaultShapeUtils, Editor } from "@tldraw/tldraw"; +import { next as A } from "@automerge/automerge"; +import { + Editor, + TLShape, + TLShapeId, + createTLStore, + defaultShapeUtils, +} from "@tldraw/tldraw"; +import { init as tldrawinit } from "automerge-tldraw"; +import { pick } from "lodash"; +import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; // When a copy of the document has been made, // update the title so it's more clear which one is the copy vs original. @@ -293,10 +297,11 @@ const valueOfAnnotation = (annotation: Annotation) => { } }; -export const TLDrawDatatype: DataType = { - id: "tldraw", - name: "Drawing", - icon: PenLine, +export const TLDrawDatatype: DataTypeWitoutMetaData< + TLDrawDoc, + TLDrawDocAnchor, + TLShape +> = { init, getTitle, setTitle, diff --git a/src/datatypes/tldraw/module.ts b/src/datatypes/tldraw/module.ts new file mode 100644 index 00000000..6ae12d3c --- /dev/null +++ b/src/datatypes/tldraw/module.ts @@ -0,0 +1,18 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; +import { TLShape } from "@tldraw/tldraw"; +import { PenLine } from "lucide-react"; +import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "tldraw", + name: "Drawing", + icon: PenLine, + }, + + load: () => import("./datatype").then(({ TLDrawDatatype }) => TLDrawDatatype), +}); diff --git a/src/os/datatypes.ts b/src/os/datatypes.ts index 63622ffe..658be60b 100644 --- a/src/os/datatypes.ts +++ b/src/os/datatypes.ts @@ -2,38 +2,32 @@ import { ChangeGroup, DecodedChangeWithMetadata, } from "@/os/versionControl/groupChanges"; -import { - Annotation, - HasVersionControlMetadata, -} from "@/os/versionControl/schema"; +import { Annotation } from "@/os/versionControl/schema"; import { TextPatch } from "@/os/versionControl/utils"; import { next as A, Doc } from "@automerge/automerge"; import { Repo } from "@automerge/automerge-repo"; // datatypes - -import bot from "@/datatypes/bot"; -import datagrid from "@/datatypes/datagrid"; -import folder from "@/datatypes/folder"; -import kanban from "@/datatypes/kanban"; -import markdown from "@/datatypes/markdown"; -import tldraw from "@/datatypes/tldraw"; import { FileExportMethod } from "./fileExports"; +import { Module, useModule } from "./modules"; -export type CoreDataType = { - id: string; +export type DataTypeMetadata = { + id: DatatypeId; name: string; icon: any; + + /* Marking a data types as experimental hides it by default + * so the user has to enable them in their account first */ + isExperimental?: boolean; +}; + +export type CoreDataType = { init: (doc: D, repo: Repo) => void; getTitle: (doc: D, repo: Repo) => Promise; setTitle?: (doc: any, title: string) => void; markCopy: (doc: D) => void; // TODO: this shouldn't be part of the interface actions?: Record, args: object) => void>; fileExportMethods?: FileExportMethod[]; - - /* Marking a data types as experimental hides it by default - * so the user has to enable them in their account first */ - isExperimental?: boolean; }; export type VersionedDataType = { @@ -110,22 +104,47 @@ export type VersionedDataType = { sortAnchorsBy?: (doc: D, anchor: T) => any; }; -export type DataType = CoreDataType & VersionedDataType; +export type DataTypeWitoutMetaData = CoreDataType & + VersionedDataType; + +export type DataType = DataTypeWitoutMetaData & + DataTypeMetadata; -// TODO: we can narrow the types below by constructing a mapping from datatype IDs -// to the corresponding typescript type. This will be more natural once we have a -// schema system for generating typescript types. +const dataTypesFolder: Record< + string, + { default: Module> } +> = import.meta.glob("../datatypes/*/module.@(ts|js|tsx|jsx)", { + eager: true, +}); -export const DATA_TYPES: Record< +const DATA_TYPE_MODULES: Record< string, - DataType, unknown, unknown> -> = { - essay: markdown, // todo: migrate, we can't just rename it - tldraw, - datagrid, - bot, - kanban, - folder, -} as const; - -export type DatatypeId = keyof typeof DATA_TYPES; + Module> +> = {}; + +for (const [path, { default: module }] of Object.entries(dataTypesFolder)) { + const id = path.split("/")[2]; + + if (id !== module.metadata.id) { + throw new Error( + `Can't load datatype: id "${module.metadata.id}" does not match the folder name "${id}" ` + ); + } + + DATA_TYPE_MODULES[id] = module; +} + +export const useDataTypeModules = () => { + return DATA_TYPE_MODULES; +}; + +export const useDataType = ( + dataTypeId: DatatypeId +): DataType | undefined => { + const dataTypeModules = useDataTypeModules(); + const dataType = useModule(dataTypeModules[dataTypeId]); + + return dataType as DataType; +}; + +export type DatatypeId = string; diff --git a/src/os/explorer/account.ts b/src/os/explorer/account.ts index 09ba6431..296ba413 100644 --- a/src/os/explorer/account.ts +++ b/src/os/explorer/account.ts @@ -11,13 +11,13 @@ import { EventEmitter } from "eventemitter3"; import { useEffect, useState } from "react"; import { uploadFile } from "./utils"; import { ChangeFn } from "@automerge/automerge/next"; -import { useForceUpdate } from "@/components/utils"; +import { useForceUpdate } from "@/os/hooks/useForceUpdate"; import { FolderDoc } from "@/datatypes/folder"; import { useFolderDocWithChildren } from "../../datatypes/folder/hooks/useFolderDocWithChildren"; import { DatatypeId } from "../datatypes"; -export type DatatypeSettingsDoc = { +export type ModuleSettingsDoc = { enabledDatatypeIds: { [id: DatatypeId]: boolean }; }; @@ -25,7 +25,7 @@ export interface AccountDoc { contactUrl: AutomergeUrl; rootFolderUrl: AutomergeUrl; uiStateUrl: AutomergeUrl; - datatypeSettingsUrl: AutomergeUrl; + moduleSettingsUrl: AutomergeUrl; } export type UIStateDoc = { @@ -268,13 +268,13 @@ export function useCurrentAccount(): Account | undefined { }); } - if (doc && doc.datatypeSettingsUrl === undefined) { - const datatypeSettingsHandle = repo.create(); - datatypeSettingsHandle.change((settings) => { + if (doc && doc.moduleSettingsUrl === undefined) { + const moduleSettingsHandle = repo.create(); + moduleSettingsHandle.change((settings) => { settings.enabledDatatypeIds = {}; }); account.handle.change((account) => { - account.datatypeSettingsUrl = datatypeSettingsHandle.url; + account.moduleSettingsUrl = moduleSettingsHandle.url; }); } }, [account?.handle.docSync()]); @@ -315,10 +315,10 @@ export function useSelf(): ContactDoc { return contactDoc; } -export const useDatatypeSettings = (): DatatypeSettingsDoc => { +export const useDatatypeSettings = (): ModuleSettingsDoc => { const [accountDoc] = useCurrentAccountDoc(); - const [datatypeSettingsDoc] = useDocument( - accountDoc?.datatypeSettingsUrl + const [datatypeSettingsDoc] = useDocument( + accountDoc?.moduleSettingsUrl ); return datatypeSettingsDoc; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 74018f8c..1871a8a2 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -5,7 +5,7 @@ import { useSelf, automergeUrlToAccountToken, accountTokenToAutomergeUrl, - DatatypeSettingsDoc, + ModuleSettingsDoc, } from "../account"; import { ChangeEvent, useEffect, useState } from "react"; @@ -23,7 +23,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import { Copy, Eye, EyeOff } from "lucide-react"; +import { Copy, Eye, EyeOff, PlusIcon, XIcon } from "lucide-react"; import { Label } from "@/components/ui/label"; import { @@ -33,7 +33,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { ContactAvatar } from "./ContactAvatar"; -import { DATA_TYPES } from "@/os/datatypes"; +import { useDataTypeModules } from "@/os/datatypes"; import { Checkbox } from "@/components/ui/checkbox"; // 1MB in bytes @@ -56,6 +56,7 @@ export const AccountPicker = ({ const currentAccount = useCurrentAccount(); const self = useSelf(); + const datatypeModules = useDataTypeModules(); const [name, setName] = useState(""); const [avatar, setAvatar] = useState(); const [activeTab, setActiveTab] = useState( @@ -74,8 +75,8 @@ export const AccountPicker = ({ ); const [accountToLogin] = useDocument(accountAutomergeUrlToLogin); const [contactToLogin] = useDocument(accountToLogin?.contactUrl); - const [datatypeSettingsDoc, changeDatatypeSettingsDoc] = - useDocument(currentAccountDoc?.datatypeSettingsUrl); + const [moduleSettingsDoc, changeModuleSettingsDoc] = + useDocument(currentAccountDoc?.moduleSettingsUrl); const accountTokenToLoginStatus: AccountTokenToLoginStatus = (() => { if (!accountTokenToLogin || accountTokenToLogin === "") return null; @@ -145,10 +146,6 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; - const experimentalDatatypes = Object.values(DATA_TYPES).filter( - ({ isExperimental }) => isExperimental - ); - return ( @@ -322,38 +319,48 @@ export const AccountPicker = ({
- +
- {datatypeSettingsDoc && - experimentalDatatypes.map((datatype) => { + {moduleSettingsDoc && + Object.values(datatypeModules).map((datatypeModule) => { + const isEnabled = + moduleSettingsDoc.enabledDatatypeIds[ + datatypeModule.metadata.id + ]; + + const isChecked = + isEnabled || + (isEnabled === undefined && + !datatypeModule.metadata.isExperimental); + return (
e.stopPropagation()} onCheckedChange={() => { - changeDatatypeSettingsDoc((settings) => { - settings.enabledDatatypeIds[datatype.id] = - !settings.enabledDatatypeIds[datatype.id]; + changeModuleSettingsDoc((settings) => { + settings.enabledDatatypeIds[ + datatypeModule.metadata.id + ] = !isChecked; }); }} />
); diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index 0f36c83b..b7243d02 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -12,7 +12,7 @@ import { useCurrentAccountDoc, useRootFolderDocWithChildren, } from "../account"; -import { DatatypeId, DATA_TYPES } from "@/os/datatypes"; +import { DatatypeId, useDataTypeModules } from "@/os/datatypes"; import { Toaster } from "@/components/ui/sonner"; import { LoadingScreen } from "./LoadingScreen"; @@ -26,9 +26,12 @@ import { DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; import { useSelectedDocLink } from "../hooks/useSelectedDocLink"; import { useSyncDocTitle } from "../hooks/useSyncDocTitle"; import { ErrorFallback } from "./ErrorFallback"; +import { Module, useModule } from "@/os/modules"; +import { ToolMetaData, Tool, useToolModulesForDataType } from "@/os/tools"; export const Explorer: React.FC = () => { const repo = useRepo(); + const datatypeModules = useDataTypeModules(); const currentAccount = useCurrentAccount(); const [accountDoc] = useCurrentAccountDoc(); @@ -49,6 +52,7 @@ export const Explorer: React.FC = () => { useDocument>(selectedDocUrl); const selectedDocName = selectedDocLink?.name; + const selectedDataType = selectedDocLink?.type; const selectedBranchUrl = selectedDocLink?.branchUrl; const selectedBranch = useMemo(() => { @@ -61,15 +65,38 @@ export const Explorer: React.FC = () => { ); }, [selectedBranchUrl, selectedDoc]); + const [selectedToolModuleId, setSelectedToolModuleId] = useState(); + + const toolModules = useToolModulesForDataType(selectedDataType); + const selectedToolModule = toolModules.find( + (module) => module.metadata.id === selectedToolModuleId + ); + + const currentToolModule = + // make sure the current tool is reset to the fallback tool + // if the selected datatype changes and the selected tool is not compatible + selectedToolModule && + selectedToolModule.metadata.supportedDatatypes.some( + (supportedDataType) => + supportedDataType === selectedDataType || supportedDataType === "*" + ) + ? selectedToolModule + : toolModules[0]; + + const currentTool = useModule(currentToolModule); + const addNewDocument = useCallback( - ({ type }: { type: DatatypeId }) => { - if (!DATA_TYPES[type]) { + async ({ type }: { type: DatatypeId }) => { + const datatypeModule = datatypeModules[type]; + + if (!datatypeModule) { throw new Error(`Unsupported document type: ${type}`); } + const datatype = await datatypeModule.load(); const newDocHandle = repo.create>(); - newDocHandle.change((doc) => DATA_TYPES[type].init(doc, repo)); + newDocHandle.change((doc) => datatype.init(doc, repo)); let parentFolderUrl: AutomergeUrl; let folderPath: AutomergeUrl[]; @@ -128,7 +155,7 @@ export const Explorer: React.FC = () => { // if there's no document selected and the user hits enter, make a new document if (!selectedDocUrl && event.key === "Enter") { - addNewDocument({ type: "essay" }); + addNewDocument({ type: "essay" as DatatypeId }); } }; @@ -213,6 +240,9 @@ export const Explorer: React.FC = () => { selectedDocHandle={selectedDocHandle} removeDocLink={removeDocLink} addNewDocument={addNewDocument} + toolModuleId={currentToolModule?.metadata.id} + setToolModuleId={setSelectedToolModuleId} + toolModules={toolModules} />
{!selectedDocUrl && ( @@ -222,7 +252,9 @@ export const Explorer: React.FC = () => { No document selected

- {Object.entries(DATA_TYPES).map(([id, datatype]) => { + {Object.values(datatypeModules).map((datatypeModule) => { + const { id } = datatypeModule.metadata; + const isEnabled = datatypeSettings?.enabledDatatypeIds[id]; if ( - datatype.isExperimental && - !datatypeSettings?.enabledDatatypeIds[id] + isEnabled == false || + (isEnabled !== true && datatypeModule.metadata.isExperimental) ) { return; } return ( -
+
{" "}
addNewDocument({ type: id as DatatypeId })} > - - New {datatype.name} + New {datatypeModule.metadata.name}
); diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index 4a1a3d5c..2a2ddd7a 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -1,22 +1,22 @@ -import { DocHandle, isValidAutomergeUrl, Doc } from "@automerge/automerge-repo"; -import React, { useCallback } from "react"; +import { DocLink, DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; +import { Doc, DocHandle, isValidAutomergeUrl } from "@automerge/automerge-repo"; +import { useRepo } from "@automerge/automerge-repo-react-hooks"; import { Bot, + BotIcon, Download, EditIcon, GitForkIcon, Menu, MoreHorizontal, - SaveIcon, ShareIcon, Trash2Icon, } from "lucide-react"; +import React, { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { useRepo } from "@automerge/automerge-repo-react-hooks"; -import { SyncIndicator } from "./SyncIndicator"; -import { AccountPicker } from "./AccountPicker"; import { saveFile } from "../utils"; -import { DocLink, DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; +import { AccountPicker } from "./AccountPicker"; +import { SyncIndicator } from "./SyncIndicator"; import { DropdownMenu, @@ -26,16 +26,18 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { getHeads, save } from "@automerge/automerge"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; -import { DatatypeId, DATA_TYPES } from "../../datatypes"; -import { runBot } from "@/datatypes/bot/essayEditingBot"; import { Button } from "@/components/ui/button"; +import { runBot } from "@/datatypes/bot/essayEditingBot"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; +import { FileExportMethod, genericExportMethods } from "@/os/fileExports"; import { HasVersionControlMetadata } from "@/os/versionControl/schema"; +import { getHeads } from "@automerge/automerge"; +import { DatatypeId, useDataTypeModules } from "../../datatypes"; import { useDatatypeSettings, useRootFolderDocWithChildren } from "../account"; -import botDataType from "@/datatypes/bot"; import { getUrlSafeName } from "../hooks/useSelectedDocLink"; -import { genericExportMethods } from "@/os/fileExports"; +import { Module, useModule } from "@/os/modules"; +import { Tool, ToolMetaData, useToolModules } from "@/os/tools"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; type TopbarProps = { showSidebar: boolean; @@ -48,6 +50,9 @@ type TopbarProps = { | undefined; addNewDocument: (doc: { type: DatatypeId }) => void; removeDocLink: (link: DocLinkWithFolderPath) => void; + toolModules: Module[]; + toolModuleId: string; + setToolModuleId: (id: string) => void; }; export const Topbar: React.FC = ({ @@ -57,33 +62,41 @@ export const Topbar: React.FC = ({ selectedDocLink, selectedDoc, selectedDocHandle, + toolModules, + toolModuleId, + setToolModuleId, removeDocLink, }) => { const repo = useRepo(); + const { flatDocLinks } = useRootFolderDocWithChildren(); const datatypeSettings = useDatatypeSettings(); - const isBotDatatypeEnabled = - datatypeSettings?.enabledDatatypeIds[botDataType.id]; + const isBotDatatypeEnabled = datatypeSettings?.enabledDatatypeIds.bot; const selectedDocUrl = selectedDocLink?.url; const selectedDocName = selectedDocLink?.name; - const selectedDocType = selectedDocLink?.type; + const selectedDataType = selectedDocLink?.type; + const selectedDataTypeRef = useRef(); + selectedDataTypeRef.current = selectedDataType; - const selectedDatatypeMetadata = DATA_TYPES[selectedDocType]; + const dataTypeModules = useDataTypeModules(); + const selectedDataTypeModule = dataTypeModules[selectedDataType]; - const downloadAsAutomerge = useCallback(() => { - const file = new Blob([save(selectedDoc)], { - type: "application/octet-stream", - }); - saveFile(file, `${selectedDocUrl}.automerge`, [ - { - accept: { - "application/octet-stream": [".automerge"], - }, - }, - ]); - }, [selectedDocUrl, selectedDoc]); + const [fileExportMethods, setFileExportMethods] = useState< + FileExportMethod[] + >([]); + useEffect(() => { + if (!selectedDataTypeModule) { + setFileExportMethods([]); + } else { + selectedDataTypeModule.load().then((datatype) => { + if (datatype.id === selectedDataType) { + setFileExportMethods(datatype.fileExportMethods ?? []); + } + }); + } + }, [selectedDataTypeModule]); const botDocLinks = flatDocLinks?.filter((doc) => doc.type === "bot") ?? []; @@ -98,9 +111,11 @@ export const Topbar: React.FC = ({
)}
- {selectedDatatypeMetadata && ( - - )} + {selectedDataTypeModule && + React.createElement(selectedDataTypeModule.metadata.icon, { + className: "inline mr-1", + size: 14, + })} {selectedDocName}
@@ -109,82 +124,27 @@ export const Topbar: React.FC = ({ )}
- {/* todo: the logic for running bots and when to show the menu should - probably live inside the bots directory -- - how do datatypes contribute things to the global topbar? */} - {selectedDocLink?.type === "essay" && ( -
- {isBotDatatypeEnabled && ( - - - - - - {botDocLinks.length === 0 && ( -
-
- No bots in sidebar.
- Click "New Bot" or get a share link from someone. -
-
- )} - {botDocLinks.map((botDocLink) => ( - { - const resultPromise = runBot({ - botDocUrl: botDocLink.url, - targetDocHandle: - selectedDocHandle as DocHandle, - repo, - }); - toast.promise(resultPromise, { - loading: `Running ${botDocLink.name}...`, - success: (result) => ( -
-
-
- {botDocLink.name} ran successfully. -
- -
-
- ), - error: `${botDocLink.name} failed, see console`, - }); - }} - > - Run {botDocLink.name} - { - selectDocLink({ ...botDocLink, type: "essay" }); - e.stopPropagation(); - }} - /> -
- ))} -
-
- )} -
+ {toolModules.length > 1 && selectedDocLink && ( + + + {toolModules.map((module) => ( + + {module.metadata.name} + + ))} + + )} -
+ +
= ({ { + const selectedDataType = await selectedDataTypeModule.load(); + const newHandle = repo.clone>( selectedDocHandle ); newHandle.change((doc: any) => { - DATA_TYPES[selectedDocType].markCopy(doc); + selectedDataType.markCopy(doc); doc.branchMetadata.source = { url: selectedDocUrl, branchHeads: getHeads(selectedDocHandle.docSync()), @@ -221,7 +183,7 @@ export const Topbar: React.FC = ({ const newDocLink: DocLink = { url: newHandle.url, - name: await DATA_TYPES[selectedDocType].getTitle( + name: await selectedDataType.getTitle( newHandle.docSync(), repo ), @@ -255,32 +217,99 @@ export const Topbar: React.FC = ({ />{" "} Make a copy - - {(selectedDatatypeMetadata?.fileExportMethods ?? []) - .concat(genericExportMethods) - .map((method) => ( - { - const blob = await method.export(selectedDoc, repo); - const filename = `${getUrlSafeName(selectedDocLink.name)}.${ - method.extension - }`; - saveFile(blob, filename, [ - { - accept: { - [method.contentType]: [`.${method.extension}`], - }, + + {fileExportMethods.concat(genericExportMethods).map((method) => ( + { + const blob = await method.export(selectedDoc, repo); + const filename = `${getUrlSafeName(selectedDocLink.name)}.${ + method.extension + }`; + saveFile(blob, filename, [ + { + accept: { + [method.contentType]: [`.${method.extension}`], }, - ]); - }} - > - {" "} - Export as {method.name} - - ))} + }, + ]); + }} + > + {" "} + Export as {method.name} + + ))} + + {selectedDocLink?.type === "essay" && isBotDatatypeEnabled && ( + <> + {/* todo: the logic for running bots and when to show the menu should + probably live inside the bots directory -- + how do datatypes contribute things to the global topbar? */} + + + {botDocLinks.map((botDocLink) => ( + { + const resultPromise = runBot({ + botDocUrl: botDocLink.url, + targetDocHandle: + selectedDocHandle as DocHandle, + repo, + }); + toast.promise(resultPromise, { + loading: `Running ${botDocLink.name}...`, + success: (result) => ( +
+
+
+ {botDocLink.name} ran successfully. +
+ +
+
+ ), + error: `${botDocLink.name} failed, see console`, + }); + }} + > +
+ {" "} + Run {botDocLink.name} +
+ { + selectDocLink({ + ...botDocLink, + type: "essay" as DatatypeId, + }); + e.stopPropagation(); + }} + /> +
+ ))} + + )} + removeDocLink(selectedDocLink)}> { return urlSafeName; }; -const isDatatypeId = (x: string): x is DatatypeId => - Object.keys(DATA_TYPES).includes(x as DatatypeId); - // Parse older URL formats and map them into our newer formatu const parseLegacyUrl = ( url: URL @@ -80,7 +77,7 @@ const parseLegacyUrl = ( if (isValidAutomergeUrl(possibleAutomergeUrl)) { return { url: possibleAutomergeUrl, - type: "essay", + type: "essay" as DatatypeId, }; } @@ -99,11 +96,6 @@ const parseLegacyUrl = ( return null; } - if (typeof docType === "string" && !isDatatypeId(docType)) { - alert(`Invalid doc type in URL: ${docType}`); - return null; - } - if (typeof branchUrl === "string" && !isValidAutomergeUrl(branchUrl)) { alert(`Invalid branch in URL: ${branchUrl}`); return null; @@ -111,7 +103,7 @@ const parseLegacyUrl = ( return { url: docUrl, - type: docType, + type: docType as DatatypeId, branchUrl: branchUrl as AutomergeUrl, }; }; @@ -133,12 +125,8 @@ const parseUrl = (url: URL): Omit | null => { return null; } - const datatypeId = - url.searchParams.get("type") ?? url.searchParams.get("docType"); // use legacy docType as a fallback - if (!isDatatypeId(datatypeId)) { - alert(`Invalid data type in URL: ${datatypeId}`); - return null; - } + const datatypeId = (url.searchParams.get("type") ?? + url.searchParams.get("docType")) as DatatypeId; // use legacy docType as a fallback const branchUrl = url.searchParams.get("branchUrl"); if (branchUrl && !isValidAutomergeUrl(branchUrl)) { diff --git a/src/os/explorer/hooks/useSyncDocTitle.ts b/src/os/explorer/hooks/useSyncDocTitle.ts index b7cb2049..38b45a31 100644 --- a/src/os/explorer/hooks/useSyncDocTitle.ts +++ b/src/os/explorer/hooks/useSyncDocTitle.ts @@ -1,5 +1,5 @@ import { HasVersionControlMetadata } from "@/os/versionControl/schema"; -import { DATA_TYPES } from "../../datatypes"; +import { useDataTypeModules } from "../../datatypes"; import { DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; import { Doc } from "@automerge/automerge/next"; @@ -26,6 +26,8 @@ export const useSyncDocTitle = ({ const counterRef = useRef(0); const selectedDocTitleRef = useRef<{ url: AutomergeUrl; title?: string }>(); + const dataTypeModule = useDataTypeModules(); + useEffect(() => { if (!selectedDocLink || !selectedDoc) { selectedDocTitleRef.current = null; @@ -40,8 +42,9 @@ export const useSyncDocTitle = ({ let counter = (counterRef.current = counterRef.current + 1); // load title - DATA_TYPES[selectedDocLink.type] - .getTitle(selectedDoc, repo) + dataTypeModule[selectedDocLink.type] + .load() + .then((dataType) => dataType.getTitle(selectedDoc, repo)) .then((title) => { // do nothing if selectedDocLink has changed in between // or if this promise resolved after newer update diff --git a/src/os/hooks/useForceUpdate.ts b/src/os/hooks/useForceUpdate.ts new file mode 100644 index 00000000..25a1a74a --- /dev/null +++ b/src/os/hooks/useForceUpdate.ts @@ -0,0 +1,6 @@ +import { useReducer } from "react"; + +export function useForceUpdate() { + const [, forceUpdate] = useReducer((x) => x + 1, 0); + return forceUpdate; +} diff --git a/src/os/main.tsx b/src/os/main.tsx index 7c976d2d..72b0834e 100644 --- a/src/os/main.tsx +++ b/src/os/main.tsx @@ -17,28 +17,29 @@ import { RepoContext } from "@automerge/automerge-repo-react-hooks"; import { getAccount } from "./explorer/account.js"; import { Explorer } from "./explorer/components/Explorer.js"; import "./index.css"; +import { Button } from "@/components/ui/button.js"; -const serviceWorker = await setupServiceWorker(); +// const serviceWorker = await setupServiceWorker(); // Service workers stop on their own, which breaks sync. // Here we ping the service worker while the tab is running // to keep it alive (and make it restart if it did stop.) -setInterval(() => { +/* setInterval(() => { serviceWorker.postMessage({ type: "PING" }); -}, 5000); +}, 5000); */ // This case should never happen // if the service worker is not defined here either the initialization failed // or we found a new case that we haven't considered yet -if (!serviceWorker) { +/* if (!serviceWorker) { throw new Error("Failed to setup service worker"); -} +} */ const repo = await setupRepo(); -establishMessageChannel(serviceWorker); +// establishMessageChannel(serviceWorker); -async function setupServiceWorker(): Promise { +/* async function setupServiceWorker(): Promise { return navigator.serviceWorker .register("/service-worker.js", { type: "module", @@ -59,7 +60,7 @@ async function setupServiceWorker(): Promise { // otherwise return the active service worker return registration.active; }); -} +} */ async function setupRepo() { // in our vendored version we export a promise that resolves once the wasm is loaded @@ -84,7 +85,7 @@ async function setupRepo() { } // Re-establish the MessageChannel if the controlling service worker changes. -navigator.serviceWorker.addEventListener("controllerchange", (event) => { +/* navigator.serviceWorker.addEventListener("controllerchange", (event) => { const newServiceWorker = (event.target as ServiceWorkerContainer).controller; // controllerchange is fired after a new service worker is installed // even if we wait above in setupServiceWorker() until the service worker state changes to activated. @@ -92,20 +93,20 @@ navigator.serviceWorker.addEventListener("controllerchange", (event) => { if (newServiceWorker !== serviceWorker) { establishMessageChannel(newServiceWorker); } -}); +}); */ // Re-establish the MessageChannel if the service worker restarts -navigator.serviceWorker.addEventListener("message", (event) => { +/* navigator.serviceWorker.addEventListener("message", (event) => { if (event.data.type === "SERVICE_WORKER_RESTARTED") { console.log("Service worker restarted, establishing message channel"); establishMessageChannel(serviceWorker); } -}); +}); */ // Connects the repo in the tab with the repo in the service worker through a message channel. // The repo in the tab takes advantage of loaded state in the SW. // TODO: clean up MessageChannels to old repos -function establishMessageChannel(serviceWorker: ServiceWorker) { +/* function establishMessageChannel(serviceWorker: ServiceWorker) { // Send one side of a MessageChannel to the service worker and register the other with the repo. const messageChannel = new MessageChannel(); repo.networkSubsystem.addNetworkAdapter( @@ -113,7 +114,7 @@ function establishMessageChannel(serviceWorker: ServiceWorker) { ); serviceWorker.postMessage({ type: "INIT_PORT" }, [messageChannel.port2]); console.log("Connected to service worker"); -} +} */ // Setup account diff --git a/src/os/modules.ts b/src/os/modules.ts new file mode 100644 index 00000000..fdde8dc6 --- /dev/null +++ b/src/os/modules.ts @@ -0,0 +1,47 @@ +import { useEffect, useRef, useState } from "react"; + +export class Module { + readonly metadata: M; + + #load: () => Promise; + + constructor({ metadata, load }: { metadata: M; load: () => Promise }) { + this.metadata = metadata; + this.#load = load; + } + + async load(): Promise { + return { + ...(await this.#load()), + ...this.metadata, + }; + } +} + +export const useModule = (module: Module): (D & M) | undefined => { + const [loadedModule, setLoadedModule] = useState(); + const moduleRef = useRef>(); + moduleRef.current = module; + + useEffect(() => { + if (!module) { + setLoadedModule(undefined); + return; + } + + module + .load() + .then((loadedModule) => { + // ignore if module has changed in the meantime + if (module !== moduleRef.current) { + return; + } + setLoadedModule(loadedModule); + }) + .catch((err) => { + console.log(err); + }); + }, [module]); + + return module ? loadedModule : undefined; +}; diff --git a/src/os/tools.ts b/src/os/tools.ts index 0ca40405..1a021a4e 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -1,24 +1,25 @@ import * as A from "@automerge/automerge/next"; -import React from "react"; -import essay from "@/tools/essay"; -import tldraw from "@/tools/tldraw"; -import folder from "@/tools/folder"; -import datagrid from "@/tools/datagrid"; -import bot from "@/tools/bot"; -import kanban from "@/tools/kanban"; - -import { AutomergeUrl } from "@automerge/automerge-repo"; +import React, { useEffect, useMemo, useRef, useState } from "react"; + import { Annotation, + AnnotationWithUIState, HasVersionControlMetadata, } from "@/os/versionControl/schema"; -import { AnnotationWithUIState } from "@/os/versionControl/schema"; -import { DatatypeId } from "./datatypes"; -import { DocHandle } from "@automerge/automerge-repo"; +import { AutomergeUrl, DocHandle } from "@automerge/automerge-repo"; +import { useRootFolderDocWithChildren } from "./explorer/account"; +import { Module } from "./modules"; +import { DocLink } from "@/datatypes/folder"; +import { useRepo } from "@automerge/automerge-repo-react-hooks"; +import { ModuleDoc } from "@/datatypes/module"; -export type Tool = { - id: DatatypeId; +export type ToolMetaData = { + id: string; + supportedDatatypes: string[]; name: string; +}; + +export type Tool = { editorComponent: React.FC>; annotationViewComponent?: React.FC< AnnotationsViewProps< @@ -50,25 +51,70 @@ export type AnnotationsViewProps< annotations: Annotation[]; }; -const getToolsMap = (tools: Tool[]): Record => { - const map = {}; +const TOOLS: Module[] = []; - tools.forEach((tool) => { - if (!map[tool.id]) { - map[tool.id] = [tool]; - } else { - map[tool.id].push(tools); - } +const toolsFolder: Record }> = + import.meta.glob("../tools/*/module.@(ts|js|tsx|jsx)", { + eager: true, }); - return map; +for (const [path, { default: module }] of Object.entries(toolsFolder)) { + const id = path.split("/")[2]; + + if (id !== module.metadata.id) { + throw new Error( + `Can't load tool: id "${module.metadata.id}" does not match the folder name "${id}"` + ); + } + + TOOLS.push(module); +} + +export const useToolModules = () => { + /*const [dynamicModules, setDynamicModules] = useState([]); + const repo = useRepo(); + + const { flatDocLinks } = useRootFolderDocWithChildren(); + + const moduleDocLinks = useMemo( + () => + flatDocLinks ? flatDocLinks.filter((link) => link.type === "module") : [], + [flatDocLinks] + ); + + const moduleDocLinksRef = useRef(); + moduleDocLinksRef.current = moduleDocLinks; + + useEffect(() => { + Promise.all( + moduleDocLinks.map(async ({ url }) => { + const moduleDoc = await repo.find(url).doc(); + const module = await import(moduleDoc.url); + return module.default; + }) + ).then((modules) => { + // skip if moduleDocLinks has changed in the meantime + if (moduleDocLinks !== moduleDocLinksRef.current) { + return; + } + + setDynamicModules(modules); + }); + }, [moduleDocLinks]); */ + + return TOOLS; }; -export const TOOLS = getToolsMap([ - essay, - tldraw, - folder, - datagrid, - bot, - kanban, -]); +export const useToolModulesForDataType = (dataTypeId: string) => { + const toolModules = useToolModules(); + + return useMemo( + () => + toolModules.filter( + (tool) => + tool.metadata.supportedDatatypes.includes(dataTypeId) || + tool.metadata.supportedDatatypes.includes("*") + ), + [toolModules, dataTypeId] + ); +}; diff --git a/src/os/versionControl/annotations.ts b/src/os/versionControl/annotations.ts index a7e1c399..d0f826ec 100644 --- a/src/os/versionControl/annotations.ts +++ b/src/os/versionControl/annotations.ts @@ -1,8 +1,8 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useRef } from "react"; import * as A from "@automerge/automerge/next"; import { isEqual, sortBy, min } from "lodash"; import { useStaticCallback } from "@/os/hooks/useStaticCallback"; -import { DatatypeId, DATA_TYPES } from "@/os/datatypes"; +import { DataType, DatatypeId, useDataTypeModules } from "@/os/datatypes"; import { Annotation, HighlightAnnotation, @@ -33,12 +33,12 @@ type HoverState = HoverAnchorState | ActiveGroupState; export function useAnnotations({ doc, - datatypeId, + dataType, diff, isCommentInputFocused, }: { doc: A.Doc>; - datatypeId: DatatypeId; + dataType: DataType; diff?: DiffWithProvenance; isCommentInputFocused: boolean; }): { @@ -105,12 +105,12 @@ export function useAnnotations({ ); const { annotations, annotationGroups } = useMemo(() => { - if (!doc) { + if (!doc || !dataType) { return { annotations: [], annotationGroups: [] }; } - const patchesToAnnotations = DATA_TYPES[datatypeId].patchesToAnnotations; - const valueOfAnchor = DATA_TYPES[datatypeId].valueOfAnchor ?? (() => null); + const patchesToAnnotations = dataType.patchesToAnnotations; + const valueOfAnchor = dataType.valueOfAnchor ?? (() => null); const discussions = Object.values(doc?.discussions ?? []); const discussionGroups: AnnotationGroup[] = []; @@ -165,7 +165,7 @@ export function useAnnotations({ editAnnotations.forEach((editAnnotation) => { if ( discussion.anchors.some((anchor) => - doAnchorsOverlap(datatypeId, editAnnotation.anchor, anchor, doc) + doAnchorsOverlap(dataType, editAnnotation.anchor, anchor, doc) ) ) { // mark any annotation that is part of a discussion as claimed @@ -184,7 +184,7 @@ export function useAnnotations({ const computedAnnotationGroups: AnnotationGroup[] = groupAnnotations( - datatypeId, + dataType, editAnnotations.filter( (annotation) => !claimedAnnotations.has(annotation) ) @@ -206,7 +206,7 @@ export function useAnnotations({ highlightAnnotations.push(...selectionAnnotations); } - const sortAnchorsBy = DATA_TYPES[datatypeId].sortAnchorsBy; + const sortAnchorsBy = dataType.sortAnchorsBy; return { annotations: editAnnotations.concat(highlightAnnotations), @@ -220,7 +220,7 @@ export function useAnnotations({ ) : combinedAnnotationGroups, }; - }, [doc, diff, selectedState, isCommentInputFocused, datatypeId]); + }, [doc, diff, selectedState, isCommentInputFocused, dataType]); const { selectedAnchors, @@ -242,7 +242,7 @@ export function useAnnotations({ // first annotationGroup that contains all selected anchors is expanded const annotationGroup = annotationGroups.find((group) => doesAnnotationGroupContainAnchors( - datatypeId, + dataType, group, selectedState.anchors, doc @@ -286,7 +286,7 @@ export function useAnnotations({ // find first discussion that contains the hovered anchor and hover all anchors that are part of that discussion as wellp const annotationGroup = annotationGroups.find((group) => doesAnnotationGroupContainAnchors( - datatypeId, + dataType, group, [hoveredState.anchor], doc @@ -377,17 +377,17 @@ export function useAnnotations({ } export const doAnchorsOverlap = ( - type: DatatypeId, + datatype: DataType, a: unknown, b: unknown, doc: HasVersionControlMetadata ) => { - const comperator = DATA_TYPES[type].doAnchorsOverlap; + const comperator = datatype.doAnchorsOverlap; return comperator ? comperator(doc, a, b) : isEqual(a, b); }; export const areAnchorSelectionsEqual = ( - type: DatatypeId, + datatype: DataType, a: unknown[], b: unknown[], doc: HasVersionControlMetadata @@ -397,7 +397,7 @@ export const areAnchorSelectionsEqual = ( } return a.every((anchor) => - b.some((other) => doAnchorsOverlap(type, anchor, other, doc)) + b.some((other) => doAnchorsOverlap(datatype, anchor, other, doc)) ); }; @@ -413,25 +413,25 @@ export function getAnnotationGroupId( return `${firstAnnotation.type}:${JSON.stringify(firstAnnotation.anchor)}`; } -export function doesAnnotationGroupContainAnchors( - datatypeId: DatatypeId, +export function doesAnnotationGroupContainAnchors( + datatype: DataType, group: AnnotationGroup, anchors: T[], doc: HasVersionControlMetadata ) { return anchors.every((anchor) => group.annotations.some((annotation) => - doAnchorsOverlap(datatypeId, annotation.anchor, anchor, doc) + doAnchorsOverlap(datatype, annotation.anchor, anchor, doc) ) ); } -export function groupAnnotations( - datatypeId: DatatypeId, +export function groupAnnotations( + datatype: DataType, annotations: Annotation[] ): Annotation[][] { const grouper = - DATA_TYPES[datatypeId].groupAnnotations ?? + datatype.groupAnnotations ?? ((annotations: Annotation[]) => annotations.map((annotation) => [annotation])); diff --git a/src/os/versionControl/branches.ts b/src/os/versionControl/branches.ts index 514ac85f..0f98883c 100644 --- a/src/os/versionControl/branches.ts +++ b/src/os/versionControl/branches.ts @@ -2,7 +2,7 @@ import * as A from "@automerge/automerge/next"; import { AutomergeUrl, DocHandle, Repo } from "@automerge/automerge-repo"; import { Branch, Branchable } from "./schema"; import { getStringCompletion } from "@/os/lib/llm"; -import { MarkdownDoc } from "@/datatypes/markdown"; +import { MarkdownDoc } from "@/datatypes/essay"; import { Hash } from "@automerge/automerge-wasm"; export const createBranch = ({ diff --git a/src/os/versionControl/components/ReviewSidebar.tsx b/src/os/versionControl/components/ReviewSidebar.tsx index 3c626824..7e33e760 100644 --- a/src/os/versionControl/components/ReviewSidebar.tsx +++ b/src/os/versionControl/components/ReviewSidebar.tsx @@ -6,11 +6,16 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; -import { DATA_TYPES, DatatypeId } from "@/os/datatypes"; +import { DataType, DatatypeId } from "@/os/datatypes"; import { useCurrentAccount } from "@/os/explorer/account"; import { ContactAvatar } from "@/os/explorer/components/ContactAvatar"; import { getRelativeTimeString } from "@/os/lib/dates"; -import { AnnotationsViewProps, TOOLS } from "@/os/tools"; +import { + AnnotationsViewProps, + Tool, + ToolMetaData, + useToolModulesForDataType, +} from "@/os/tools"; import { AnnotationGroup, AnnotationGroupWithState, @@ -23,11 +28,12 @@ import { DocHandle } from "@automerge/automerge-repo"; import { Check, MessageCircleIcon } from "lucide-react"; import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react"; import { getAnnotationGroupId } from "../annotations"; +import { useModule } from "@/os/modules"; type ReviewSidebarProps = { doc: HasVersionControlMetadata; handle: DocHandle>; - datatypeId: DatatypeId; + dataType: DataType; annotationGroups: AnnotationGroupWithState[]; selectedAnchors: unknown[]; changeDoc: ( @@ -47,7 +53,7 @@ export const ReviewSidebar = React.memo( ({ doc, handle, - datatypeId, + dataType, annotationGroups, selectedAnchors, changeDoc, @@ -67,14 +73,13 @@ export const ReviewSidebar = React.memo( unknown >[] = useMemo(() => { if (!doc) return []; - const valueOfAnchor = - DATA_TYPES[datatypeId].valueOfAnchor ?? (() => null); + const valueOfAnchor = dataType.valueOfAnchor ?? (() => null); return selectedAnchors.map((anchor) => ({ type: "highlighted", anchor: [anchor], value: valueOfAnchor(doc, anchor), })); - }, [selectedAnchors, doc, datatypeId]); + }, [selectedAnchors, doc, dataType]); const addCommentToAnnotationGroup = ( annotationGroup: AnnotationGroup, @@ -179,7 +184,7 @@ export const ReviewSidebar = React.memo(
@@ -565,11 +570,18 @@ const AnnotationsView = ({ unknown, unknown >) => { + const tools = useToolModulesForDataType(datatypeId); + const tool = useModule(tools[0]); + + if (!tool) { + return; + } + // For now, we just use the first annotation viewer available for this doc type. // In the future, we might want to: // - use an annotations view that's similar to the viewer being used for the main doc // - allow switching between different viewers? - const Viewer = TOOLS[datatypeId]?.[0].annotationViewComponent; + const Viewer = tool.annotationViewComponent; if (!Viewer) { return (
diff --git a/src/os/versionControl/components/TimelineSidebar.tsx b/src/os/versionControl/components/TimelineSidebar.tsx index 72b0c5d8..b043bc7c 100644 --- a/src/os/versionControl/components/TimelineSidebar.tsx +++ b/src/os/versionControl/components/TimelineSidebar.tsx @@ -11,7 +11,6 @@ import { GenericChangeGroup, groupingByEditTime, } from "../groupChanges"; -import { DATA_TYPES } from "@/os/datatypes"; import { MilestoneIcon, @@ -49,7 +48,7 @@ import { useAutoPopulateChangeGroupSummaries, } from "@/os/versionControl/changeGroupSummaries"; -import { DatatypeId } from "@/os/datatypes"; +import { DataType } from "@/os/datatypes"; import { ChangeGroupingOptions } from "../groupChanges"; import { ChangeGrouper } from "../ChangeGrouper"; @@ -110,14 +109,14 @@ export type ChangelogSelection = | undefined; export const TimelineSidebar: React.FC<{ - datatypeId: DatatypeId; + dataType: DataType; docUrl: AutomergeUrl; selectedBranch: Branch; setSelectedBranch: (branch: Branch) => void; setDocHeads: (heads: Heads) => void; setDiff: (diff: DiffWithProvenance) => void; }> = ({ - datatypeId, + dataType, docUrl, selectedBranch, setSelectedBranch, @@ -138,7 +137,7 @@ export const TimelineSidebar: React.FC<{ includePatchInChangeGroup, promptForAIChangeGroupSummary: promptForAutoChangeGroupDescription, fallbackSummaryForChangeGroup, - } = DATA_TYPES[datatypeId] ?? {}; + } = dataType ?? {}; // todo: extract this as an interface that different doc types can implement const changeGroupingOptions = useMemo< @@ -154,7 +153,7 @@ export const TimelineSidebar: React.FC<{ fallbackSummaryForChangeGroup, }; }, [ - datatypeId, + dataType.id, includeChangeInHistory, includePatchInChangeGroup, fallbackSummaryForChangeGroup, diff --git a/src/os/versionControl/components/VersionControlEditor.tsx b/src/os/versionControl/components/VersionControlEditor.tsx index 36a2aa5a..aee47a35 100644 --- a/src/os/versionControl/components/VersionControlEditor.tsx +++ b/src/os/versionControl/components/VersionControlEditor.tsx @@ -23,11 +23,16 @@ import { import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "@/os/explorer/components/ErrorFallback"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { isMarkdownDoc } from "@/datatypes/markdown"; -import { DatatypeId } from "@/os/datatypes"; +import { isMarkdownDoc } from "@/datatypes/essay"; +import { DatatypeId, useDataType } from "@/os/datatypes"; import { getRelativeTimeString } from "@/os/lib/dates"; import { isLLMActive } from "@/os/lib/llm"; -import { EditorProps, TOOLS } from "@/os/tools"; +import { + EditorProps, + Tool, + ToolMetaData, + useToolModulesForDataType, +} from "@/os/tools"; import { SideBySide as TLDrawSideBySide } from "@/tools/tldraw/components/TLDraw"; import { AutomergeUrl } from "@automerge/automerge-repo"; import { @@ -86,11 +91,13 @@ type SidebarMode = "comments" | "timeline"; export const VersionControlEditor: React.FC<{ docUrl: AutomergeUrl; datatypeId: DatatypeId; + tool: Tool; selectedBranch: Branch; setSelectedBranch: (branch: Branch) => void; }> = ({ docUrl: mainDocUrl, datatypeId, + tool, selectedBranch, setSelectedBranch, }) => { @@ -101,9 +108,7 @@ export const VersionControlEditor: React.FC<{ useHandle>(mainDocUrl); const account = useCurrentAccount(); const [sessionStartHeads, setSessionStartHeads] = useState(); - const [isCommentInputFocused, setIsCommentInputFocused] = useState(false); - const [isHoveringYankToBranchOption, setIsHoveringYankToBranchOption] = useState(false); const [showChangesFlag, setShowChangesFlag] = useState(true); @@ -303,6 +308,8 @@ export const VersionControlEditor: React.FC<{ const activeChangeDoc = selectedBranch ? changeBranchDoc : changeDoc; const activeHandle = selectedBranch ? branchHandle : handle; + const dataType = useDataType(datatypeId); + const { annotations, annotationGroups, @@ -314,7 +321,7 @@ export const VersionControlEditor: React.FC<{ setSelectedAnnotationGroupId, } = useAnnotations({ doc: activeDoc, - datatypeId, + dataType, diff: diffForEditor, isCommentInputFocused, }); @@ -544,6 +551,7 @@ export const VersionControlEditor: React.FC<{ key={mainDocUrl} mainDocUrl={mainDocUrl} datatypeId={datatypeId} + tool={tool} docUrl={selectedBranch.url} docHeads={docHeads} annotations={annotations} @@ -555,6 +563,7 @@ export const VersionControlEditor: React.FC<{ extends EditorProps { datatypeId: DatatypeId; + tool: Tool; } /* Wrapper component that dispatches to the tool for the doc type */ const DocEditor = ({ - datatypeId, + tool, docUrl, docHeads, annotations, @@ -650,8 +660,11 @@ const DocEditor = ({ setSelectedAnchors, setHoveredAnchor, }: EditorPropsWithDatatype) => { - // Currently we don't have a toolpicker so we just show the first tool for the doc type - const Component = TOOLS[datatypeId][0].editorComponent; + if (!tool) { + return; + } + + const Component = tool.editorComponent; return ( ({ + metadata: { + id: "bot", + name: "Bot", + supportedDatatypes: ["bot"], + }, + + load: () => import("./tool").then(({ BotTool }) => BotTool), +}); diff --git a/src/tools/bot/index.ts b/src/tools/bot/tool.ts similarity index 65% rename from src/tools/bot/index.ts rename to src/tools/bot/tool.ts index dc52028e..48ae076d 100644 --- a/src/tools/bot/index.ts +++ b/src/tools/bot/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { BotEditor } from "./BotEditor"; -export default { - id: "bot", - name: "Bot", +export const BotTool: Tool = { editorComponent: BotEditor, -} as Tool; +}; diff --git a/src/tools/datagrid/module.ts b/src/tools/datagrid/module.ts new file mode 100644 index 00000000..56eb0451 --- /dev/null +++ b/src/tools/datagrid/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + name: "Spreadsheet", + id: "datagrid", + supportedDatatypes: ["datagrid"], + }, + + load: () => import("./tool").then(({ SpreadsheetTool }) => SpreadsheetTool), +}); diff --git a/src/tools/datagrid/index.ts b/src/tools/datagrid/tool.ts similarity index 60% rename from src/tools/datagrid/index.ts rename to src/tools/datagrid/tool.ts index 81a1cd34..09ae193a 100644 --- a/src/tools/datagrid/index.ts +++ b/src/tools/datagrid/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { DataGrid } from "./DataGrid"; -export default { - id: "datagrid", - name: "Spreadsheet", +export const SpreadsheetTool: Tool = { editorComponent: DataGrid, -} as Tool; +}; diff --git a/src/tools/essay/codemirrorPlugins/annotationDecorations.ts b/src/tools/essay/codemirrorPlugins/annotationDecorations.ts index 79436a18..88968e58 100644 --- a/src/tools/essay/codemirrorPlugins/annotationDecorations.ts +++ b/src/tools/essay/codemirrorPlugins/annotationDecorations.ts @@ -2,7 +2,7 @@ import { Decoration, EditorView, WidgetType } from "@codemirror/view"; import { StateEffect, StateField } from "@codemirror/state"; import { AnnotationWithUIState } from "@/os/versionControl/schema"; -import { ResolvedMarkdownDocAnchor } from "../../../datatypes/markdown/schema"; +import { ResolvedMarkdownDocAnchor } from "../../../datatypes/essay/schema"; export const setAnnotationsEffect = StateEffect.define< diff --git a/src/tools/essay/codemirrorPlugins/previewMarkdownImages.ts b/src/tools/essay/codemirrorPlugins/previewMarkdownImages.ts index dee77b88..131da54d 100644 --- a/src/tools/essay/codemirrorPlugins/previewMarkdownImages.ts +++ b/src/tools/essay/codemirrorPlugins/previewMarkdownImages.ts @@ -15,7 +15,7 @@ import { DocumentId, Repo, } from "@automerge/automerge-repo"; -import { MarkdownDoc } from "../../../datatypes/markdown/schema"; +import { MarkdownDoc } from "../../../datatypes/essay/schema"; import { AssetsDoc } from "../assets"; import * as A from "@automerge/automerge"; diff --git a/src/tools/essay/codemirrorPlugins/tableOfContentsPreview.tsx b/src/tools/essay/codemirrorPlugins/tableOfContentsPreview.tsx index 3230f274..45085faa 100644 --- a/src/tools/essay/codemirrorPlugins/tableOfContentsPreview.tsx +++ b/src/tools/essay/codemirrorPlugins/tableOfContentsPreview.tsx @@ -13,7 +13,7 @@ import { } from "@codemirror/view"; import { isEqual } from "lodash"; import { Tree } from "@lezer/common"; -import { jsxToHtmlElement } from "../../../datatypes/markdown/utils"; +import { jsxToHtmlElement } from "../../../datatypes/essay/utils"; type Heading = { level: number; content: string; from: number; to: number }; diff --git a/src/tools/essay/components/CodeMirrorEditor.tsx b/src/tools/essay/components/CodeMirrorEditor.tsx index b7aa9581..d0ffb363 100644 --- a/src/tools/essay/components/CodeMirrorEditor.tsx +++ b/src/tools/essay/components/CodeMirrorEditor.tsx @@ -42,7 +42,7 @@ import { MarkdownDoc, MarkdownDocAnchor, ResolvedMarkdownDocAnchor, -} from "../../../datatypes/markdown/schema"; +} from "../../../datatypes/essay/schema"; import { DebugHighlight, diff --git a/src/tools/essay/components/EssayAnnotations.tsx b/src/tools/essay/components/EssayAnnotations.tsx index f8e65a61..81adb5b9 100644 --- a/src/tools/essay/components/EssayAnnotations.tsx +++ b/src/tools/essay/components/EssayAnnotations.tsx @@ -1,7 +1,7 @@ import { MarkdownDoc, MarkdownDocAnchor, -} from "../../../datatypes/markdown/schema"; +} from "../../../datatypes/essay/schema"; import { truncate } from "lodash"; import { AnnotationsViewProps } from "@/os/tools"; diff --git a/src/tools/essay/components/EssayEditor.tsx b/src/tools/essay/components/EssayEditor.tsx index ea5d9a78..697b648a 100644 --- a/src/tools/essay/components/EssayEditor.tsx +++ b/src/tools/essay/components/EssayEditor.tsx @@ -8,7 +8,7 @@ import { MarkdownDoc, MarkdownDocAnchor, ResolvedMarkdownDocAnchor, -} from "@/datatypes/markdown"; +} from "@/datatypes/essay"; import { EditorView } from "@codemirror/view"; diff --git a/src/tools/essay/module.ts b/src/tools/essay/module.ts new file mode 100644 index 00000000..6ea8ca63 --- /dev/null +++ b/src/tools/essay/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "essay", + name: "Editor", + supportedDatatypes: ["essay"], + }, + + load: () => import("./tool").then(({ EssayEditorTool }) => EssayEditorTool), +}); diff --git a/src/tools/essay/index.ts b/src/tools/essay/tool.ts similarity index 79% rename from src/tools/essay/index.ts rename to src/tools/essay/tool.ts index b0918c2d..4e44a131 100644 --- a/src/tools/essay/index.ts +++ b/src/tools/essay/tool.ts @@ -2,9 +2,7 @@ import { Tool } from "@/os/tools"; import { EssayEditor } from "./components/EssayEditor"; import { EssayAnnotations } from "./components/EssayAnnotations"; -export default { - id: "essay", - name: "Editor", +export const EssayEditorTool: Tool = { editorComponent: EssayEditor, annotationViewComponent: EssayAnnotations, -} as Tool; +}; diff --git a/src/tools/folder/FolderViewer.tsx b/src/tools/folder/FolderViewer.tsx deleted file mode 100644 index 717f63bc..00000000 --- a/src/tools/folder/FolderViewer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import * as A from "@automerge/automerge/next"; -import React from "react"; - -import { EditorProps } from "@/os/tools"; -import { FolderDoc } from "@/datatypes/folder"; -import { TOOLS } from "@/os/tools"; -import { selectDocLink } from "@/os/explorer/hooks/useSelectedDocLink"; -import { DATA_TYPES } from "@/os/datatypes"; - -export const FolderViewer: React.FC> = ({ - docUrl, - docHeads, -}: EditorProps) => { - const [folder] = useDocument(docUrl); // used to trigger re-rendering when the doc loads - - const folderAtHeads = docHeads ? A.view(folder, docHeads) : folder; - - if (!folder) { - return null; - } - - return ( -
-
- {folderAtHeads.docs.length} documents -
-
- {folderAtHeads.docs.map((docLink) => { - const Tool = TOOLS[docLink.type][0].editorComponent; - const Icon = DATA_TYPES[docLink.type].icon; - - return ( -
-
- -
{docLink.name}
- -
-
- {!Tool &&
No editor available
} - {Tool && docLink.type !== "folder" && ( - - )} - {docLink.type === "folder" && ( -
- Click "open" to see nested folder contents -
- )} -
-
- ); - })} -
-
- ); -}; diff --git a/src/tools/folder/index.ts b/src/tools/folder/index.ts deleted file mode 100644 index 2da8ed12..00000000 --- a/src/tools/folder/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Tool } from "@/os/tools"; -import { FolderViewer } from "./FolderViewer"; - -export default { - id: "folder", - name: "Folder", - editorComponent: FolderViewer, -} as Tool; diff --git a/src/tools/kanban/KanbanBoard.tsx b/src/tools/kanban/KanbanBoard.tsx index 344498a1..31ee5dcb 100644 --- a/src/tools/kanban/KanbanBoard.tsx +++ b/src/tools/kanban/KanbanBoard.tsx @@ -1,10 +1,6 @@ import * as A from "@automerge/automerge/next"; -import { - KanbanBoardDatatype, - KanbanBoardDoc, - KanbanBoardDocAnchor, -} from "../../datatypes/kanban/datatype"; +import { KanbanBoardDoc, KanbanBoardDocAnchor } from "@/datatypes/kanban"; import { EditorProps } from "@/os/tools"; import { useDocumentWithActions } from "@/datatypes/kanban/useDocumentWithActions"; @@ -18,7 +14,7 @@ export const KanbanBoard = ({ annotations = [], }: EditorProps & { readOnly?: boolean }) => { const [latestDoc, _changeDoc, actions] = - useDocumentWithActions(docUrl, KanbanBoardDatatype); // used to trigger re-rendering when the doc loads + useDocumentWithActions(docUrl, "kanban"); // used to trigger re-rendering when the doc loads const doc = useMemo( () => (docHeads ? A.view(latestDoc, docHeads) : latestDoc), diff --git a/src/tools/kanban/module.ts b/src/tools/kanban/module.ts new file mode 100644 index 00000000..721dbda5 --- /dev/null +++ b/src/tools/kanban/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "kanban", + name: "Kanban", + supportedDatatypes: ["kanban"], + }, + + load: () => import("./tool").then(({ KanbanTool }) => KanbanTool), +}); diff --git a/src/tools/kanban/index.ts b/src/tools/kanban/tool.ts similarity index 64% rename from src/tools/kanban/index.ts rename to src/tools/kanban/tool.ts index 310af2ad..086c14e9 100644 --- a/src/tools/kanban/index.ts +++ b/src/tools/kanban/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { KanbanBoard } from "./KanbanBoard"; -export default { - id: "kanban", - name: "Kanban", +export const KanbanTool: Tool = { editorComponent: KanbanBoard, -} as Tool; +}; diff --git a/src/tools/module/ModuleEditor.tsx b/src/tools/module/ModuleEditor.tsx new file mode 100644 index 00000000..f71bfdc8 --- /dev/null +++ b/src/tools/module/ModuleEditor.tsx @@ -0,0 +1,35 @@ +import { useDocument } from "@automerge/automerge-repo-react-hooks"; +import * as A from "@automerge/automerge/next"; +import React from "react"; + +import { ModuleDoc } from "@/datatypes/module"; +import { EditorProps } from "@/os/tools"; +import { Input } from "@/components/ui/input"; + +export const ModuleEditor: React.FC> = ({ + docUrl, + docHeads, +}: EditorProps) => { + const [moduleDoc, changeModuleDoc] = useDocument(docUrl); + + const moduleAtHeads = docHeads ? A.view(moduleDoc, docHeads) : moduleDoc; + + if (!moduleDoc) { + return null; + } + + const onChangeUrlInput = (evt) => { + changeModuleDoc((doc) => { + doc.url = evt.target.value; + }); + }; + + return ( +
+
+
URL
+ +
+
+ ); +}; diff --git a/src/tools/module/module.ts b/src/tools/module/module.ts new file mode 100644 index 00000000..1e465890 --- /dev/null +++ b/src/tools/module/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "module", + name: "Module", + supportedDatatypes: ["module"], + }, + + load: () => import("./tool").then(({ FolderViewerTool }) => FolderViewerTool), +}); diff --git a/src/tools/module/tool.ts b/src/tools/module/tool.ts new file mode 100644 index 00000000..c094b3eb --- /dev/null +++ b/src/tools/module/tool.ts @@ -0,0 +1,6 @@ +import { Tool } from "@/os/tools"; +import { ModuleEditor } from "./ModuleEditor"; + +export const FolderViewerTool: Tool = { + editorComponent: ModuleEditor, +}; diff --git a/src/tools/tldraw/module.ts b/src/tools/tldraw/module.ts new file mode 100644 index 00000000..3dfaa757 --- /dev/null +++ b/src/tools/tldraw/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "tldraw", + name: "Drawing", + supportedDatatypes: ["tldraw"], + }, + + load: () => import("./tool").then(({ DrawingTool }) => DrawingTool), +}); diff --git a/src/tools/tldraw/index.ts b/src/tools/tldraw/tool.ts similarity index 77% rename from src/tools/tldraw/index.ts rename to src/tools/tldraw/tool.ts index 15b1fe39..df1ff2be 100644 --- a/src/tools/tldraw/index.ts +++ b/src/tools/tldraw/tool.ts @@ -2,9 +2,7 @@ import { Tool } from "@/os/tools"; import { TLDraw } from "./components/TLDraw"; import { TLDrawAnnotations } from "./components/TLDrawAnnotations"; -export default { - id: "tldraw", - name: "Drawing", +export const DrawingTool: Tool = { editorComponent: TLDraw, annotationViewComponent: TLDrawAnnotations, -} as Tool; +}; diff --git a/vite.config.ts b/vite.config.ts index fcb35fc6..b2c49f99 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,75 @@ // vite.config.ts -import { defineConfig } from "vite"; -import path from "path"; import react from "@vitejs/plugin-react"; -import wasm from "vite-plugin-wasm"; +import * as cheerio from "cheerio"; +import fs from "fs"; +import { globSync } from "glob"; +import { fileURLToPath } from "node:url"; +import path from "path"; +import { ViteDevServer, defineConfig } from "vite"; import topLevelAwait from "vite-plugin-top-level-await"; +import wasm from "vite-plugin-wasm"; + +const SHARED_DEPENDENCIES = [ + "@automerge/automerge", + "@automerge/automerge-repo", + "@automerge/automerge-repo-react-hooks", + "react", +]; export default defineConfig({ base: "./", - plugins: [topLevelAwait(), react()], + plugins: [ + topLevelAwait(), + react(), + { + name: "index transform", + configureServer(server: ViteDevServer) { + const originalTransformIndexHtml = server.transformIndexHtml; + + server.transformIndexHtml = async (url, html, originalUrl) => { + const transformed = await originalTransformIndexHtml.call( + server, + url, + html, + originalUrl + ); + + const $ = cheerio.load(transformed); + + const metadata = JSON.parse( + fs.readFileSync( + path.join(__dirname, "node_modules/.vite/deps/_metadata.json"), + "utf-8" + ) + ); + + const imports = {}; + for (const dep of SHARED_DEPENDENCIES) { + const m = metadata.optimized[dep]; + + if (!m) { + console.log("can't find", dep); + continue; + } + + imports[ + dep + ] = `./node_modules/.vite/deps/${m.file}?v=${m.fileHash}`; + } + + $("head").append( + `` + ); + + return $.html(); + }; + }, + }, + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), @@ -32,9 +94,34 @@ export default defineConfig({ }, build: { rollupOptions: { + external: SHARED_DEPENDENCIES, input: { main: path.resolve(__dirname, "index.html"), "service-worker": path.resolve(__dirname, "service-worker.js"), + ...Object.fromEntries( + globSync( + path.resolve(__dirname, "src/datatypes/*/module.@(ts|js|tsx|jsx)") + ).map((path) => { + const datatypeId = path.split("/").slice(-2)[0]; + + return [ + `dataType-${datatypeId}`, + fileURLToPath(new URL(path, import.meta.url)), + ]; + }) + ), + ...Object.fromEntries( + globSync( + path.resolve(__dirname, "src/tools/*/module.@(ts|js|tsx|jsx)") + ).map((path) => { + const toolId = path.split("/").slice(-2)[0]; + + return [ + `tool-${toolId}`, + fileURLToPath(new URL(path, import.meta.url)), + ]; + }) + ), }, output: { // We put index.css in dist instead of dist/assets so that we can link to fonts @@ -52,9 +139,24 @@ export default defineConfig({ if (chunkInfo.name === "service-worker") { return "[name].js"; // This will place service-worker.js directly under dist } + + // output tools under "/tools" + if (chunkInfo.name.startsWith("tool-")) { + const typeId = chunkInfo.name.split("-")[1]; + return `tools/${typeId}.js`; + } + + // output datatypes under "/dataTypes" + if (chunkInfo.name.startsWith("dataType-")) { + const typeId = chunkInfo.name.split("-")[1]; + return `dataTypes/${typeId}.js`; + } + return "assets/[name]-[hash].js"; // Default behavior for other entries }, + exports: "named", }, + preserveEntrySignatures: "allow-extension", }, }, diff --git a/yarn.lock b/yarn.lock index 0ad23e0b..6457db82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2381,11 +2381,26 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/jsonfile@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.199": version "4.17.1" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8" @@ -2406,6 +2421,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@^14.18.63": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + "@types/node@^18.11.18": version "18.19.33" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.33.tgz#98cd286a1b8a5e11aa06623210240bcc28e95c48" @@ -2836,6 +2856,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2976,6 +3001,31 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + chevrotain@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-6.5.0.tgz#dcbef415516b0af80fd423cc0d96b28d3f11374e" @@ -3135,6 +3185,17 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" @@ -3144,6 +3205,11 @@ css-to-react-native@3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3528,6 +3594,20 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -3535,11 +3615,27 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + dompurify@^2.1.1: version "2.5.3" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.3.tgz#bc901a9c40a7d97176c1d0ab9a24939db54270a2" integrity sha512-09uyBM2URzOfXMUAqGRnm9R9IUeSkzO9PktXc2eVQIsBmmJUqRmfL1xW2QPBxVJEtlEVs5d8ndrsIQsyAqs81g== +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3560,7 +3656,7 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -entities@^4.4.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -3843,6 +3939,15 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fs-extra@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3898,6 +4003,17 @@ glob@^10.3.10: minipass "^7.0.4" path-scurry "^1.11.0" +glob@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3934,6 +4050,11 @@ globby@^11.0.4, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -3999,6 +4120,16 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -4185,6 +4316,15 @@ jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.1.2.tgz#eada67ea949c6b71de50f1b09c92a961897b90ab" + integrity sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jiti@^1.21.0: version "1.21.0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" @@ -4261,6 +4401,15 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -4454,7 +4603,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1: +minimatch@^9.0.1, minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== @@ -4466,6 +4615,11 @@ minimatch@^9.0.1: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.1.tgz#f7f85aff59aa22f110b20e27692465cf3bf89481" integrity sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mlly@^1.4.0, mlly@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.0.tgz#587383ae40dda23cadb11c3c3cc972b277724271" @@ -4564,6 +4718,13 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + numbro@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.2.tgz#2d51104f09b5d69aef7e15bb565d7795e47ecfd6" @@ -4652,7 +4813,15 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse5@^7.1.2: +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== @@ -4679,7 +4848,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.11.0: +path-scurry@^1.11.0, path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -5632,6 +5801,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + update-browserslist-db@^1.0.13: version "1.0.15" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz#60ed9f8cba4a728b7ecf7356f641a31e3a691d97" @@ -5719,6 +5893,20 @@ vite-node@0.34.6: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" +vite-plugin-external@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/vite-plugin-external/-/vite-plugin-external-4.3.1.tgz#d91cf44736fc9e311ed7576d0d41acc630901b1b" + integrity sha512-aoukfac66QevFAbRF2ZD81WPSqeqhgEfbfhPYQP9RPWO1en+Lw4HyhWRdvSjYn56gcKUJCtHdFG2jEpYgleLng== + dependencies: + "@types/fs-extra" "^11.0.4" + "@types/node" "^14.18.63" + fs-extra "^11.1.1" + +vite-plugin-externalize-dependencies@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/vite-plugin-externalize-dependencies/-/vite-plugin-externalize-dependencies-0.12.0.tgz#f0a74b6796dffdfbd861eb10b473217f82a84f84" + integrity sha512-sauIb6N7I93DbRKD2SNWZhrfCdBB3+/L/O4uujCH+8NyhISqSGcGCUDD7bYz0hkVpXmUhTzIQGnzUJDGbFb0Cg== + vite-plugin-top-level-await@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.1.tgz#607dfe084157550fa33df18062b99ceea774cd9c"