├── __mocks__
├── points-on-curve.js
├── roughjs
│ └── bin
│ │ ├── rough.js
│ │ └── math.js
└── @excalidraw
│ └── excalidraw.js
├── .yarnrc.yml
├── firestore.indexes.json
├── .firebaserc
├── screenshot.png
├── .yarn
└── install-state.gz
├── postcss.config.js
├── src
├── environment-interface
│ ├── copyImageToClipboard.ts
│ ├── authentication.ts
│ ├── index.tsx
│ ├── loom.ts
│ └── storage.ts
├── environments
│ ├── copyImageToClipboard
│ │ ├── test.ts
│ │ └── browser.ts
│ ├── loom
│ │ ├── test.ts
│ │ └── browser.ts
│ ├── authentication
│ │ ├── test.ts
│ │ └── browser.ts
│ ├── storage
│ │ ├── test.ts
│ │ └── browser.ts
│ ├── test.ts
│ └── browser.ts
├── firebase.config.json
├── pages
│ ├── excalidraw
│ │ ├── index.tsx
│ │ ├── useExcalidraw
│ │ │ ├── types.ts
│ │ │ ├── index.tsx
│ │ │ ├── reducer.ts
│ │ │ └── index.test.tsx
│ │ ├── useRecording.tsx
│ │ ├── ExcalidrawCanvas.tsx
│ │ └── Excalidraw.tsx
│ ├── dashboard
│ │ ├── Dashboard.tsx
│ │ ├── index.tsx
│ │ ├── useDashboard.tsx
│ │ ├── useUserDashboard.tsx
│ │ ├── useDashboard.test.tsx
│ │ ├── ExcalidrawPreview.tsx
│ │ ├── useNavigation.tsx
│ │ ├── useNavigation.test.tsx
│ │ └── Navigation.tsx
│ ├── index.tsx
│ ├── useAuth.tsx
│ └── useAuth.test.tsx
├── index.css
├── main.tsx
├── favicon.svg
├── logo.svg
└── utils.ts
├── vite.config.ts
├── firestore.rules
├── firebase.json
├── index.html
├── tailwind.config.js
├── tsconfig.json
├── README.md
├── LICENSE
├── .codesandbox
└── tasks.json
├── package.json
├── .gitignore
├── public
└── index.html
└── .pnp.loader.mjs
/__mocks__/points-on-curve.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | yarnPath: .yarn/releases/yarn-4.0.1.cjs
2 |
--------------------------------------------------------------------------------
/__mocks__/roughjs/bin/rough.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/__mocks__/@excalidraw/excalidraw.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [],
3 | "fieldOverrides": []
4 | }
5 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "excalidraw-8b385"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codesandbox/excalidraw-firebase/HEAD/screenshot.png
--------------------------------------------------------------------------------
/.yarn/install-state.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codesandbox/excalidraw-firebase/HEAD/.yarn/install-state.gz
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/environment-interface/copyImageToClipboard.ts:
--------------------------------------------------------------------------------
1 | export interface CopyImageToClipboard {
2 | (image: Blob): void;
3 | }
4 |
--------------------------------------------------------------------------------
/__mocks__/roughjs/bin/math.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | Random: class {
3 | next() {
4 | return 0;
5 | }
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | import react from "@vitejs/plugin-react";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | });
9 |
--------------------------------------------------------------------------------
/src/environments/copyImageToClipboard/test.ts:
--------------------------------------------------------------------------------
1 | import { CopyImageToClipboard } from "../../environment-interface/copyImageToClipboard";
2 |
3 | export const createCopyImageToClipboard = (): CopyImageToClipboard => jest.fn();
4 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service cloud.firestore {
3 | match /databases/{database}/documents {
4 | match /{document=**} {
5 | allow read, write: if request.auth.token.email.matches('.*@codesandbox.io$');
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/environments/loom/test.ts:
--------------------------------------------------------------------------------
1 | import { createEmitter } from "react-states";
2 | import { Loom } from "../../environment-interface/loom";
3 |
4 | export const createLoom = (): Loom => ({
5 | ...createEmitter(),
6 | configure: jest.fn(),
7 | openVideo: jest.fn(),
8 | });
9 |
--------------------------------------------------------------------------------
/src/environments/authentication/test.ts:
--------------------------------------------------------------------------------
1 | import { createEmitter } from "react-states";
2 | import { Authentication } from "../../environment-interface/authentication";
3 |
4 | export const createAuthentication = (): Authentication => ({
5 | ...createEmitter(),
6 | signIn: jest.fn(),
7 | });
8 |
--------------------------------------------------------------------------------
/src/firebase.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiKey": "AIzaSyD0We3AAFK0UNTBsf5Cweisi_5hV9X9h9I",
3 | "authDomain": "excalidraw-8b385.firebaseapp.com",
4 | "projectId": "excalidraw-8b385",
5 | "storageBucket": "excalidraw-8b385.appspot.com",
6 | "messagingSenderId": "807535976681",
7 | "appId": "1:807535976681:web:2fe48e87a254911d81d078"
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Excalidraw } from "./Excalidraw";
4 |
5 | import { useParams } from "react-router-dom";
6 |
7 | export const ExcalidrawPage = () => {
8 | const { id, userId } = useParams<{ id: string; userId: string }>();
9 |
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | },
6 | "hosting": {
7 | "public": "dist",
8 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/environments/copyImageToClipboard/browser.ts:
--------------------------------------------------------------------------------
1 | import { CopyImageToClipboard } from "../../environment-interface/copyImageToClipboard";
2 |
3 | export const createCopyImageToClipboard = (): CopyImageToClipboard => (
4 | image
5 | ) => {
6 | // @ts-ignore
7 | navigator.clipboard.write([
8 | // @ts-ignore
9 | new window.ClipboardItem({ "image/png": image }),
10 | ]);
11 | };
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | CSB Excalidraw
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/environments/storage/test.ts:
--------------------------------------------------------------------------------
1 | import { createEmitter } from "react-states";
2 | import { Storage } from "../../environment-interface/storage";
3 |
4 | export const createStorage = (): Storage => ({
5 | ...createEmitter(),
6 | createExcalidraw: jest.fn(),
7 | fetchExcalidraw: jest.fn(),
8 | fetchPreviews: jest.fn(),
9 | fetchUserPreviews: jest.fn(),
10 | saveExcalidraw: jest.fn(),
11 | getImageSrc: jest.fn(),
12 | saveTitle: jest.fn(),
13 | });
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: {
3 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
4 | safelist: [
5 | "text-gray-500",
6 | "bg-gray-50",
7 | "hover:bg-gray-100",
8 | "focus:ring-gray-50",
9 | "opacity-50",
10 | "text-green-500",
11 | "bg-green-50",
12 | "hover:bg-green-100",
13 | "focus:ring-green-50",
14 | ],
15 | },
16 | darkMode: false, // or 'media' or 'class'
17 | theme: {
18 | extend: {},
19 | },
20 | variants: {
21 | extend: {},
22 | },
23 | plugins: [],
24 | };
25 |
--------------------------------------------------------------------------------
/src/environments/test.ts:
--------------------------------------------------------------------------------
1 | import { createEnvironment } from "../environment-interface";
2 | import { createAuthentication } from "./authentication/test";
3 | import { createCopyImageToClipboard } from "./copyImageToClipboard/test";
4 | import { createLoom } from "./loom/test";
5 | import { createStorage } from "./storage/test";
6 |
7 | export const createTestEnvironment = () =>
8 | createEnvironment(() => {
9 | return {
10 | authentication: createAuthentication(),
11 | copyImageToClipboard: createCopyImageToClipboard(),
12 | loom: createLoom(),
13 | storage: createStorage(),
14 | };
15 | });
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "types": ["vite/client", "jest", "node"],
6 | "paths": {
7 | "@excalidraw/excalidraw": ["./src/excalidraw.d.ts"]
8 | },
9 | "allowJs": false,
10 | "skipLibCheck": false,
11 | "esModuleInterop": false,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "ESNext",
16 | "moduleResolution": "Node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": ["./src"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/environment-interface/authentication.ts:
--------------------------------------------------------------------------------
1 | import { TEmit, TSubscribe } from "react-states";
2 |
3 | export type User = {
4 | uid: string;
5 | name: string;
6 | avatarUrl: string | null;
7 | };
8 |
9 | export type AuthenticationEvent =
10 | | {
11 | type: "AUTHENTICATION:AUTHENTICATED";
12 | user: User;
13 | loomApiKey: string | null;
14 | }
15 | | {
16 | type: "AUTHENTICATION:UNAUTHENTICATED";
17 | }
18 | | {
19 | type: "AUTHENTICATION:SIGN_IN_ERROR";
20 | error: string;
21 | };
22 |
23 | export interface Authentication {
24 | subscribe: TSubscribe
25 | emit: TEmit
26 | signIn(): void;
27 | }
28 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .lds-dual-ring {
6 | display: inline-block;
7 | position: absolute;
8 | left: 50%;
9 | top: 50%;
10 | transform: translate(-50%, -50%);
11 | }
12 |
13 | .lds-dual-ring:after {
14 | content: " ";
15 | display: block;
16 | width: 12px;
17 | height: 12px;
18 | border-radius: 50%;
19 | border: 4px solid #333;
20 | border-color: #333 transparent #333 transparent;
21 | animation: lds-dual-ring 1.2s linear infinite;
22 | opacity: 0.4;
23 | }
24 |
25 | @keyframes lds-dual-ring {
26 | 0% {
27 | transform: rotate(0deg);
28 | }
29 | 100% {
30 | transform: rotate(360deg);
31 | }
32 | }
33 |
34 | .library-button {
35 | display: none !important;
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ExcalidrawPreview as ExcalidrawPreviewComponent } from "./ExcalidrawPreview";
3 | import { ExcalidrawPreview } from "../../environment-interface/storage";
4 |
5 | export const Dashboard = ({
6 | excalidraws,
7 | }: {
8 | excalidraws: ExcalidrawPreview[];
9 | }) => {
10 | return (
11 |
12 |
13 | {excalidraws.map((excalidraw) => (
14 |
19 | ))}
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/environments/browser.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/auth";
3 | import "firebase/firestore";
4 | import "firebase/storage";
5 |
6 | import config from "../firebase.config.json";
7 | import { createEnvironment } from "../environment-interface";
8 | import { createAuthentication } from "./authentication/browser";
9 | import { createCopyImageToClipboard } from "./copyImageToClipboard/browser";
10 | import { createLoom } from "./loom/browser";
11 | import { createStorage } from "./storage/browser";
12 |
13 | export const environment = createEnvironment(() => {
14 | const app = firebase.initializeApp(config);
15 |
16 | return {
17 | authentication: createAuthentication(app),
18 | copyImageToClipboard: createCopyImageToClipboard(),
19 | loom: createLoom(),
20 | storage: createStorage(app),
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { DevtoolsProvider } from "react-states/devtools";
4 | import "./index.css";
5 | import { Pages } from "./pages";
6 |
7 | import { EnvironmentProvider } from "./environment-interface";
8 | import { environment } from "./environments/browser";
9 |
10 | // Polyfill for Loom
11 | if (typeof (window as any).global === "undefined") {
12 | (window as any).global = window;
13 | }
14 |
15 | const app = (
16 |
17 |
18 |
19 | );
20 |
21 | const container = document.getElementById("root")!;
22 | const root = createRoot(container); // createRoot(container!) if you use TypeScript
23 | root.render(
24 |
25 | {import.meta.env.PROD ? app : {app}}
26 | ,
27 | );
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # excalidraw-firebase
2 |
3 | > A persistent catalogue of your and/or company Excalidraws
4 |
5 | 
6 |
7 | ## How to set up
8 |
9 | 1. Create a Firebase Project on the [Firebase Console](https://console.firebase.google.com/u/0/)
10 | 2. Copy the Firebase config to the `src/firebase.config.json` file:
11 |
12 | ```json
13 | {
14 | "apiKey": "...",
15 | "authDomain": "...",
16 | "projectId": "...",
17 | "storageBucket": "...",
18 | "messagingSenderId": "...",
19 | "appId": "..."
20 | }
21 | ```
22 |
23 | 3. Add **Google** as Authentication -> Sign In Method, in Firebase Console
24 | 4. Install the Firebase tools: `yarn add -g firebase-tools` and log in `firebase login`
25 | 5. Change the `firestore.rules` file to reflect your personal email or your company Google domain
26 | 6. (Optional) Go to Authentication -> Sign In Method and add a custom domain
27 | 7. Deploy it with `yarn deploy`
28 |
--------------------------------------------------------------------------------
/src/environment-interface/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Authentication } from "./authentication";
3 | import { CopyImageToClipboard } from "./copyImageToClipboard";
4 | import { Storage } from "./storage";
5 | import { Loom } from "./loom";
6 | import { createContext, useContext } from "react";
7 |
8 |
9 | export interface Environment {
10 | storage: Storage;
11 | authentication: Authentication;
12 | copyImageToClipboard: CopyImageToClipboard;
13 | loom: Loom;
14 | }
15 |
16 | const environmentContext = createContext({} as Environment)
17 |
18 | export const useEnvironment = () => useContext(environmentContext)
19 |
20 | export const EnvironmentProvider: React.FC<{ environment: Environment }> = ({ children, environment}) => (
21 | {children}
22 | )
23 |
24 | export const createEnvironment = (constr: () => Environment) => constr()
--------------------------------------------------------------------------------
/src/environment-interface/loom.ts:
--------------------------------------------------------------------------------
1 | import { TEmit, TSubscribe } from "react-states";
2 |
3 | export interface LoomVideo {
4 | id: string;
5 | title: string;
6 | height: number;
7 | width: number;
8 | sharedUrl: string;
9 | embedUrl: string;
10 | thumbnailHeight?: number;
11 | thumbnailWidth?: number;
12 | thumbnailUrl?: string;
13 | duration?: number;
14 | providerUrl: string;
15 | }
16 |
17 | export type LoomEvent =
18 | | {
19 | type: "LOOM:CONFIGURED";
20 | }
21 | | {
22 | type: "LOOM:INSERT";
23 | video: LoomVideo;
24 | }
25 | | {
26 | type: "LOOM:START";
27 | }
28 | | {
29 | type: "LOOM:CANCEL";
30 | }
31 | | {
32 | type: "LOOM:COMPLETE";
33 | }
34 | | {
35 | type: "LOOM:ERROR";
36 | error: string;
37 | };
38 |
39 | export interface Loom {
40 | subscribe: TSubscribe;
41 | emit: TEmit
42 | configure(apiKey: string, buttonId: string): void;
43 | openVideo(video: LoomVideo): void;
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 CodeSandbox
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.codesandbox/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // These tasks will run in order when initializing your CodeSandbox project.
3 | "setupTasks": [
4 | {
5 | "name": "Install Dependencies",
6 | "command": "yarn install"
7 | }
8 | ],
9 |
10 | // These tasks can be run from CodeSandbox. Running one will open a log in the app.
11 | "tasks": {
12 | "auth": {
13 | "name": "auth",
14 | "command": "npx firebase login --no-localhost",
15 | "preview": {
16 | "port": 9005
17 | }
18 | },
19 | "dev": {
20 | "name": "dev",
21 | "command": "yarn dev",
22 | "runAtStart": true,
23 | "preview": {
24 | "port": 3000
25 | }
26 | },
27 | "build": {
28 | "name": "build",
29 | "command": "yarn build",
30 | "runAtStart": false
31 | },
32 | "deploy": {
33 | "name": "deploy",
34 | "command": "yarn deploy",
35 | "runAtStart": false
36 | },
37 | "serve": {
38 | "name": "serve",
39 | "command": "yarn serve",
40 | "runAtStart": false
41 | },
42 | "test": {
43 | "name": "test",
44 | "command": "yarn test",
45 | "runAtStart": false
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "excalidraw-firebase",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "deploy": "yarn build && firebase deploy",
8 | "serve": "vite preview"
9 | },
10 | "dependencies": {
11 | "@excalidraw/excalidraw": "^0.16.1",
12 | "@fortawesome/fontawesome-svg-core": "^1.2.34",
13 | "@fortawesome/free-solid-svg-icons": "^5.15.2",
14 | "@fortawesome/react-fontawesome": "^0.1.14",
15 | "@loomhq/loom-sdk": "^1.4.11",
16 | "date-fns": "^2.19.0",
17 | "firebase": "^8.2.9",
18 | "lodash.debounce": "^4.0.8",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-router": "^5.2.0",
22 | "react-router-dom": "^6.18.0",
23 | "react-states": "^6.2.3",
24 | "tailwindcss": "^2.2.4"
25 | },
26 | "devDependencies": {
27 | "@types/lodash.debounce": "^4.0.6",
28 | "@types/node": "^20",
29 | "@types/react": "^18.2.34",
30 | "@types/react-dom": "^18.2.14",
31 | "@types/react-router-dom": "^5.1.7",
32 | "@vitejs/plugin-react": "^4.1.1",
33 | "autoprefixer": "^10.2.6",
34 | "postcss": "^8.3.5",
35 | "typescript": "^4.1.2",
36 | "vite": "^4.5.0"
37 | },
38 | "packageManager": "yarn@4.0.1"
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/useExcalidraw/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ExcalidrawData,
3 | ExcalidrawMetadata,
4 | } from "../../../environment-interface/storage";
5 |
6 | export type ClipboardState =
7 | | {
8 | state: "COPIED";
9 | }
10 | | {
11 | state: "NOT_COPIED";
12 | };
13 |
14 | export type BaseState = {
15 | data: ExcalidrawData;
16 | remoteData?: ExcalidrawData;
17 | metadata: ExcalidrawMetadata;
18 | image: Blob;
19 | clipboard: ClipboardState;
20 | };
21 |
22 | export type ExcalidrawState =
23 | | {
24 | state: "LOADING";
25 | }
26 | | {
27 | state: "ERROR";
28 | error: string;
29 | }
30 | | (BaseState &
31 | (
32 | | {
33 | state: "LOADED";
34 | }
35 | | {
36 | state: "EDIT";
37 | }
38 | | {
39 | state: "DIRTY";
40 | }
41 | | {
42 | state: "SYNCING";
43 | }
44 | | {
45 | state: "SYNCING_DIRTY";
46 | }
47 | ));
48 |
49 | export type ExcalidrawAction =
50 | | {
51 | type: "INITIALIZE_CANVAS_SUCCESS";
52 | }
53 | | {
54 | type: "COPY_TO_CLIPBOARD";
55 | }
56 | | {
57 | type: "EXCALIDRAW_CHANGE";
58 | data: ExcalidrawData;
59 | }
60 | | {
61 | type: "SAVE_TITLE";
62 | title: string;
63 | };
64 |
65 | export type PrivateAction = {
66 | type: "SYNC";
67 | };
68 |
--------------------------------------------------------------------------------
/src/pages/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Dashboard } from "./Dashboard";
3 | import { match, PickState } from "react-states";
4 |
5 | import { Navigation } from "./Navigation";
6 | import { useMatch } from "react-router-dom";
7 |
8 | import { useDashboard } from "./useDashboard";
9 | import { useUserDashboard } from "./useUserDashboard";
10 |
11 | const SharedDashboard = () => {
12 | const state = useDashboard();
13 |
14 | return match(state, {
15 | LOADING_PREVIEWS: () => ,
16 | PREVIEWS_ERROR: ({ error }) => (
17 | There was an error: {error}
18 | ),
19 | PREVIEWS_LOADED: ({ excalidraws }) => (
20 |
21 | ),
22 | });
23 | };
24 |
25 | const UserDashboard: React.FC<{ uid: string }> = ({ uid }) => {
26 | const [state] = useUserDashboard({ uid });
27 |
28 | return match(state, {
29 | LOADING_PREVIEWS: () => ,
30 | PREVIEWS_ERROR: ({ error }) => (
31 | There was an error: {error}
32 | ),
33 | PREVIEWS_LOADED: ({ excalidraws }) => (
34 |
35 | ),
36 | });
37 | };
38 |
39 | export const DashboardPage = () => {
40 | const match = useMatch("/:userId");
41 |
42 | return (
43 |
44 |
45 | {match?.params.userId ? (
46 |
47 | ) : (
48 |
49 | )}
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/pages/dashboard/useDashboard.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, useEffect, useReducer } from "react";
2 | import { transition, useDevtools, useStateEffect } from "react-states";
3 | import { useEnvironment } from "../../environment-interface";
4 | import {
5 | ExcalidrawPreviews,
6 | StorageEvent,
7 | } from "../../environment-interface/storage";
8 |
9 | export type DashboardState =
10 | | {
11 | state: "LOADING_PREVIEWS";
12 | }
13 | | {
14 | state: "PREVIEWS_LOADED";
15 | excalidraws: ExcalidrawPreviews;
16 | }
17 | | {
18 | state: "PREVIEWS_ERROR";
19 | error: string;
20 | };
21 |
22 | const reducer = (state: DashboardState, action: StorageEvent) =>
23 | transition(state, action, {
24 | LOADING_PREVIEWS: {
25 | "STORAGE:FETCH_PREVIEWS_SUCCESS": (
26 | _,
27 | { excalidraws }
28 | ): DashboardState => ({
29 | state: "PREVIEWS_LOADED",
30 | excalidraws,
31 | }),
32 | "STORAGE:FETCH_PREVIEWS_ERROR": (_, { error }): DashboardState => ({
33 | state: "PREVIEWS_ERROR",
34 | error,
35 | }),
36 | },
37 | PREVIEWS_LOADED: {},
38 | PREVIEWS_ERROR: {},
39 | });
40 |
41 | export const useDashboard = (
42 | initialState: DashboardState = {
43 | state: "LOADING_PREVIEWS",
44 | }
45 | ): DashboardState => {
46 | const { storage } = useEnvironment();
47 | const dashboardReducer = useReducer(reducer, initialState);
48 |
49 | useDevtools("dashboard", dashboardReducer);
50 |
51 | const [state, dispatch] = dashboardReducer;
52 |
53 | useEffect(() => storage.subscribe(dispatch), []);
54 |
55 | useStateEffect(state, "LOADING_PREVIEWS", () => storage.fetchPreviews());
56 |
57 | return state;
58 | };
59 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/useExcalidraw/index.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, useEffect, useReducer } from "react";
2 |
3 | import { reducer } from "./reducer";
4 | import { ExcalidrawAction, ExcalidrawState } from "./types";
5 |
6 | import { useEnvironment } from "../../../environment-interface";
7 | import { useDevtools, useTransitionEffect } from "react-states";
8 |
9 | export const useExcalidraw = ({
10 | id,
11 | userId,
12 |
13 | initialState = {
14 | state: "LOADING",
15 | },
16 | }: {
17 | id: string;
18 | userId: string;
19 |
20 | initialState?: ExcalidrawState;
21 | }): [ExcalidrawState, Dispatch] => {
22 | const { storage, copyImageToClipboard } = useEnvironment();
23 | const excalidrawReducer = useReducer(reducer, initialState);
24 |
25 | useDevtools("excalidraw", excalidrawReducer);
26 |
27 | const [state, dispatch] = excalidrawReducer;
28 |
29 | useEffect(() => storage.subscribe(dispatch), []);
30 |
31 | useTransitionEffect(state, "EDIT", "COPY_TO_CLIPBOARD", ({ image }) => {
32 | copyImageToClipboard(image);
33 | });
34 |
35 | useTransitionEffect(state, "EDIT", "SAVE_TITLE", (_, { title }) => {
36 | storage.saveTitle(userId, id, title);
37 | });
38 |
39 | useTransitionEffect(state, "LOADING", () =>
40 | storage.fetchExcalidraw(userId, id)
41 | );
42 |
43 | useTransitionEffect(state, "SYNCING", ({ data }) => {
44 | storage.saveExcalidraw(userId, id, data);
45 | });
46 |
47 | useTransitionEffect(state, "DIRTY", () => {
48 | const id = setTimeout(() => {
49 | dispatch({
50 | type: "SYNC",
51 | });
52 | }, 500);
53 |
54 | return () => {
55 | clearTimeout(id);
56 | };
57 | });
58 |
59 | return excalidrawReducer;
60 | };
61 |
--------------------------------------------------------------------------------
/src/pages/dashboard/useUserDashboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, Dispatch, useEffect, useReducer } from "react";
2 | import { transition, useDevtools, useStateEffect } from "react-states";
3 | import { useEnvironment } from "../../environment-interface";
4 | import {
5 | ExcalidrawPreviews,
6 | StorageEvent,
7 | } from "../../environment-interface/storage";
8 |
9 | export type UserDashboardState =
10 | | {
11 | state: "LOADING_PREVIEWS";
12 | }
13 | | {
14 | state: "PREVIEWS_LOADED";
15 | excalidraws: ExcalidrawPreviews;
16 | }
17 | | {
18 | state: "PREVIEWS_ERROR";
19 | error: string;
20 | };
21 |
22 | export type UserDashboardAction = {
23 | type: "CREATE_EXCALIDRAW";
24 | };
25 |
26 | const reducer = (
27 | state: UserDashboardState,
28 | action: UserDashboardAction | StorageEvent
29 | ) =>
30 | transition(state, action, {
31 | LOADING_PREVIEWS: {
32 | "STORAGE:FETCH_USER_PREVIEWS_SUCCESS": (
33 | _,
34 | { excalidraws }
35 | ): UserDashboardState => ({
36 | state: "PREVIEWS_LOADED",
37 | excalidraws,
38 | }),
39 | "STORAGE:FETCH_USER_PREVIEWS_ERROR": (
40 | _,
41 | { error }
42 | ): UserDashboardState => ({
43 | state: "PREVIEWS_ERROR",
44 | error,
45 | }),
46 | },
47 | PREVIEWS_LOADED: {},
48 | PREVIEWS_ERROR: {},
49 | });
50 |
51 | export const useUserDashboard = ({
52 | uid,
53 | initialState = {
54 | state: "LOADING_PREVIEWS",
55 | },
56 | }: {
57 | uid: string;
58 | initialState?: UserDashboardState;
59 | }): [UserDashboardState, Dispatch] => {
60 | const { storage } = useEnvironment();
61 | const userDashboardReducer = useReducer(reducer, initialState);
62 |
63 | useDevtools("dashboard", userDashboardReducer);
64 |
65 | const [state, dispatch] = userDashboardReducer;
66 |
67 | useEffect(() => storage.subscribe(dispatch), []);
68 |
69 | useStateEffect(state, "LOADING_PREVIEWS", () =>
70 | storage.fetchUserPreviews(uid)
71 | );
72 |
73 | return userDashboardReducer;
74 | };
75 |
--------------------------------------------------------------------------------
/src/environments/authentication/browser.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import { createEmitter } from "react-states";
3 | import {
4 | Authentication,
5 | AuthenticationEvent,
6 | User,
7 | } from "../../environment-interface/authentication";
8 |
9 | const USERS_COLLECTION = "users";
10 | const CONFIG_COLLECTION = "config";
11 | const API_KEYS_DOCUMENT = "apiKeys";
12 |
13 | const getUser = (firebaseUser: firebase.User): User => ({
14 | uid: firebaseUser.uid,
15 | name: firebaseUser.email!.split("@")[0],
16 | avatarUrl: firebaseUser.providerData[0]?.photoURL ?? null,
17 | });
18 |
19 | export const createAuthentication = (app: firebase.app.App): Authentication => {
20 | const { emit, subscribe } = createEmitter();
21 |
22 | app.auth().onAuthStateChanged((firebaseUser) => {
23 | if (firebaseUser) {
24 | const user = getUser(firebaseUser);
25 |
26 | /*
27 | We update the user document with name and avatarUrl so other
28 | users can see it as well
29 | */
30 | app.firestore().collection(USERS_COLLECTION).doc(user.uid).set(
31 | {
32 | name: user.name,
33 | avatarUrl: user.avatarUrl,
34 | },
35 | {
36 | merge: true,
37 | }
38 | );
39 |
40 | app
41 | .firestore()
42 | .collection(CONFIG_COLLECTION)
43 | .doc(API_KEYS_DOCUMENT)
44 | .get()
45 | .then((doc) => {
46 | const data = doc.data();
47 | emit({
48 | type: "AUTHENTICATION:AUTHENTICATED",
49 | user,
50 | loomApiKey: data?.loom ?? null,
51 | });
52 | });
53 | } else {
54 | emit({
55 | type: "AUTHENTICATION:UNAUTHENTICATED",
56 | });
57 | }
58 | });
59 |
60 | return {
61 | emit,
62 | subscribe,
63 | signIn: () => {
64 | const provider = new firebase.auth.GoogleAuthProvider();
65 | app
66 | .auth()
67 | .signInWithPopup(provider)
68 | .catch((error: Error) => {
69 | emit({
70 | type: "AUTHENTICATION:SIGN_IN_ERROR",
71 | error: error.message,
72 | });
73 | });
74 | },
75 | };
76 | };
77 |
--------------------------------------------------------------------------------
/src/environments/loom/browser.ts:
--------------------------------------------------------------------------------
1 | import { setup } from "@loomhq/loom-sdk";
2 | import { createEmitter } from "react-states";
3 | import { Loom, LoomEvent } from "../../environment-interface/loom";
4 |
5 | type ButtonFn = ReturnType extends Promise
6 | ? R extends { configureButton: any }
7 | ? R["configureButton"]
8 | : never
9 | : never;
10 |
11 | export const createLoom = (): Loom => {
12 | const { subscribe, emit } = createEmitter();
13 |
14 | let configureButton: ButtonFn | undefined;
15 |
16 | function initialize(configure: ButtonFn, buttonId: string) {
17 | const element = document.querySelector(`#${buttonId}`);
18 |
19 | if (!element) {
20 | emit({
21 | type: "LOOM:ERROR",
22 | error: "No button",
23 | });
24 | return;
25 | }
26 |
27 | configure({
28 | element: element as HTMLElement,
29 | hooks: {
30 | onInsertClicked: (video) => {
31 | if (video) {
32 | emit({
33 | type: "LOOM:INSERT",
34 | video,
35 | });
36 | } else {
37 | emit({
38 | type: "LOOM:CANCEL",
39 | });
40 | }
41 | },
42 | onStart: () => {
43 | emit({
44 | type: "LOOM:START",
45 | });
46 | },
47 | onCancel: () => {
48 | emit({
49 | type: "LOOM:CANCEL",
50 | });
51 | },
52 | onComplete: () => {
53 | emit({
54 | type: "LOOM:COMPLETE",
55 | });
56 | },
57 | },
58 | });
59 | }
60 |
61 | return {
62 | subscribe,
63 | emit,
64 | configure(apiKey, buttonId) {
65 | if (configureButton) {
66 | initialize(configureButton, buttonId);
67 | } else {
68 | setup({
69 | apiKey,
70 | }).then((result) => {
71 | emit({
72 | type: "LOOM:CONFIGURED",
73 | });
74 | configureButton = result.configureButton;
75 | initialize(configureButton, buttonId);
76 | });
77 | }
78 | },
79 | openVideo(video) {
80 | window.open(video.sharedUrl);
81 | },
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .DS_Store
9 | .firebase/**
10 | ssl.crt
11 | ssl.key
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # TypeScript v1 declaration files
49 | typings/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variables file
76 | .env
77 | .env.test
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 |
82 | # Next.js build output
83 | .next
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
3 | import { match } from "react-states";
4 | import { AuthenticatedAuthProvider, useAuth } from "./useAuth";
5 | import { DashboardPage } from "./dashboard";
6 | import { ExcalidrawPage } from "./excalidraw";
7 |
8 | const router = createBrowserRouter([
9 | {
10 | path: "/",
11 | element: ,
12 | },
13 | {
14 | path: "/:userId",
15 | element: ,
16 | },
17 | {
18 | path: "/:userId/:id",
19 | element: ,
20 | },
21 | ]);
22 |
23 | export const Pages = () => {
24 | const [auth, dispatch] = useAuth();
25 |
26 | return (
27 |
28 | {match(auth, {
29 | UNAUTHENTICATED: () => (
30 |
31 |
37 |
38 | ),
39 | CHECKING_AUTHENTICATION: () => (
40 |
43 | ),
44 | AUTHENTICATED: (authenticatedAuth) => (
45 |
46 |
47 |
48 | ),
49 | SIGNING_IN: () => (
50 |
53 | ),
54 | ERROR: ({ error }) => (
55 |
56 |
Uh oh, something bad happened
57 | {error}
58 |
59 | ),
60 | })}
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/useRecording.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, useEffect, useReducer } from "react";
2 | import { transition, useDevtools, useTransitionEffect } from "react-states";
3 | import { useEnvironment } from "../../environment-interface";
4 |
5 | import { LoomEvent, LoomVideo } from "../../environment-interface/loom";
6 |
7 | export type RecordingState =
8 | | {
9 | state: "DISABLED";
10 | }
11 | | {
12 | state: "NOT_CONFIGURED";
13 | apiKey: string;
14 | buttonId: "loom-record";
15 | }
16 | | {
17 | state: "READY";
18 | }
19 | | {
20 | state: "RECORDING";
21 | };
22 |
23 | type RecordingAction = {
24 | type: "RECORD";
25 | };
26 |
27 | const reducer = (state: RecordingState, action: RecordingAction | LoomEvent) =>
28 | transition(state, action, {
29 | DISABLED: {},
30 | NOT_CONFIGURED: {
31 | "LOOM:CONFIGURED": (state): RecordingState => ({
32 | ...state,
33 | state: "READY",
34 | }),
35 | },
36 | READY: {
37 | "LOOM:INSERT": (state): RecordingState => ({
38 | ...state,
39 | }),
40 | "LOOM:START": (): RecordingState => ({
41 | state: "RECORDING",
42 | }),
43 | },
44 | RECORDING: {
45 | "LOOM:CANCEL": (): RecordingState => ({
46 | state: "READY",
47 | }),
48 | "LOOM:COMPLETE": (): RecordingState => ({
49 | state: "READY",
50 | }),
51 | },
52 | });
53 |
54 | export const useRecording = ({
55 | apiKey,
56 | initialState = apiKey
57 | ? {
58 | state: "NOT_CONFIGURED",
59 | apiKey,
60 | buttonId: "loom-record",
61 | }
62 | : {
63 | state: "DISABLED",
64 | },
65 | }: {
66 | apiKey: string | null;
67 | initialState?: RecordingState;
68 | }): [RecordingState, Dispatch] => {
69 | const { loom } = useEnvironment();
70 | const recording = useReducer(reducer, initialState);
71 |
72 | useDevtools("recording", recording);
73 |
74 | const [state, dispatch] = recording;
75 |
76 | useEffect(() => loom.subscribe(dispatch), []);
77 |
78 | useTransitionEffect(state, "NOT_CONFIGURED", ({ apiKey, buttonId }) => {
79 | loom.configure(apiKey, buttonId);
80 | });
81 |
82 | useTransitionEffect(state, "READY", "LOOM:INSERT", (_, { video }) => {
83 | loom.openVideo(video);
84 | });
85 |
86 | return recording;
87 | };
88 |
--------------------------------------------------------------------------------
/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/dashboard/useDashboard.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { act } from "@testing-library/react";
3 |
4 | import { createStorage } from "../../environments/storage/test";
5 | import { createAuthentication } from "../../environments/authentication/test";
6 | import { createTestEnvironment } from "../../environments/test";
7 | import { DashboardState, useDashboard } from "./useDashboard";
8 | import { EnvironmentProvider } from "../../environment-interface";
9 | import { renderReducer } from "react-states/test";
10 |
11 | describe("Dashboard", () => {
12 | test("Should go to PREVIEWS_LOADED when mounting and successfully downloading previews", () => {
13 | const environment = createTestEnvironment();
14 |
15 | const [state] = renderReducer(
16 | () => [useDashboard(), () => {}],
17 | (UseDashboard) => (
18 |
19 |
20 |
21 | )
22 | );
23 |
24 | const mockedPreviews = [
25 | {
26 | user: {
27 | avatarUrl: "",
28 | name: "Kate",
29 | uid: "123",
30 | },
31 | metadata: {
32 | author: "Kate",
33 | id: "456",
34 | title: "Test",
35 | last_updated: new Date(),
36 | },
37 | },
38 | ];
39 |
40 | expect(environment.storage.fetchPreviews).toBeCalled();
41 |
42 | act(() => {
43 | environment.storage.emit({
44 | type: "STORAGE:FETCH_PREVIEWS_SUCCESS",
45 | excalidraws: mockedPreviews,
46 | });
47 | });
48 |
49 | expect(state).toEqual({
50 | state: "PREVIEWS_LOADED",
51 | excalidraws: mockedPreviews,
52 | });
53 | });
54 | test("Should go to PREVIEWS_ERROR when mounting and unsuccessfully downloading previews", () => {
55 | const environment = createTestEnvironment();
56 | const [state] = renderReducer(
57 | () => [useDashboard(), () => {}],
58 | (UseDashboard) => (
59 |
60 |
61 |
62 | )
63 | );
64 |
65 | expect(environment.storage.fetchPreviews).toBeCalled();
66 |
67 | act(() => {
68 | environment.storage.emit({
69 | type: "STORAGE:FETCH_PREVIEWS_ERROR",
70 | error: "Unable to download",
71 | });
72 | });
73 |
74 | expect(state).toEqual({
75 | state: "PREVIEWS_ERROR",
76 | error: "Unable to download",
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/pages/useAuth.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | Dispatch,
4 | useContext,
5 | useEffect,
6 | useReducer,
7 | } from "react";
8 | import { PickState, transition, useStateEffect } from "react-states";
9 |
10 | import { useDevtools } from "react-states";
11 | import { useEnvironment } from "../environment-interface";
12 | import {
13 | AuthenticationEvent,
14 | User,
15 | } from "../environment-interface/authentication";
16 |
17 | export type AuthState =
18 | | {
19 | state: "CHECKING_AUTHENTICATION";
20 | }
21 | | {
22 | state: "UNAUTHENTICATED";
23 | }
24 | | {
25 | state: "SIGNING_IN";
26 | }
27 | | {
28 | state: "AUTHENTICATED";
29 | user: User;
30 | loomApiKey: string | null;
31 | }
32 | | {
33 | state: "ERROR";
34 | error: string;
35 | };
36 |
37 | export type AuthAction = {
38 | type: "SIGN_IN";
39 | };
40 |
41 | const reducer = (state: AuthState, action: AuthAction | AuthenticationEvent) =>
42 | transition(state, action, {
43 | CHECKING_AUTHENTICATION: {
44 | "AUTHENTICATION:AUTHENTICATED": (_, { user, loomApiKey }): AuthState => ({
45 | state: "AUTHENTICATED",
46 | user,
47 | loomApiKey,
48 | }),
49 | "AUTHENTICATION:UNAUTHENTICATED": (): AuthState => ({
50 | state: "UNAUTHENTICATED",
51 | }),
52 | },
53 | UNAUTHENTICATED: {
54 | SIGN_IN: (): AuthState => ({ state: "SIGNING_IN" }),
55 | },
56 | SIGNING_IN: {
57 | "AUTHENTICATION:AUTHENTICATED": (_, { user, loomApiKey }): AuthState => ({
58 | state: "AUTHENTICATED",
59 | user,
60 | loomApiKey,
61 | }),
62 | "AUTHENTICATION:SIGN_IN_ERROR": (_, { error }): AuthState => ({
63 | state: "ERROR",
64 | error,
65 | }),
66 | },
67 | AUTHENTICATED: {},
68 | ERROR: {},
69 | });
70 |
71 | const authenticatedAuthContext = createContext(
72 | null as unknown as PickState
73 | );
74 |
75 | export const AuthenticatedAuthProvider: React.FC<{
76 | auth: PickState;
77 | }> = ({ auth, children }) => (
78 |
79 | {children}
80 |
81 | );
82 |
83 | export const useAuthenticatedAuth = () => useContext(authenticatedAuthContext);
84 |
85 | export const useAuth = (
86 | initialState: AuthState = {
87 | state: "CHECKING_AUTHENTICATION",
88 | }
89 | ): [AuthState, Dispatch] => {
90 | const { authentication } = useEnvironment();
91 | const auth = useReducer(reducer, initialState);
92 |
93 | useDevtools("auth", auth);
94 |
95 | const [state, dispatch] = auth;
96 |
97 | useEffect(() => authentication.subscribe(dispatch), []);
98 |
99 | useStateEffect(state, "SIGNING_IN", () => authentication.signIn());
100 |
101 | return auth;
102 | };
103 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/ExcalidrawCanvas.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { getSceneVersion } from "@excalidraw/excalidraw";
3 |
4 | import { getChangedData } from "../../utils";
5 | import {
6 | ExcalidrawData,
7 | ExcalidrawElement,
8 | } from "../../environment-interface/storage";
9 | import { useEnvironment } from "../../environment-interface";
10 |
11 | export type ResolvablePromise = Promise & {
12 | resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
13 | reject: (error: Error) => void;
14 | };
15 |
16 | function resolvablePromise() {
17 | let resolve!: any;
18 | let reject!: any;
19 | const promise = new Promise((_resolve, _reject) => {
20 | resolve = _resolve;
21 | reject = _reject;
22 | });
23 | (promise as any).resolve = resolve;
24 | (promise as any).reject = reject;
25 | return promise as ResolvablePromise;
26 | }
27 |
28 | export const ExcalidrawCanvas = React.memo(
29 | ({
30 | data,
31 | onChange,
32 | onInitialized,
33 | readOnly,
34 | }: {
35 | data: ExcalidrawData;
36 | onChange: (elements: readonly ExcalidrawElement[], appState: any) => void;
37 | onInitialized: () => void;
38 | readOnly: boolean;
39 | }) => {
40 | const { storage } = useEnvironment();
41 | const excalidrawRef = useRef({
42 | readyPromise: resolvablePromise(),
43 | });
44 | const [Comp, setComp] = useState | null>(null);
45 |
46 | useEffect(() => {
47 | import("@excalidraw/excalidraw").then((comp) => {
48 | setComp(comp.Excalidraw);
49 | });
50 | }, [Comp]);
51 |
52 | const excalidrawWrapperRef = useRef(null);
53 |
54 | useEffect(() => {
55 | excalidrawRef.current.readyPromise.then(onInitialized);
56 | }, []);
57 |
58 | useEffect(
59 | () =>
60 | storage.subscribe((event) => {
61 | if (event.type === "STORAGE:EXCALIDRAW_DATA_UPDATE") {
62 | excalidrawRef.current.readyPromise.then(
63 | ({ getSceneElementsIncludingDeleted, getAppState }: any) => {
64 | const currentElements = getSceneElementsIncludingDeleted();
65 | const changedData = getChangedData(event.data, {
66 | appState: getAppState(),
67 | elements: currentElements,
68 | version: getSceneVersion(currentElements),
69 | });
70 |
71 | if (changedData) {
72 | excalidrawRef.current.updateScene(changedData);
73 | }
74 | },
75 | );
76 | }
77 | }),
78 | [],
79 | );
80 |
81 | return (
82 |
83 | {Comp ? (
84 |
90 | ) : null}
91 |
92 | );
93 | },
94 | );
95 |
--------------------------------------------------------------------------------
/src/environment-interface/storage.ts:
--------------------------------------------------------------------------------
1 | import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
2 | import { AppState } from "@excalidraw/excalidraw/types/types";
3 | import { TEmit, TSubscribe } from "react-states";
4 |
5 |
6 | export type StorageError = {
7 | type: "ERROR";
8 | data: string;
9 | };
10 |
11 | export type { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
12 |
13 | export type ExcalidrawMetadata = {
14 | id: string;
15 | author: string;
16 | last_updated: Date;
17 | title: string;
18 | };
19 |
20 | export type ExcalidrawData = {
21 | elements: readonly ExcalidrawElement[];
22 | appState: AppState;
23 | version: number;
24 | };
25 |
26 | export type ExcalidrawPreview = {
27 | metadata: ExcalidrawMetadata;
28 | user: {
29 | uid: string;
30 | name: string;
31 | avatarUrl: string | null;
32 | };
33 | };
34 |
35 | export type ExcalidrawPreviews = ExcalidrawPreview[];
36 |
37 | export type StorageEvent =
38 | | {
39 | type: "STORAGE:FETCH_EXCALIDRAW_SUCCESS";
40 | metadata: ExcalidrawMetadata;
41 | data: ExcalidrawData;
42 | image: Blob;
43 | }
44 | | {
45 | type: "STORAGE:FETCH_EXCALIDRAW_ERROR";
46 | error: string;
47 | }
48 | | {
49 | type: "STORAGE:EXCALIDRAW_DATA_UPDATE";
50 | id: string;
51 | data: ExcalidrawData;
52 | }
53 | | {
54 | type: "STORAGE:CREATE_EXCALIDRAW_SUCCESS";
55 | id: string;
56 | }
57 | | {
58 | type: "STORAGE:CREATE_EXCALIDRAW_ERROR";
59 | error: string;
60 | }
61 | | {
62 | type: "STORAGE:SAVE_EXCALIDRAW_SUCCESS";
63 | metadata: ExcalidrawMetadata;
64 | image: Blob;
65 | }
66 | | {
67 | type: "STORAGE:SAVE_EXCALIDRAW_ERROR";
68 | error: string;
69 | }
70 | | {
71 | type: "STORAGE:SAVE_EXCALIDRAW_OLD_VERSION";
72 | }
73 | | {
74 | type: "STORAGE:FETCH_PREVIEWS_SUCCESS";
75 | excalidraws: ExcalidrawPreviews;
76 | }
77 | | {
78 | type: "STORAGE:FETCH_PREVIEWS_ERROR";
79 | error: string;
80 | }
81 | | {
82 | type: "STORAGE:FETCH_USER_PREVIEWS_SUCCESS";
83 | excalidraws: ExcalidrawPreviews;
84 | }
85 | | {
86 | type: "STORAGE:FETCH_USER_PREVIEWS_ERROR";
87 | error: string;
88 | }
89 | | {
90 | type: "STORAGE:IMAGE_SRC_SUCCESS";
91 | id: string;
92 | src: string;
93 | }
94 | | {
95 | type: "STORAGE:IMAGE_SRC_ERROR";
96 | id: string;
97 | error: string;
98 | }
99 | | {
100 | type: "STORAGE:SAVE_TITLE_SUCCESS";
101 | id: string;
102 | title: string;
103 | }
104 | | {
105 | type: "STORAGE:SAVE_TITLE_ERROR";
106 | id: string;
107 | title: string;
108 | error: string;
109 | };
110 |
111 | export interface Storage {
112 | subscribe: TSubscribe
113 | emit: TEmit
114 | createExcalidraw(userId: string): void;
115 | fetchExcalidraw(userId: string, id: string): void;
116 | fetchPreviews(): void;
117 | fetchUserPreviews(uid: string): void;
118 | saveExcalidraw(userId: string, id: string, data: ExcalidrawData): void;
119 | getImageSrc(userId: string, id: string): void;
120 | saveTitle(userId: string, id: string, title: string): void;
121 | }
122 |
--------------------------------------------------------------------------------
/src/pages/dashboard/ExcalidrawPreview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { match, transition, useStateEffect } from "react-states";
3 | import {
4 | ExcalidrawMetadata,
5 | StorageEvent,
6 | } from "../../environment-interface/storage";
7 | import formatDistanceToNow from "date-fns/formatDistanceToNow";
8 |
9 | import { Link } from "react-router-dom";
10 | import { User } from "../../environment-interface/authentication";
11 | import { useEnvironment } from "../../environment-interface";
12 |
13 | type State =
14 | | {
15 | state: "LOADING_PREVIEW";
16 | id: string;
17 | }
18 | | {
19 | state: "PREVIEW_LOADED";
20 | src: string;
21 | }
22 | | {
23 | state: "LOADING_ERROR";
24 | error: string;
25 | };
26 |
27 | const reducer = (state: State, action: StorageEvent) =>
28 | transition(state, action, {
29 | LOADING_PREVIEW: {
30 | "STORAGE:IMAGE_SRC_SUCCESS": (state, { id, src }): State =>
31 | id === state.id
32 | ? {
33 | state: "PREVIEW_LOADED",
34 | src,
35 | }
36 | : state,
37 | "STORAGE:IMAGE_SRC_ERROR": (state, { id, error }): State =>
38 | state.id === id
39 | ? {
40 | state: "LOADING_ERROR",
41 | error,
42 | }
43 | : state,
44 | },
45 | PREVIEW_LOADED: {},
46 | LOADING_ERROR: {},
47 | });
48 |
49 | export const ExcalidrawPreview = ({
50 | user,
51 | metadata,
52 | }: {
53 | user: User;
54 | metadata: ExcalidrawMetadata;
55 | }) => {
56 | const { storage } = useEnvironment();
57 | const [preview, dispatch] = React.useReducer(reducer, {
58 | state: "LOADING_PREVIEW",
59 | id: metadata.id,
60 | });
61 |
62 | React.useEffect(() => storage.subscribe(dispatch), []);
63 |
64 | useStateEffect(preview, "LOADING_PREVIEW", () => {
65 | storage.getImageSrc(user.uid, metadata.id);
66 | });
67 |
68 | const renderPreview = (background: string) => (
69 |
70 | );
71 |
72 | return (
73 |
74 |
75 |
76 | {user.avatarUrl ? (
77 |

82 | ) : null}
83 |
84 |
85 | {metadata.title || ""}
86 |
87 |
88 |
89 | {match(preview, {
90 | LOADING_PREVIEW: () =>
,
91 | PREVIEW_LOADED: ({ src }) =>
92 | renderPreview(`center / contain no-repeat url(${src})`),
93 | LOADING_ERROR: () => renderPreview("#FFF"),
94 | })}
95 |
96 |
97 |
98 | {formatDistanceToNow(metadata.last_updated)} ago
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/src/pages/dashboard/useNavigation.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, Dispatch, useEffect, useReducer } from "react";
2 | import { StorageEvent } from "../../environment-interface/storage";
3 | import { useEnvironment } from "../../environment-interface";
4 | import {
5 | PickState,
6 | transition,
7 | useDevtools,
8 | useStateEffect,
9 | } from "react-states";
10 |
11 | import { useAuthenticatedAuth } from "../useAuth";
12 |
13 | export type NavigationState =
14 | | {
15 | state: "ALL_EXCALIDRAWS";
16 | }
17 | | {
18 | state: "USER_EXCALIDRAWS";
19 | }
20 | | {
21 | state: "CREATING_EXCALIDRAW";
22 | }
23 | | {
24 | state: "EXCALIDRAW_CREATED";
25 | id: string;
26 | }
27 | | {
28 | state: "CREATE_EXCALIDRAW_ERROR";
29 | error: string;
30 | };
31 |
32 | export type NavigationAction =
33 | | {
34 | type: "CREATE_EXCALIDRAW";
35 | }
36 | | {
37 | type: "SHOW_ALL_EXCALIDRAWS";
38 | }
39 | | {
40 | type: "SHOW_MY_EXCALIDRAWSS";
41 | };
42 |
43 | const reducer = (
44 | state: NavigationState,
45 | action: NavigationAction | StorageEvent
46 | ) =>
47 | transition(state, action, {
48 | ALL_EXCALIDRAWS: {
49 | CREATE_EXCALIDRAW: (state): NavigationState => ({
50 | ...state,
51 | state: "CREATING_EXCALIDRAW",
52 | }),
53 | SHOW_MY_EXCALIDRAWSS: (): NavigationState => ({
54 | state: "USER_EXCALIDRAWS",
55 | }),
56 | },
57 | USER_EXCALIDRAWS: {
58 | CREATE_EXCALIDRAW: (state): NavigationState => ({
59 | ...state,
60 | state: "CREATING_EXCALIDRAW",
61 | }),
62 | SHOW_ALL_EXCALIDRAWS: (): NavigationState => ({
63 | state: "ALL_EXCALIDRAWS",
64 | }),
65 | },
66 | CREATING_EXCALIDRAW: {
67 | "STORAGE:CREATE_EXCALIDRAW_SUCCESS": (
68 | state,
69 | { id }
70 | ): NavigationState => ({
71 | ...state,
72 | state: "EXCALIDRAW_CREATED",
73 | id,
74 | }),
75 | "STORAGE:CREATE_EXCALIDRAW_ERROR": (
76 | state,
77 | { error }
78 | ): NavigationState => ({
79 | ...state,
80 | state: "CREATE_EXCALIDRAW_ERROR",
81 | error,
82 | }),
83 | },
84 | CREATE_EXCALIDRAW_ERROR: {
85 | CREATE_EXCALIDRAW: (state): NavigationState => ({
86 | ...state,
87 | state: "CREATING_EXCALIDRAW",
88 | }),
89 | },
90 | EXCALIDRAW_CREATED: {},
91 | });
92 |
93 | export const useNavigation = ({
94 | initialState = {
95 | state: "ALL_EXCALIDRAWS",
96 | },
97 | navigate,
98 | }: {
99 | navigate: (url: string) => void;
100 | initialState?: NavigationState;
101 | }): [NavigationState, Dispatch] => {
102 | const auth = useAuthenticatedAuth();
103 | const { storage } = useEnvironment();
104 | const navigationReducer = useReducer(reducer, initialState);
105 |
106 | useDevtools("navigation", navigationReducer);
107 |
108 | const [state, dispatch] = navigationReducer;
109 |
110 | useEffect(() => storage.subscribe(dispatch), []);
111 |
112 | useStateEffect(state, "CREATING_EXCALIDRAW", () =>
113 | storage.createExcalidraw(auth.user.uid)
114 | );
115 |
116 | useStateEffect(state, "EXCALIDRAW_CREATED", ({ id }) => {
117 | navigate(`/${auth.user.uid}/${id}`);
118 | });
119 |
120 | useStateEffect(state, "ALL_EXCALIDRAWS", () => {
121 | navigate("/");
122 | });
123 |
124 | useStateEffect(state, "USER_EXCALIDRAWS", () => {
125 | navigate(`/${auth.user.uid}`);
126 | });
127 |
128 | return navigationReducer;
129 | };
130 |
--------------------------------------------------------------------------------
/src/pages/dashboard/useNavigation.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { act } from "@testing-library/react";
3 |
4 | import { createStorage } from "../../environments/storage/test";
5 | import { createAuthentication } from "../../environments/authentication/test";
6 | import { createMemoryHistory } from "history";
7 | import { Router } from "react-router-dom";
8 | import { renderReducer } from "react-states/test";
9 | import { createTestEnvironment } from "../../environments/test";
10 | import { NavigationState, useNavigation } from "./useNavigation";
11 | import { EnvironmentProvider } from "../../environment-interface";
12 | import { AuthenticatedAuthProvider } from "../useAuth";
13 |
14 | describe("Dashboard", () => {
15 | test("Should go to EXCALIDRAW_CREATED when creating a new Excalidraw successfully", () => {
16 | const environment = createTestEnvironment();
17 | const history = createMemoryHistory();
18 | const navigate = jest.fn();
19 | const [state, dispatch] = renderReducer(
20 | () =>
21 | useNavigation({
22 | navigate,
23 | }),
24 | (UseNavigation) => (
25 |
26 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | );
44 |
45 | act(() => {
46 | dispatch({ type: "CREATE_EXCALIDRAW" });
47 | });
48 |
49 | expect(state).toEqual({
50 | state: "CREATING_EXCALIDRAW",
51 | });
52 |
53 | expect(environment.storage.createExcalidraw).toBeCalled();
54 |
55 | act(() => {
56 | environment.storage.emit({
57 | type: "STORAGE:CREATE_EXCALIDRAW_SUCCESS",
58 | id: "456",
59 | });
60 | });
61 |
62 | expect(state).toEqual({
63 | state: "EXCALIDRAW_CREATED",
64 | id: "456",
65 | });
66 | expect(history.entries[1].pathname).toBe("/123/456");
67 | });
68 | test("Should go to CREATE_EXCALIDRAW_ERROR when creating a new Excalidraw unsuccessfully", () => {
69 | const environment = createTestEnvironment();
70 |
71 | const navigate = jest.fn();
72 | const [state, dispatch] = renderReducer(
73 | () =>
74 | useNavigation({
75 | navigate: navigate,
76 | }),
77 | (UseNavigation) => (
78 |
79 |
90 |
91 |
92 |
93 | )
94 | );
95 |
96 | act(() => {
97 | dispatch({ type: "CREATE_EXCALIDRAW" });
98 | });
99 |
100 | expect(state).toEqual({
101 | state: "CREATING_EXCALIDRAW",
102 | });
103 |
104 | expect(environment.storage.createExcalidraw).toBeCalled();
105 |
106 | act(() => {
107 | environment.storage.emit({
108 | type: "STORAGE:CREATE_EXCALIDRAW_ERROR",
109 | error: "Could not create Excalidraw",
110 | });
111 | });
112 |
113 | expect(state).toEqual({
114 | state: "CREATE_EXCALIDRAW_ERROR",
115 | error: "Could not create Excalidraw",
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/useExcalidraw/reducer.ts:
--------------------------------------------------------------------------------
1 | import { ExcalidrawState, ExcalidrawAction, PrivateAction } from "./types";
2 | import { StorageEvent } from "../../../environment-interface/storage";
3 |
4 | import { transition } from "react-states";
5 | import { hasChangedExcalidraw } from "../../../utils";
6 |
7 | export const reducer = (
8 | state: ExcalidrawState,
9 | action: ExcalidrawAction | PrivateAction | StorageEvent
10 | ) =>
11 | transition(state, action, {
12 | LOADING: {
13 | "STORAGE:FETCH_EXCALIDRAW_SUCCESS": (
14 | _,
15 | { data, metadata, image }
16 | ): ExcalidrawState => ({
17 | state: "LOADED",
18 | data,
19 | metadata,
20 | image,
21 | clipboard: {
22 | state: "NOT_COPIED",
23 | },
24 | }),
25 | "STORAGE:FETCH_EXCALIDRAW_ERROR": (_, { error }): ExcalidrawState => ({
26 | state: "ERROR",
27 | error,
28 | }),
29 | },
30 | LOADED: {
31 | INITIALIZE_CANVAS_SUCCESS: (state): ExcalidrawState => ({
32 | ...state,
33 | state: "EDIT",
34 | }),
35 | },
36 | EDIT: {
37 | EXCALIDRAW_CHANGE: (state, { data }): ExcalidrawState =>
38 | hasChangedExcalidraw(state.data, data)
39 | ? {
40 | ...state,
41 | clipboard: {
42 | state: "NOT_COPIED",
43 | },
44 | state: "DIRTY",
45 | data,
46 | }
47 | : state,
48 | COPY_TO_CLIPBOARD: (state): ExcalidrawState => ({
49 | ...state,
50 | clipboard: {
51 | state: "COPIED",
52 | },
53 | }),
54 | "STORAGE:SAVE_TITLE_SUCCESS": (state, { title }): ExcalidrawState => ({
55 | ...state,
56 | metadata: {
57 | ...state.metadata,
58 | title,
59 | },
60 | }),
61 | SAVE_TITLE: (state, { title }): ExcalidrawState => ({
62 | ...state,
63 | }),
64 | },
65 | DIRTY: {
66 | SYNC: (state): ExcalidrawState => ({
67 | ...state,
68 | state: "SYNCING",
69 | }),
70 | EXCALIDRAW_CHANGE: (state, { data }): ExcalidrawState =>
71 | hasChangedExcalidraw(state.data, data)
72 | ? {
73 | ...state,
74 | state: "DIRTY",
75 | data,
76 | }
77 | : state,
78 | },
79 | SYNCING: {
80 | EXCALIDRAW_CHANGE: (state, { data }): ExcalidrawState =>
81 | hasChangedExcalidraw(state.data, data)
82 | ? {
83 | ...state,
84 | state: "SYNCING_DIRTY",
85 | data,
86 | }
87 | : state,
88 | "STORAGE:SAVE_EXCALIDRAW_SUCCESS": (
89 | state,
90 | { image, metadata }
91 | ): ExcalidrawState => ({
92 | ...state,
93 | state: "EDIT",
94 | metadata,
95 | image,
96 | }),
97 | "STORAGE:SAVE_EXCALIDRAW_ERROR": (_, { error }): ExcalidrawState => ({
98 | state: "ERROR",
99 | error,
100 | }),
101 | "STORAGE:SAVE_EXCALIDRAW_OLD_VERSION": (state): ExcalidrawState => ({
102 | ...state,
103 | state: "EDIT",
104 | }),
105 | },
106 | SYNCING_DIRTY: {
107 | EXCALIDRAW_CHANGE: (state, { data }): ExcalidrawState =>
108 | hasChangedExcalidraw(state.data, data)
109 | ? {
110 | ...state,
111 | state: "SYNCING_DIRTY",
112 | data,
113 | }
114 | : state,
115 | "STORAGE:SAVE_EXCALIDRAW_SUCCESS": (
116 | state,
117 | { metadata }
118 | ): ExcalidrawState => ({
119 | ...state,
120 | metadata,
121 | state: "DIRTY",
122 | }),
123 | "STORAGE:SAVE_EXCALIDRAW_ERROR": (state): ExcalidrawState => ({
124 | ...state,
125 | state: "DIRTY",
126 | }),
127 | "STORAGE:SAVE_EXCALIDRAW_OLD_VERSION": (state): ExcalidrawState => ({
128 | ...state,
129 | state: "DIRTY",
130 | }),
131 | },
132 | ERROR: {},
133 | });
134 |
--------------------------------------------------------------------------------
/src/pages/useAuth.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { renderReducer } from "react-states/test";
3 | import { act } from "@testing-library/react";
4 | import { useAuth, AuthState } from "./useAuth";
5 | import { createTestEnvironment } from "../environments/test";
6 | import { EnvironmentProvider } from "../environment-interface";
7 |
8 | describe("Auth", () => {
9 | test("Should go to AUTHENTICATED when mounted and is logged in", () => {
10 | const environment = createTestEnvironment();
11 | const [state] = renderReducer(
12 | () => useAuth(),
13 | (UseAuth) => (
14 |
15 |
16 |
17 | )
18 | );
19 |
20 | act(() => {
21 | environment.authentication.emit({
22 | type: "AUTHENTICATION:AUTHENTICATED",
23 | user: {
24 | avatarUrl: "",
25 | name: "Karen",
26 | uid: "123",
27 | },
28 | loomApiKey: "",
29 | });
30 | });
31 |
32 | expect(state).toEqual({
33 | state: "AUTHENTICATED",
34 | user: {
35 | avatarUrl: "",
36 | name: "Karen",
37 | uid: "123",
38 | },
39 | loomApiKey: "",
40 | });
41 | });
42 | test("Should go to UNAUTHENTICATED when mounted and is not logged in", () => {
43 | const environment = createTestEnvironment();
44 | const [state] = renderReducer(
45 | () => useAuth(),
46 | (UseAuth) => (
47 |
48 |
49 |
50 | )
51 | );
52 |
53 | act(() => {
54 | environment.authentication.emit({
55 | type: "AUTHENTICATION:UNAUTHENTICATED",
56 | });
57 | });
58 |
59 | expect(state).toEqual({
60 | state: "UNAUTHENTICATED",
61 | });
62 | });
63 | test("Should go to AUTHENTICATED when signing in successfully", () => {
64 | const environment = createTestEnvironment();
65 |
66 | const [state, dispatch] = renderReducer(
67 | () =>
68 | useAuth({
69 | state: "UNAUTHENTICATED",
70 | }),
71 | (UseAuth) => (
72 |
73 |
74 |
75 | )
76 | );
77 |
78 | act(() => {
79 | dispatch({
80 | type: "SIGN_IN",
81 | });
82 | });
83 |
84 | expect(state.state).toEqual({
85 | state: "SIGNING_IN",
86 | });
87 | expect(environment.authentication.signIn).toBeCalled();
88 |
89 | act(() => {
90 | environment.authentication.emit({
91 | type: "AUTHENTICATION:AUTHENTICATED",
92 | user: {
93 | avatarUrl: "",
94 | name: "Karen",
95 | uid: "123",
96 | },
97 | loomApiKey: "",
98 | });
99 | });
100 |
101 | expect(state).toEqual({
102 | state: "AUTHENTICATED",
103 | user: {
104 | avatarUrl: "",
105 | name: "Karen",
106 | uid: "123",
107 | },
108 | loomApiKey: "",
109 | });
110 | });
111 | test("Should go to ERROR when signing in unsuccsessfully", () => {
112 | const environment = createTestEnvironment();
113 | const [state, dispatch] = renderReducer(
114 | () =>
115 | useAuth({
116 | state: "UNAUTHENTICATED",
117 | }),
118 | (UseAuth) => (
119 |
120 |
121 |
122 | )
123 | );
124 |
125 | act(() => {
126 | dispatch({
127 | type: "SIGN_IN",
128 | });
129 | });
130 |
131 | expect(state.state).toBe("SIGNING_IN");
132 | expect(environment.authentication.signIn).toBeCalled();
133 |
134 | act(() => {
135 | environment.authentication.emit({
136 | type: "AUTHENTICATION:SIGN_IN_ERROR",
137 | error: "Something bad happened",
138 | });
139 | });
140 |
141 | expect(state).toEqual({
142 | state: "ERROR",
143 | error: "Something bad happened",
144 | });
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Welcome to Firebase Hosting
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
40 |
41 |
42 |
43 |
Welcome
44 |
Firebase Hosting Setup Complete
45 |
You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!
46 |
Open Hosting Documentation
47 |
48 | Firebase SDK Loading…
49 |
50 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/pages/dashboard/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { match } from "react-states";
4 |
5 | import { useNavigation } from "./useNavigation";
6 |
7 | export const Navigation: React.FC = () => {
8 | const navigate = useNavigate();
9 | const [state, dispatch] = useNavigation({
10 | navigate: (path) => {
11 | navigate(path);
12 | },
13 | });
14 |
15 | return (
16 |
17 |
18 |
19 |
42 |
43 |
44 | CodeSandbox Excalidraw
45 |
46 |
47 |
48 |
49 |
64 |
79 |
80 |
81 |
90 |
91 |
92 |
93 | );
94 | };
95 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/Excalidraw.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, useMemo, useState } from "react";
2 | import debounce from "lodash.debounce";
3 | import { getSceneVersion } from "@excalidraw/excalidraw";
4 | import { PickState, match } from "react-states";
5 | import { ExcalidrawCanvas } from "./ExcalidrawCanvas";
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import {
8 | faCheck,
9 | faClipboard,
10 | faVideo,
11 | } from "@fortawesome/free-solid-svg-icons";
12 |
13 | import { useExcalidraw } from "./useExcalidraw";
14 | import { ExcalidrawAction, ExcalidrawState } from "./useExcalidraw/types";
15 | import { useRecording } from "./useRecording";
16 | import { useAuthenticatedAuth } from "../useAuth";
17 |
18 | type EditExcalidrawState = PickState<
19 | ExcalidrawState,
20 | "LOADED" | "EDIT" | "SYNCING" | "DIRTY" | "SYNCING_DIRTY"
21 | >;
22 |
23 | type EditExcalidrawReducer = [EditExcalidrawState, Dispatch];
24 |
25 | const EditExcalidraw = ({
26 | excalidrawReducer: [state, dispatch],
27 | }: {
28 | excalidrawReducer: EditExcalidrawReducer;
29 | }) => {
30 | const auth = useAuthenticatedAuth();
31 |
32 | const [title, setTitle] = useState(state.metadata.title || "");
33 |
34 | const onChange = useMemo(
35 | () =>
36 | debounce((elements, appState) => {
37 | dispatch({
38 | type: "EXCALIDRAW_CHANGE",
39 | data: {
40 | elements,
41 | appState,
42 | version: getSceneVersion(elements),
43 | },
44 | });
45 | }, 100),
46 | [],
47 | );
48 |
49 | const copyToClipboard = () => {
50 | dispatch({ type: "COPY_TO_CLIPBOARD" });
51 | };
52 |
53 | const variants = {
54 | default: () => ({
55 | className:
56 | "text-gray-500 bg-gray-50 hover:bg-gray-100 focus:ring-gray-50",
57 | content: ,
58 | onClick: copyToClipboard,
59 | }),
60 | active: () => ({
61 | className:
62 | "text-green-500 bg-green-50 hover:bg-green-100 focus:ring-green-50",
63 | content: ,
64 | onClick: undefined,
65 | }),
66 | loading: () => ({
67 | className:
68 | "opacity-50 text-gray-500 bg-gray-50 hover:bg-gray-100 focus:ring-gray-50",
69 | content: ,
70 | onClick: undefined,
71 | }),
72 | };
73 |
74 | const variant = match(state, {
75 | DIRTY: variants.loading,
76 | LOADED: variants.loading,
77 | SYNCING: variants.loading,
78 | SYNCING_DIRTY: variants.loading,
79 | EDIT: () =>
80 | match(state.clipboard, {
81 | COPIED: variants.active,
82 | NOT_COPIED: variants.default,
83 | }),
84 | });
85 |
86 | return (
87 |
88 |
{
93 | dispatch({ type: "INITIALIZE_CANVAS_SUCCESS" });
94 | }}
95 | />
96 |
97 |
98 |
{
103 | setTitle(event.target.value);
104 | }}
105 | onKeyDown={(event) => {
106 | if (event.key === "Enter") {
107 | dispatch({
108 | type: "SAVE_TITLE",
109 | title,
110 | });
111 | }
112 | }}
113 | className="focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 sm:text-sm border-gray-300 rounded-md h-10"
114 | placeholder="Title..."
115 | />
116 | {!title || title === state.metadata.title ? null : (
117 |
{
120 | dispatch({
121 | type: "SAVE_TITLE",
122 | title,
123 | });
124 | }}
125 | >
126 |
127 |
128 | )}
129 |
130 |
136 |
137 |
138 | );
139 | };
140 |
141 | export const Excalidraw: React.FC<{ id: string; userId: string }> = ({
142 | id,
143 | userId,
144 | }) => {
145 | const [state, dispatch] = useExcalidraw({
146 | id,
147 | userId,
148 | });
149 |
150 | const renderExcalidraw = (state: EditExcalidrawState) => (
151 |
152 | );
153 |
154 | return match(state, {
155 | LOADING: () => (
156 |
159 | ),
160 | ERROR: ({ error }) => (
161 |
162 |
OMG, error, {error}
163 |
164 | ),
165 | LOADED: renderExcalidraw,
166 | EDIT: renderExcalidraw,
167 | SYNCING: renderExcalidraw,
168 | DIRTY: renderExcalidraw,
169 | SYNCING_DIRTY: renderExcalidraw,
170 | });
171 | };
172 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from "@excalidraw/excalidraw/types/types";
2 | import {
3 | ExcalidrawData,
4 | ExcalidrawElement,
5 | } from "./providers/Excalidraw/types";
6 |
7 | export const hasChangedExcalidraw = (
8 | oldData: ExcalidrawData,
9 | newData: ExcalidrawData
10 | ) => {
11 | return (
12 | oldData.version !== newData.version ||
13 | oldData.appState.viewBackgroundColor !==
14 | newData.appState.viewBackgroundColor
15 | );
16 | };
17 |
18 | export const getChangedData = (
19 | newData: ExcalidrawData,
20 | oldData: ExcalidrawData
21 | ): ExcalidrawData => {
22 | if (newData.version === oldData.version) {
23 | return oldData;
24 | }
25 |
26 | return {
27 | ...newData,
28 | elements: reconcileElements(
29 | oldData.elements,
30 | newData.elements,
31 | oldData.appState
32 | ),
33 | };
34 | };
35 |
36 | export type ReconciledElements = readonly ExcalidrawElement[] & {
37 | _brand: "reconciledElements";
38 | };
39 |
40 | export type BroadcastedExcalidrawElement = ExcalidrawElement & {
41 | parent?: string;
42 | };
43 |
44 | const shouldDiscardRemoteElement = (
45 | localAppState: AppState,
46 | local: ExcalidrawElement | undefined,
47 | remote: BroadcastedExcalidrawElement
48 | ): boolean => {
49 | if (
50 | local &&
51 | // local element is being edited
52 | (local.id === localAppState.editingElement?.id ||
53 | local.id === localAppState.resizingElement?.id ||
54 | local.id === localAppState.draggingElement?.id ||
55 | // local element is newer
56 | local.version > remote.version ||
57 | // resolve conflicting edits deterministically by taking the one with
58 | // the lowest versionNonce
59 | (local.version === remote.version &&
60 | local.versionNonce < remote.versionNonce))
61 | ) {
62 | return true;
63 | }
64 | return false;
65 | };
66 |
67 | const getElementsMapWithIndex = (
68 | elements: readonly T[]
69 | ) =>
70 | elements.reduce(
71 | (
72 | acc: {
73 | [key: string]: [element: T, index: number] | undefined;
74 | },
75 | element: T,
76 | idx
77 | ) => {
78 | acc[element.id] = [element, idx];
79 | return acc;
80 | },
81 | {}
82 | );
83 |
84 | export const reconcileElements = (
85 | localElements: readonly ExcalidrawElement[],
86 | remoteElements: readonly BroadcastedExcalidrawElement[],
87 | localAppState: AppState
88 | ): ReconciledElements => {
89 | const localElementsData =
90 | getElementsMapWithIndex(localElements);
91 |
92 | const reconciledElements: ExcalidrawElement[] = localElements.slice();
93 |
94 | const duplicates = new WeakMap();
95 |
96 | let cursor = 0;
97 | let offset = 0;
98 |
99 | let remoteElementIdx = -1;
100 | for (const remoteElement of remoteElements) {
101 | remoteElementIdx++;
102 |
103 | const local = localElementsData[remoteElement.id];
104 |
105 | if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
106 | if (remoteElement.parent) {
107 | delete remoteElement.parent;
108 | }
109 |
110 | continue;
111 | }
112 |
113 | if (local) {
114 | // mark for removal since it'll be replaced with the remote element
115 | duplicates.set(local[0], true);
116 | }
117 |
118 | // parent may not be defined in case the remote client is running an older
119 | // excalidraw version
120 | const parent =
121 | remoteElement.parent || remoteElements[remoteElementIdx - 1]?.id || null;
122 |
123 | if (parent != null) {
124 | delete remoteElement.parent;
125 |
126 | // ^ indicates the element is the first in elements array
127 | if (parent === "^") {
128 | offset++;
129 | if (cursor === 0) {
130 | reconciledElements.unshift(remoteElement);
131 | localElementsData[remoteElement.id] = [
132 | remoteElement,
133 | cursor - offset,
134 | ];
135 | } else {
136 | reconciledElements.splice(cursor + 1, 0, remoteElement);
137 | localElementsData[remoteElement.id] = [
138 | remoteElement,
139 | cursor + 1 - offset,
140 | ];
141 | cursor++;
142 | }
143 | } else {
144 | let idx = localElementsData[parent]
145 | ? localElementsData[parent]![1]
146 | : null;
147 | if (idx != null) {
148 | idx += offset;
149 | }
150 | if (idx != null && idx >= cursor) {
151 | reconciledElements.splice(idx + 1, 0, remoteElement);
152 | offset++;
153 | localElementsData[remoteElement.id] = [
154 | remoteElement,
155 | idx + 1 - offset,
156 | ];
157 | cursor = idx + 1;
158 | } else if (idx != null) {
159 | reconciledElements.splice(cursor + 1, 0, remoteElement);
160 | offset++;
161 | localElementsData[remoteElement.id] = [
162 | remoteElement,
163 | cursor + 1 - offset,
164 | ];
165 | cursor++;
166 | } else {
167 | reconciledElements.push(remoteElement);
168 | localElementsData[remoteElement.id] = [
169 | remoteElement,
170 | reconciledElements.length - 1 - offset,
171 | ];
172 | }
173 | }
174 | // no parent z-index information, local element exists → replace in place
175 | } else if (local) {
176 | reconciledElements[local[1]] = remoteElement;
177 | localElementsData[remoteElement.id] = [remoteElement, local[1]];
178 | // otherwise push to the end
179 | } else {
180 | reconciledElements.push(remoteElement);
181 | localElementsData[remoteElement.id] = [
182 | remoteElement,
183 | reconciledElements.length - 1 - offset,
184 | ];
185 | }
186 | }
187 |
188 | const ret: readonly ExcalidrawElement[] = reconciledElements.filter(
189 | (element) => !duplicates.has(element)
190 | );
191 |
192 | return ret as ReconciledElements;
193 | };
194 |
--------------------------------------------------------------------------------
/src/pages/excalidraw/useExcalidraw/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { act, waitFor } from "@testing-library/react";
3 |
4 | import { createTestEnvironment } from "../../../environments/test";
5 | import { useExcalidraw } from ".";
6 | import { EnvironmentProvider } from "../../../environment-interface";
7 | import { renderReducer } from "react-states/test";
8 | import {
9 | ExcalidrawData,
10 | ExcalidrawMetadata,
11 | } from "../../../environment-interface/storage";
12 | import { ExcalidrawState } from "./types";
13 |
14 | describe("Excalidraw", () => {
15 | test("Should go to EDIT when loaded excalidraw and canvas is ready", () => {
16 | const userId = "123";
17 | const id = "456";
18 | const environment = createTestEnvironment();
19 |
20 | const [state, dispatch] = renderReducer(
21 | () =>
22 | useExcalidraw({
23 | id,
24 | userId,
25 | }),
26 | (UseExcalidraw) => (
27 |
28 |
29 |
30 | )
31 | );
32 |
33 | const data: ExcalidrawData = {
34 | appState: { viewBackgroundColor: "#FFF" } as ExcalidrawData["appState"],
35 | elements: [],
36 | version: 0,
37 | };
38 | const image = {} as Blob;
39 | const metadata: ExcalidrawMetadata = {
40 | author: "123",
41 | id: "456",
42 | last_updated: new Date(),
43 | title: "Test",
44 | };
45 |
46 | act(() => {
47 | environment.storage.emit({
48 | type: "STORAGE:FETCH_EXCALIDRAW_SUCCESS",
49 | data,
50 | image,
51 | metadata,
52 | });
53 | });
54 |
55 | expect(state).toEqual({
56 | state: "LOADED",
57 | data,
58 | metadata,
59 | image,
60 | clipboard: {
61 | state: "NOT_COPIED",
62 | },
63 | });
64 |
65 | act(() => {
66 | dispatch({
67 | type: "INITIALIZE_CANVAS_SUCCESS",
68 | });
69 | });
70 |
71 | expect(state).toEqual({
72 | state: "EDIT",
73 | data,
74 | metadata,
75 | image,
76 | clipboard: {
77 | state: "NOT_COPIED",
78 | },
79 | });
80 | });
81 | test("Should go to SYNCING when excalidraw is changed", async () => {
82 | const userId = "123";
83 | const id = "456";
84 | const environment = createTestEnvironment();
85 |
86 | const image = {} as Blob;
87 | const metadata = {
88 | author: "123",
89 | id: "456",
90 | last_updated: new Date(),
91 | title: "Test",
92 | };
93 | const [excalidraw, send] = renderReducer(
94 | () =>
95 | useExcalidraw({
96 | id,
97 | userId,
98 | initialState: {
99 | state: "EDIT",
100 | data: {
101 | appState: {
102 | viewBackgroundColor: "#FFF",
103 | } as ExcalidrawData["appState"],
104 | elements: [],
105 | version: 0,
106 | },
107 | image,
108 | metadata,
109 | clipboard: {
110 | state: "NOT_COPIED",
111 | },
112 | },
113 | }),
114 | (UseExcalidraw) => (
115 |
116 |
117 |
118 | )
119 | );
120 |
121 | const newData: ExcalidrawData = {
122 | appState: { viewBackgroundColor: "#FFF" } as ExcalidrawData["appState"],
123 | elements: [{ id: "4", version: 0 } as ExcalidrawData["elements"][number]],
124 | version: 1,
125 | };
126 |
127 | act(() => {
128 | send({ type: "EXCALIDRAW_CHANGE", data: newData });
129 | });
130 |
131 | expect(excalidraw).toEqual({
132 | state: "DIRTY",
133 | data: newData,
134 | metadata,
135 | image,
136 | clipboard: {
137 | state: "NOT_COPIED",
138 | },
139 | });
140 |
141 | await waitFor(() =>
142 | expect(excalidraw).toEqual({
143 | state: "SYNCING",
144 | data: newData,
145 | metadata,
146 | image,
147 | clipboard: {
148 | state: "NOT_COPIED",
149 | },
150 | })
151 | );
152 | });
153 | test("Should go to DIRTY when excalidraw is changed during SYNCING that is successful", async () => {
154 | const userId = "123";
155 | const id = "456";
156 | const environment = createTestEnvironment();
157 |
158 | const image = {} as Blob;
159 | const metadata = {
160 | author: "123",
161 | id: "456",
162 | last_updated: new Date(),
163 | title: "Test",
164 | };
165 | const [excalidraw, send] = renderReducer(
166 | () =>
167 | useExcalidraw({
168 | id,
169 | userId,
170 | initialState: {
171 | state: "SYNCING",
172 | data: {
173 | appState: {
174 | viewBackgroundColor: "#FFF",
175 | } as ExcalidrawData["appState"],
176 | elements: [],
177 | version: 0,
178 | },
179 | image,
180 | metadata,
181 | clipboard: {
182 | state: "NOT_COPIED",
183 | },
184 | },
185 | }),
186 | (UseExcalidraw) => (
187 |
188 |
189 |
190 | )
191 | );
192 |
193 | const newData: ExcalidrawData = {
194 | appState: { viewBackgroundColor: "#FFF" } as ExcalidrawData["appState"],
195 | elements: [{ id: "4", version: 0 } as ExcalidrawData["elements"][number]],
196 | version: 1,
197 | };
198 |
199 | act(() => {
200 | send({ type: "EXCALIDRAW_CHANGE", data: newData });
201 | });
202 |
203 | expect(excalidraw).toEqual({
204 | state: "SYNCING_DIRTY",
205 | data: newData,
206 | metadata,
207 | image,
208 | clipboard: {
209 | state: "NOT_COPIED",
210 | },
211 | });
212 |
213 | const newImage = {} as Blob;
214 | const newMetadata = {
215 | author: "123",
216 | id: "456",
217 | last_updated: new Date(),
218 | title: "Test",
219 | };
220 |
221 | act(() => {
222 | environment.storage.emit({
223 | type: "STORAGE:SAVE_EXCALIDRAW_SUCCESS",
224 | image: newImage,
225 | metadata: newMetadata,
226 | });
227 | });
228 |
229 | expect(excalidraw).toEqual({
230 | state: "DIRTY",
231 | data: newData,
232 | metadata: newMetadata,
233 | image: newImage,
234 | clipboard: {
235 | state: "NOT_COPIED",
236 | },
237 | });
238 | });
239 | });
240 |
--------------------------------------------------------------------------------
/src/environments/storage/browser.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import {
3 | ExcalidrawData,
4 | ExcalidrawMetadata,
5 | ExcalidrawPreview,
6 | Storage,
7 | StorageEvent,
8 | } from "../../environment-interface/storage";
9 | import { exportToBlob, getSceneVersion } from "@excalidraw/excalidraw";
10 |
11 | import { subMonths } from "date-fns";
12 | import { createEmitter } from "react-states";
13 |
14 | export const createExcalidrawImage = (
15 | elements: readonly any[],
16 | appState: any
17 | ) =>
18 | exportToBlob({
19 | elements: elements.filter((element) => !element.isDeleted),
20 | appState,
21 | files: null,
22 | });
23 |
24 | const EXCALIDRAWS_COLLECTION = "excalidraws";
25 | const EXCALIDRAWS_DATA_COLLECTION = "excalidrawsData";
26 | const USERS_COLLECTION = "users";
27 |
28 | export const createStorage = (app: firebase.app.App): Storage => {
29 | const { subscribe, emit } = createEmitter();
30 |
31 | const excalidrawSnapshotSubscriptions: {
32 | [id: string]: () => void;
33 | } = {};
34 |
35 | const lastSavedVersions: {
36 | [id: string]: number;
37 | } = {};
38 |
39 | function getUserExcalidraws(
40 | {
41 | id,
42 | name,
43 | avatarUrl,
44 | }: {
45 | id: string;
46 | name: string;
47 | avatarUrl: string;
48 | },
49 | since: Date
50 | ) {
51 | return app
52 | .firestore()
53 | .collection(USERS_COLLECTION)
54 | .doc(id)
55 | .collection(EXCALIDRAWS_COLLECTION)
56 | .where("last_updated", ">", since)
57 | .orderBy("last_updated", "desc")
58 | .get()
59 | .then((collection): ExcalidrawPreview[] => {
60 | return collection.docs.map((doc) => {
61 | const data = doc.data();
62 |
63 | return {
64 | user: {
65 | uid: id,
66 | name,
67 | avatarUrl,
68 | },
69 | metadata: {
70 | id: doc.id,
71 | title: data.title,
72 | last_updated: data.last_updated.toDate(),
73 | author: id,
74 | },
75 | };
76 | });
77 | });
78 | }
79 |
80 | function sortExcalidrawPreviews(a: ExcalidrawPreview, b: ExcalidrawPreview) {
81 | if (a.metadata.last_updated.getTime() > b.metadata.last_updated.getTime()) {
82 | return -1;
83 | } else if (
84 | a.metadata.last_updated.getTime() < b.metadata.last_updated.getTime()
85 | ) {
86 | return 1;
87 | }
88 |
89 | return 0;
90 | }
91 |
92 | return {
93 | subscribe,
94 | emit,
95 | createExcalidraw(userId) {
96 | app
97 | .firestore()
98 | .collection(USERS_COLLECTION)
99 | .doc(userId)
100 | .collection(EXCALIDRAWS_COLLECTION)
101 | .add({
102 | last_updated: firebase.firestore.FieldValue.serverTimestamp(),
103 | })
104 | .then((ref) => {
105 | emit({
106 | type: "STORAGE:CREATE_EXCALIDRAW_SUCCESS",
107 | id: ref.id,
108 | });
109 | })
110 | .catch((error: Error) => {
111 | emit({
112 | type: "STORAGE:CREATE_EXCALIDRAW_ERROR",
113 | error: error.message,
114 | });
115 | });
116 | },
117 | fetchExcalidraw(userId: string, id: string) {
118 | Promise.all([
119 | app
120 | .firestore()
121 | .collection(USERS_COLLECTION)
122 | .doc(userId)
123 | .collection(EXCALIDRAWS_COLLECTION)
124 | .doc(id)
125 | .get(),
126 | app
127 | .firestore()
128 | .collection(USERS_COLLECTION)
129 | .doc(userId)
130 | .collection(EXCALIDRAWS_DATA_COLLECTION)
131 | .doc(id)
132 | .get(),
133 | ])
134 | .then(([metadataDoc, dataDoc]) => {
135 | const metadata = {
136 | ...metadataDoc.data(),
137 | id,
138 | } as any;
139 | const data = dataDoc.exists
140 | ? dataDoc.data()!
141 | : {
142 | elements: JSON.stringify([]),
143 | appState: JSON.stringify({
144 | viewBackgroundColor: "#FFF",
145 | currentItemFontFamily: 1,
146 | }),
147 | version: 0,
148 | };
149 |
150 | return {
151 | metadata: {
152 | ...(metadata as ExcalidrawMetadata),
153 | id,
154 | last_updated: metadata.last_updated.toDate() as Date,
155 | },
156 | data: {
157 | appState: JSON.parse(data.appState),
158 | elements: JSON.parse(data.elements),
159 | version: data.version,
160 | },
161 | };
162 | })
163 | .then(({ metadata, data }) => {
164 | return createExcalidrawImage(data.elements, data.appState).then(
165 | (image) =>
166 | image
167 | ? {
168 | image,
169 | metadata,
170 | data,
171 | }
172 | : Promise.reject("No image")
173 | );
174 | })
175 | .then(({ data, image, metadata }) => {
176 | emit({
177 | type: "STORAGE:FETCH_EXCALIDRAW_SUCCESS",
178 | metadata,
179 | data,
180 | image,
181 | });
182 |
183 | if (!excalidrawSnapshotSubscriptions[id]) {
184 | excalidrawSnapshotSubscriptions[id] = app
185 | .firestore()
186 | .collection(USERS_COLLECTION)
187 | .doc(userId)
188 | .collection(EXCALIDRAWS_DATA_COLLECTION)
189 | .doc(id)
190 | .onSnapshot((doc) => {
191 | if (doc.metadata.hasPendingWrites) return;
192 |
193 | const data = doc.data();
194 |
195 | if (!data) {
196 | return;
197 | }
198 |
199 | if (lastSavedVersions[id] !== data.version) {
200 | lastSavedVersions[id] = data.version;
201 | emit({
202 | type: "STORAGE:EXCALIDRAW_DATA_UPDATE",
203 | id,
204 | data: {
205 | appState: JSON.parse(data.appState),
206 | elements: JSON.parse(data.elements),
207 | version: data.version,
208 | },
209 | });
210 | }
211 | });
212 | }
213 | })
214 | .catch((error: Error) => {
215 | emit({
216 | type: "STORAGE:FETCH_EXCALIDRAW_ERROR",
217 | error: error.message,
218 | });
219 | });
220 | },
221 | saveExcalidraw(userId, id, data) {
222 | const dataDoc = app
223 | .firestore()
224 | .collection(USERS_COLLECTION)
225 | .doc(userId)
226 | .collection(EXCALIDRAWS_DATA_COLLECTION)
227 | .doc(id);
228 |
229 | app
230 | .firestore()
231 | .runTransaction((transaction) => {
232 | return transaction.get(dataDoc).then((existingDoc) => {
233 | if (existingDoc.exists) {
234 | const existingData = existingDoc.data()!;
235 | const parsedData: ExcalidrawData = {
236 | appState: JSON.parse(existingData.appState),
237 | elements: JSON.parse(existingData.elements),
238 | version: existingData.version,
239 | };
240 |
241 | const newSceneVersion = getSceneVersion(data.elements);
242 | const currentSceneVersion = parsedData.version;
243 |
244 | if (newSceneVersion > currentSceneVersion) {
245 | transaction.update(dataDoc, {
246 | elements: JSON.stringify(data.elements),
247 | appState: JSON.stringify({
248 | viewBackgroundColor: data.appState.viewBackgroundColor,
249 | }),
250 | version: data.version,
251 | });
252 | lastSavedVersions[id] = data.version;
253 | } else {
254 | return Promise.reject("NEWER_VERSION");
255 | }
256 | } else {
257 | transaction.set(dataDoc, {
258 | elements: JSON.stringify(data.elements),
259 | appState: JSON.stringify({
260 | viewBackgroundColor: data.appState.viewBackgroundColor,
261 | }),
262 | version: data.version,
263 | });
264 | lastSavedVersions[id] = data.version;
265 | }
266 | });
267 | })
268 | .then(() =>
269 | app
270 | .firestore()
271 | .collection(USERS_COLLECTION)
272 | .doc(userId)
273 | .collection(EXCALIDRAWS_COLLECTION)
274 | .doc(id)
275 | .set(
276 | {
277 | last_updated: firebase.firestore.FieldValue.serverTimestamp(),
278 | },
279 | {
280 | merge: true,
281 | }
282 | )
283 | )
284 | .then(() =>
285 | app
286 | .firestore()
287 | .collection(USERS_COLLECTION)
288 | .doc(userId)
289 | .collection(EXCALIDRAWS_COLLECTION)
290 | .doc(id)
291 | .get()
292 | .then((doc) => {
293 | const metadata = doc.data()!;
294 |
295 | return createExcalidrawImage(data.elements, data.appState).then(
296 | (image) =>
297 | image
298 | ? {
299 | metadata,
300 | image,
301 | }
302 | : Promise.reject("No image")
303 | );
304 | })
305 | )
306 | .then(({ metadata, image }) => {
307 | emit({
308 | type: "STORAGE:SAVE_EXCALIDRAW_SUCCESS",
309 | metadata: {
310 | ...(metadata as ExcalidrawMetadata),
311 | id,
312 | last_updated: metadata.last_updated.toDate() as Date,
313 | },
314 | image,
315 | });
316 |
317 | app.storage().ref().child(`previews/${userId}/${id}`).put(image);
318 | })
319 | .catch((error) => {
320 | if (error === "NEWER_VERSION") {
321 | emit({
322 | type: "STORAGE:SAVE_EXCALIDRAW_OLD_VERSION",
323 | });
324 |
325 | return;
326 | }
327 |
328 | emit({
329 | type: "STORAGE:SAVE_EXCALIDRAW_ERROR",
330 | error: error.message,
331 | });
332 | });
333 | },
334 | fetchPreviews() {
335 | app
336 | .firestore()
337 | .collection(USERS_COLLECTION)
338 | .get()
339 | .then((collection) =>
340 | Promise.all(
341 | collection.docs.map((userDoc) =>
342 | getUserExcalidraws(
343 | {
344 | id: userDoc.id,
345 | avatarUrl: userDoc.data().avatarUrl,
346 | name: userDoc.data().name,
347 | },
348 | subMonths(new Date(), 2)
349 | )
350 | )
351 | )
352 | )
353 | .then((excalidraws) => {
354 | const flattenedAndSortedExcalidraws = excalidraws
355 | .reduce(
356 | (aggr, userExcalidraws) => aggr.concat(userExcalidraws),
357 | []
358 | )
359 | .sort(sortExcalidrawPreviews);
360 |
361 | emit({
362 | type: "STORAGE:FETCH_PREVIEWS_SUCCESS",
363 | excalidraws: flattenedAndSortedExcalidraws,
364 | });
365 | })
366 | .catch((error: Error) => {
367 | emit({
368 | type: "STORAGE:FETCH_PREVIEWS_ERROR",
369 | error: error.message,
370 | });
371 | });
372 | },
373 | fetchUserPreviews(uid) {
374 | app
375 | .firestore()
376 | .collection(USERS_COLLECTION)
377 | .doc(uid)
378 | .get()
379 | .then((userDoc) => {
380 | const data = userDoc.data();
381 |
382 | if (data) {
383 | return getUserExcalidraws(
384 | {
385 | id: uid,
386 | avatarUrl: data.avatarUrl,
387 | name: data.name,
388 | },
389 | subMonths(new Date(), 6)
390 | );
391 | }
392 |
393 | throw new Error("Invalid user");
394 | })
395 | .then((excalidraws) => {
396 | const sortedExcalidraws = excalidraws.sort(sortExcalidrawPreviews);
397 |
398 | emit({
399 | type: "STORAGE:FETCH_USER_PREVIEWS_SUCCESS",
400 | excalidraws: sortedExcalidraws,
401 | });
402 | })
403 | .catch((error: Error) => {
404 | emit({
405 | type: "STORAGE:FETCH_USER_PREVIEWS_ERROR",
406 | error: error.message,
407 | });
408 | });
409 | },
410 | getImageSrc(userId, id) {
411 | firebase
412 | .storage()
413 | .ref()
414 | .child(`previews/${userId}/${id}`)
415 | .getDownloadURL()
416 | .then((src) => {
417 | emit({
418 | type: "STORAGE:IMAGE_SRC_SUCCESS",
419 | id,
420 | src,
421 | });
422 | })
423 | .catch((error: Error) => {
424 | emit({
425 | type: "STORAGE:IMAGE_SRC_ERROR",
426 | id,
427 | error: error.message,
428 | });
429 | });
430 | },
431 | saveTitle(userId, id, title) {
432 | emit({
433 | type: "STORAGE:SAVE_TITLE_SUCCESS",
434 | id,
435 | title,
436 | });
437 |
438 | app
439 | .firestore()
440 | .collection(USERS_COLLECTION)
441 | .doc(userId)
442 | .collection(EXCALIDRAWS_COLLECTION)
443 | .doc(id)
444 | .set(
445 | {
446 | last_updated: firebase.firestore.FieldValue.serverTimestamp(),
447 | title,
448 | },
449 | {
450 | merge: true,
451 | }
452 | )
453 | .catch((error) => {
454 | emit({
455 | type: "STORAGE:SAVE_TITLE_ERROR",
456 | id,
457 | title,
458 | error: error.message,
459 | });
460 | });
461 | },
462 | };
463 | };
464 |
--------------------------------------------------------------------------------
/.pnp.loader.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { URL as URL$1, fileURLToPath, pathToFileURL } from 'url';
3 | import path from 'path';
4 | import { createHash } from 'crypto';
5 | import { EOL } from 'os';
6 | import moduleExports, { isBuiltin } from 'module';
7 | import assert from 'assert';
8 |
9 | const SAFE_TIME = 456789e3;
10 |
11 | const PortablePath = {
12 | root: `/`,
13 | dot: `.`,
14 | parent: `..`
15 | };
16 | const npath = Object.create(path);
17 | const ppath = Object.create(path.posix);
18 | npath.cwd = () => process.cwd();
19 | ppath.cwd = process.platform === `win32` ? () => toPortablePath(process.cwd()) : process.cwd;
20 | if (process.platform === `win32`) {
21 | ppath.resolve = (...segments) => {
22 | if (segments.length > 0 && ppath.isAbsolute(segments[0])) {
23 | return path.posix.resolve(...segments);
24 | } else {
25 | return path.posix.resolve(ppath.cwd(), ...segments);
26 | }
27 | };
28 | }
29 | const contains = function(pathUtils, from, to) {
30 | from = pathUtils.normalize(from);
31 | to = pathUtils.normalize(to);
32 | if (from === to)
33 | return `.`;
34 | if (!from.endsWith(pathUtils.sep))
35 | from = from + pathUtils.sep;
36 | if (to.startsWith(from)) {
37 | return to.slice(from.length);
38 | } else {
39 | return null;
40 | }
41 | };
42 | npath.contains = (from, to) => contains(npath, from, to);
43 | ppath.contains = (from, to) => contains(ppath, from, to);
44 | const WINDOWS_PATH_REGEXP = /^([a-zA-Z]:.*)$/;
45 | const UNC_WINDOWS_PATH_REGEXP = /^\/\/(\.\/)?(.*)$/;
46 | const PORTABLE_PATH_REGEXP = /^\/([a-zA-Z]:.*)$/;
47 | const UNC_PORTABLE_PATH_REGEXP = /^\/unc\/(\.dot\/)?(.*)$/;
48 | function fromPortablePathWin32(p) {
49 | let portablePathMatch, uncPortablePathMatch;
50 | if (portablePathMatch = p.match(PORTABLE_PATH_REGEXP))
51 | p = portablePathMatch[1];
52 | else if (uncPortablePathMatch = p.match(UNC_PORTABLE_PATH_REGEXP))
53 | p = `\\\\${uncPortablePathMatch[1] ? `.\\` : ``}${uncPortablePathMatch[2]}`;
54 | else
55 | return p;
56 | return p.replace(/\//g, `\\`);
57 | }
58 | function toPortablePathWin32(p) {
59 | p = p.replace(/\\/g, `/`);
60 | let windowsPathMatch, uncWindowsPathMatch;
61 | if (windowsPathMatch = p.match(WINDOWS_PATH_REGEXP))
62 | p = `/${windowsPathMatch[1]}`;
63 | else if (uncWindowsPathMatch = p.match(UNC_WINDOWS_PATH_REGEXP))
64 | p = `/unc/${uncWindowsPathMatch[1] ? `.dot/` : ``}${uncWindowsPathMatch[2]}`;
65 | return p;
66 | }
67 | const toPortablePath = process.platform === `win32` ? toPortablePathWin32 : (p) => p;
68 | const fromPortablePath = process.platform === `win32` ? fromPortablePathWin32 : (p) => p;
69 | npath.fromPortablePath = fromPortablePath;
70 | npath.toPortablePath = toPortablePath;
71 | function convertPath(targetPathUtils, sourcePath) {
72 | return targetPathUtils === npath ? fromPortablePath(sourcePath) : toPortablePath(sourcePath);
73 | }
74 |
75 | const defaultTime = new Date(SAFE_TIME * 1e3);
76 | const defaultTimeMs = defaultTime.getTime();
77 | async function copyPromise(destinationFs, destination, sourceFs, source, opts) {
78 | const normalizedDestination = destinationFs.pathUtils.normalize(destination);
79 | const normalizedSource = sourceFs.pathUtils.normalize(source);
80 | const prelayout = [];
81 | const postlayout = [];
82 | const { atime, mtime } = opts.stableTime ? { atime: defaultTime, mtime: defaultTime } : await sourceFs.lstatPromise(normalizedSource);
83 | await destinationFs.mkdirpPromise(destinationFs.pathUtils.dirname(destination), { utimes: [atime, mtime] });
84 | await copyImpl(prelayout, postlayout, destinationFs, normalizedDestination, sourceFs, normalizedSource, { ...opts, didParentExist: true });
85 | for (const operation of prelayout)
86 | await operation();
87 | await Promise.all(postlayout.map((operation) => {
88 | return operation();
89 | }));
90 | }
91 | async function copyImpl(prelayout, postlayout, destinationFs, destination, sourceFs, source, opts) {
92 | const destinationStat = opts.didParentExist ? await maybeLStat(destinationFs, destination) : null;
93 | const sourceStat = await sourceFs.lstatPromise(source);
94 | const { atime, mtime } = opts.stableTime ? { atime: defaultTime, mtime: defaultTime } : sourceStat;
95 | let updated;
96 | switch (true) {
97 | case sourceStat.isDirectory():
98 | {
99 | updated = await copyFolder(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts);
100 | }
101 | break;
102 | case sourceStat.isFile():
103 | {
104 | updated = await copyFile(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts);
105 | }
106 | break;
107 | case sourceStat.isSymbolicLink():
108 | {
109 | updated = await copySymlink(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts);
110 | }
111 | break;
112 | default:
113 | {
114 | throw new Error(`Unsupported file type (${sourceStat.mode})`);
115 | }
116 | }
117 | if (opts.linkStrategy?.type !== `HardlinkFromIndex` || !sourceStat.isFile()) {
118 | if (updated || destinationStat?.mtime?.getTime() !== mtime.getTime() || destinationStat?.atime?.getTime() !== atime.getTime()) {
119 | postlayout.push(() => destinationFs.lutimesPromise(destination, atime, mtime));
120 | updated = true;
121 | }
122 | if (destinationStat === null || (destinationStat.mode & 511) !== (sourceStat.mode & 511)) {
123 | postlayout.push(() => destinationFs.chmodPromise(destination, sourceStat.mode & 511));
124 | updated = true;
125 | }
126 | }
127 | return updated;
128 | }
129 | async function maybeLStat(baseFs, p) {
130 | try {
131 | return await baseFs.lstatPromise(p);
132 | } catch (e) {
133 | return null;
134 | }
135 | }
136 | async function copyFolder(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) {
137 | if (destinationStat !== null && !destinationStat.isDirectory()) {
138 | if (opts.overwrite) {
139 | prelayout.push(async () => destinationFs.removePromise(destination));
140 | destinationStat = null;
141 | } else {
142 | return false;
143 | }
144 | }
145 | let updated = false;
146 | if (destinationStat === null) {
147 | prelayout.push(async () => {
148 | try {
149 | await destinationFs.mkdirPromise(destination, { mode: sourceStat.mode });
150 | } catch (err) {
151 | if (err.code !== `EEXIST`) {
152 | throw err;
153 | }
154 | }
155 | });
156 | updated = true;
157 | }
158 | const entries = await sourceFs.readdirPromise(source);
159 | const nextOpts = opts.didParentExist && !destinationStat ? { ...opts, didParentExist: false } : opts;
160 | if (opts.stableSort) {
161 | for (const entry of entries.sort()) {
162 | if (await copyImpl(prelayout, postlayout, destinationFs, destinationFs.pathUtils.join(destination, entry), sourceFs, sourceFs.pathUtils.join(source, entry), nextOpts)) {
163 | updated = true;
164 | }
165 | }
166 | } else {
167 | const entriesUpdateStatus = await Promise.all(entries.map(async (entry) => {
168 | await copyImpl(prelayout, postlayout, destinationFs, destinationFs.pathUtils.join(destination, entry), sourceFs, sourceFs.pathUtils.join(source, entry), nextOpts);
169 | }));
170 | if (entriesUpdateStatus.some((status) => status)) {
171 | updated = true;
172 | }
173 | }
174 | return updated;
175 | }
176 | async function copyFileViaIndex(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts, linkStrategy) {
177 | const sourceHash = await sourceFs.checksumFilePromise(source, { algorithm: `sha1` });
178 | const indexPath = destinationFs.pathUtils.join(linkStrategy.indexPath, sourceHash.slice(0, 2), `${sourceHash}.dat`);
179 | let AtomicBehavior;
180 | ((AtomicBehavior2) => {
181 | AtomicBehavior2[AtomicBehavior2["Lock"] = 0] = "Lock";
182 | AtomicBehavior2[AtomicBehavior2["Rename"] = 1] = "Rename";
183 | })(AtomicBehavior || (AtomicBehavior = {}));
184 | let atomicBehavior = 1 /* Rename */;
185 | let indexStat = await maybeLStat(destinationFs, indexPath);
186 | if (destinationStat) {
187 | const isDestinationHardlinkedFromIndex = indexStat && destinationStat.dev === indexStat.dev && destinationStat.ino === indexStat.ino;
188 | const isIndexModified = indexStat?.mtimeMs !== defaultTimeMs;
189 | if (isDestinationHardlinkedFromIndex) {
190 | if (isIndexModified && linkStrategy.autoRepair) {
191 | atomicBehavior = 0 /* Lock */;
192 | indexStat = null;
193 | }
194 | }
195 | if (!isDestinationHardlinkedFromIndex) {
196 | if (opts.overwrite) {
197 | prelayout.push(async () => destinationFs.removePromise(destination));
198 | destinationStat = null;
199 | } else {
200 | return false;
201 | }
202 | }
203 | }
204 | const tempPath = !indexStat && atomicBehavior === 1 /* Rename */ ? `${indexPath}.${Math.floor(Math.random() * 4294967296).toString(16).padStart(8, `0`)}` : null;
205 | let tempPathCleaned = false;
206 | prelayout.push(async () => {
207 | if (!indexStat) {
208 | if (atomicBehavior === 0 /* Lock */) {
209 | await destinationFs.lockPromise(indexPath, async () => {
210 | const content = await sourceFs.readFilePromise(source);
211 | await destinationFs.writeFilePromise(indexPath, content);
212 | });
213 | }
214 | if (atomicBehavior === 1 /* Rename */ && tempPath) {
215 | const content = await sourceFs.readFilePromise(source);
216 | await destinationFs.writeFilePromise(tempPath, content);
217 | try {
218 | await destinationFs.linkPromise(tempPath, indexPath);
219 | } catch (err) {
220 | if (err.code === `EEXIST`) {
221 | tempPathCleaned = true;
222 | await destinationFs.unlinkPromise(tempPath);
223 | } else {
224 | throw err;
225 | }
226 | }
227 | }
228 | }
229 | if (!destinationStat) {
230 | await destinationFs.linkPromise(indexPath, destination);
231 | }
232 | });
233 | postlayout.push(async () => {
234 | if (!indexStat)
235 | await destinationFs.lutimesPromise(indexPath, defaultTime, defaultTime);
236 | if (tempPath && !tempPathCleaned) {
237 | await destinationFs.unlinkPromise(tempPath);
238 | }
239 | });
240 | return false;
241 | }
242 | async function copyFileDirect(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) {
243 | if (destinationStat !== null) {
244 | if (opts.overwrite) {
245 | prelayout.push(async () => destinationFs.removePromise(destination));
246 | destinationStat = null;
247 | } else {
248 | return false;
249 | }
250 | }
251 | prelayout.push(async () => {
252 | const content = await sourceFs.readFilePromise(source);
253 | await destinationFs.writeFilePromise(destination, content);
254 | });
255 | return true;
256 | }
257 | async function copyFile(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) {
258 | if (opts.linkStrategy?.type === `HardlinkFromIndex`) {
259 | return copyFileViaIndex(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts, opts.linkStrategy);
260 | } else {
261 | return copyFileDirect(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts);
262 | }
263 | }
264 | async function copySymlink(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) {
265 | if (destinationStat !== null) {
266 | if (opts.overwrite) {
267 | prelayout.push(async () => destinationFs.removePromise(destination));
268 | destinationStat = null;
269 | } else {
270 | return false;
271 | }
272 | }
273 | prelayout.push(async () => {
274 | await destinationFs.symlinkPromise(convertPath(destinationFs.pathUtils, await sourceFs.readlinkPromise(source)), destination);
275 | });
276 | return true;
277 | }
278 |
279 | class FakeFS {
280 | constructor(pathUtils) {
281 | this.pathUtils = pathUtils;
282 | }
283 | async *genTraversePromise(init, { stableSort = false } = {}) {
284 | const stack = [init];
285 | while (stack.length > 0) {
286 | const p = stack.shift();
287 | const entry = await this.lstatPromise(p);
288 | if (entry.isDirectory()) {
289 | const entries = await this.readdirPromise(p);
290 | if (stableSort) {
291 | for (const entry2 of entries.sort()) {
292 | stack.push(this.pathUtils.join(p, entry2));
293 | }
294 | } else {
295 | throw new Error(`Not supported`);
296 | }
297 | } else {
298 | yield p;
299 | }
300 | }
301 | }
302 | async checksumFilePromise(path, { algorithm = `sha512` } = {}) {
303 | const fd = await this.openPromise(path, `r`);
304 | try {
305 | const CHUNK_SIZE = 65536;
306 | const chunk = Buffer.allocUnsafeSlow(CHUNK_SIZE);
307 | const hash = createHash(algorithm);
308 | let bytesRead = 0;
309 | while ((bytesRead = await this.readPromise(fd, chunk, 0, CHUNK_SIZE)) !== 0)
310 | hash.update(bytesRead === CHUNK_SIZE ? chunk : chunk.slice(0, bytesRead));
311 | return hash.digest(`hex`);
312 | } finally {
313 | await this.closePromise(fd);
314 | }
315 | }
316 | async removePromise(p, { recursive = true, maxRetries = 5 } = {}) {
317 | let stat;
318 | try {
319 | stat = await this.lstatPromise(p);
320 | } catch (error) {
321 | if (error.code === `ENOENT`) {
322 | return;
323 | } else {
324 | throw error;
325 | }
326 | }
327 | if (stat.isDirectory()) {
328 | if (recursive) {
329 | const entries = await this.readdirPromise(p);
330 | await Promise.all(entries.map((entry) => {
331 | return this.removePromise(this.pathUtils.resolve(p, entry));
332 | }));
333 | }
334 | for (let t = 0; t <= maxRetries; t++) {
335 | try {
336 | await this.rmdirPromise(p);
337 | break;
338 | } catch (error) {
339 | if (error.code !== `EBUSY` && error.code !== `ENOTEMPTY`) {
340 | throw error;
341 | } else if (t < maxRetries) {
342 | await new Promise((resolve) => setTimeout(resolve, t * 100));
343 | }
344 | }
345 | }
346 | } else {
347 | await this.unlinkPromise(p);
348 | }
349 | }
350 | removeSync(p, { recursive = true } = {}) {
351 | let stat;
352 | try {
353 | stat = this.lstatSync(p);
354 | } catch (error) {
355 | if (error.code === `ENOENT`) {
356 | return;
357 | } else {
358 | throw error;
359 | }
360 | }
361 | if (stat.isDirectory()) {
362 | if (recursive)
363 | for (const entry of this.readdirSync(p))
364 | this.removeSync(this.pathUtils.resolve(p, entry));
365 | this.rmdirSync(p);
366 | } else {
367 | this.unlinkSync(p);
368 | }
369 | }
370 | async mkdirpPromise(p, { chmod, utimes } = {}) {
371 | p = this.resolve(p);
372 | if (p === this.pathUtils.dirname(p))
373 | return void 0;
374 | const parts = p.split(this.pathUtils.sep);
375 | let createdDirectory;
376 | for (let u = 2; u <= parts.length; ++u) {
377 | const subPath = parts.slice(0, u).join(this.pathUtils.sep);
378 | if (!this.existsSync(subPath)) {
379 | try {
380 | await this.mkdirPromise(subPath);
381 | } catch (error) {
382 | if (error.code === `EEXIST`) {
383 | continue;
384 | } else {
385 | throw error;
386 | }
387 | }
388 | createdDirectory ??= subPath;
389 | if (chmod != null)
390 | await this.chmodPromise(subPath, chmod);
391 | if (utimes != null) {
392 | await this.utimesPromise(subPath, utimes[0], utimes[1]);
393 | } else {
394 | const parentStat = await this.statPromise(this.pathUtils.dirname(subPath));
395 | await this.utimesPromise(subPath, parentStat.atime, parentStat.mtime);
396 | }
397 | }
398 | }
399 | return createdDirectory;
400 | }
401 | mkdirpSync(p, { chmod, utimes } = {}) {
402 | p = this.resolve(p);
403 | if (p === this.pathUtils.dirname(p))
404 | return void 0;
405 | const parts = p.split(this.pathUtils.sep);
406 | let createdDirectory;
407 | for (let u = 2; u <= parts.length; ++u) {
408 | const subPath = parts.slice(0, u).join(this.pathUtils.sep);
409 | if (!this.existsSync(subPath)) {
410 | try {
411 | this.mkdirSync(subPath);
412 | } catch (error) {
413 | if (error.code === `EEXIST`) {
414 | continue;
415 | } else {
416 | throw error;
417 | }
418 | }
419 | createdDirectory ??= subPath;
420 | if (chmod != null)
421 | this.chmodSync(subPath, chmod);
422 | if (utimes != null) {
423 | this.utimesSync(subPath, utimes[0], utimes[1]);
424 | } else {
425 | const parentStat = this.statSync(this.pathUtils.dirname(subPath));
426 | this.utimesSync(subPath, parentStat.atime, parentStat.mtime);
427 | }
428 | }
429 | }
430 | return createdDirectory;
431 | }
432 | async copyPromise(destination, source, { baseFs = this, overwrite = true, stableSort = false, stableTime = false, linkStrategy = null } = {}) {
433 | return await copyPromise(this, destination, baseFs, source, { overwrite, stableSort, stableTime, linkStrategy });
434 | }
435 | copySync(destination, source, { baseFs = this, overwrite = true } = {}) {
436 | const stat = baseFs.lstatSync(source);
437 | const exists = this.existsSync(destination);
438 | if (stat.isDirectory()) {
439 | this.mkdirpSync(destination);
440 | const directoryListing = baseFs.readdirSync(source);
441 | for (const entry of directoryListing) {
442 | this.copySync(this.pathUtils.join(destination, entry), baseFs.pathUtils.join(source, entry), { baseFs, overwrite });
443 | }
444 | } else if (stat.isFile()) {
445 | if (!exists || overwrite) {
446 | if (exists)
447 | this.removeSync(destination);
448 | const content = baseFs.readFileSync(source);
449 | this.writeFileSync(destination, content);
450 | }
451 | } else if (stat.isSymbolicLink()) {
452 | if (!exists || overwrite) {
453 | if (exists)
454 | this.removeSync(destination);
455 | const target = baseFs.readlinkSync(source);
456 | this.symlinkSync(convertPath(this.pathUtils, target), destination);
457 | }
458 | } else {
459 | throw new Error(`Unsupported file type (file: ${source}, mode: 0o${stat.mode.toString(8).padStart(6, `0`)})`);
460 | }
461 | const mode = stat.mode & 511;
462 | this.chmodSync(destination, mode);
463 | }
464 | async changeFilePromise(p, content, opts = {}) {
465 | if (Buffer.isBuffer(content)) {
466 | return this.changeFileBufferPromise(p, content, opts);
467 | } else {
468 | return this.changeFileTextPromise(p, content, opts);
469 | }
470 | }
471 | async changeFileBufferPromise(p, content, { mode } = {}) {
472 | let current = Buffer.alloc(0);
473 | try {
474 | current = await this.readFilePromise(p);
475 | } catch (error) {
476 | }
477 | if (Buffer.compare(current, content) === 0)
478 | return;
479 | await this.writeFilePromise(p, content, { mode });
480 | }
481 | async changeFileTextPromise(p, content, { automaticNewlines, mode } = {}) {
482 | let current = ``;
483 | try {
484 | current = await this.readFilePromise(p, `utf8`);
485 | } catch (error) {
486 | }
487 | const normalizedContent = automaticNewlines ? normalizeLineEndings(current, content) : content;
488 | if (current === normalizedContent)
489 | return;
490 | await this.writeFilePromise(p, normalizedContent, { mode });
491 | }
492 | changeFileSync(p, content, opts = {}) {
493 | if (Buffer.isBuffer(content)) {
494 | return this.changeFileBufferSync(p, content, opts);
495 | } else {
496 | return this.changeFileTextSync(p, content, opts);
497 | }
498 | }
499 | changeFileBufferSync(p, content, { mode } = {}) {
500 | let current = Buffer.alloc(0);
501 | try {
502 | current = this.readFileSync(p);
503 | } catch (error) {
504 | }
505 | if (Buffer.compare(current, content) === 0)
506 | return;
507 | this.writeFileSync(p, content, { mode });
508 | }
509 | changeFileTextSync(p, content, { automaticNewlines = false, mode } = {}) {
510 | let current = ``;
511 | try {
512 | current = this.readFileSync(p, `utf8`);
513 | } catch (error) {
514 | }
515 | const normalizedContent = automaticNewlines ? normalizeLineEndings(current, content) : content;
516 | if (current === normalizedContent)
517 | return;
518 | this.writeFileSync(p, normalizedContent, { mode });
519 | }
520 | async movePromise(fromP, toP) {
521 | try {
522 | await this.renamePromise(fromP, toP);
523 | } catch (error) {
524 | if (error.code === `EXDEV`) {
525 | await this.copyPromise(toP, fromP);
526 | await this.removePromise(fromP);
527 | } else {
528 | throw error;
529 | }
530 | }
531 | }
532 | moveSync(fromP, toP) {
533 | try {
534 | this.renameSync(fromP, toP);
535 | } catch (error) {
536 | if (error.code === `EXDEV`) {
537 | this.copySync(toP, fromP);
538 | this.removeSync(fromP);
539 | } else {
540 | throw error;
541 | }
542 | }
543 | }
544 | async lockPromise(affectedPath, callback) {
545 | const lockPath = `${affectedPath}.flock`;
546 | const interval = 1e3 / 60;
547 | const startTime = Date.now();
548 | let fd = null;
549 | const isAlive = async () => {
550 | let pid;
551 | try {
552 | [pid] = await this.readJsonPromise(lockPath);
553 | } catch (error) {
554 | return Date.now() - startTime < 500;
555 | }
556 | try {
557 | process.kill(pid, 0);
558 | return true;
559 | } catch (error) {
560 | return false;
561 | }
562 | };
563 | while (fd === null) {
564 | try {
565 | fd = await this.openPromise(lockPath, `wx`);
566 | } catch (error) {
567 | if (error.code === `EEXIST`) {
568 | if (!await isAlive()) {
569 | try {
570 | await this.unlinkPromise(lockPath);
571 | continue;
572 | } catch (error2) {
573 | }
574 | }
575 | if (Date.now() - startTime < 60 * 1e3) {
576 | await new Promise((resolve) => setTimeout(resolve, interval));
577 | } else {
578 | throw new Error(`Couldn't acquire a lock in a reasonable time (via ${lockPath})`);
579 | }
580 | } else {
581 | throw error;
582 | }
583 | }
584 | }
585 | await this.writePromise(fd, JSON.stringify([process.pid]));
586 | try {
587 | return await callback();
588 | } finally {
589 | try {
590 | await this.closePromise(fd);
591 | await this.unlinkPromise(lockPath);
592 | } catch (error) {
593 | }
594 | }
595 | }
596 | async readJsonPromise(p) {
597 | const content = await this.readFilePromise(p, `utf8`);
598 | try {
599 | return JSON.parse(content);
600 | } catch (error) {
601 | error.message += ` (in ${p})`;
602 | throw error;
603 | }
604 | }
605 | readJsonSync(p) {
606 | const content = this.readFileSync(p, `utf8`);
607 | try {
608 | return JSON.parse(content);
609 | } catch (error) {
610 | error.message += ` (in ${p})`;
611 | throw error;
612 | }
613 | }
614 | async writeJsonPromise(p, data, { compact = false } = {}) {
615 | const space = compact ? 0 : 2;
616 | return await this.writeFilePromise(p, `${JSON.stringify(data, null, space)}
617 | `);
618 | }
619 | writeJsonSync(p, data, { compact = false } = {}) {
620 | const space = compact ? 0 : 2;
621 | return this.writeFileSync(p, `${JSON.stringify(data, null, space)}
622 | `);
623 | }
624 | async preserveTimePromise(p, cb) {
625 | const stat = await this.lstatPromise(p);
626 | const result = await cb();
627 | if (typeof result !== `undefined`)
628 | p = result;
629 | await this.lutimesPromise(p, stat.atime, stat.mtime);
630 | }
631 | async preserveTimeSync(p, cb) {
632 | const stat = this.lstatSync(p);
633 | const result = cb();
634 | if (typeof result !== `undefined`)
635 | p = result;
636 | this.lutimesSync(p, stat.atime, stat.mtime);
637 | }
638 | }
639 | class BasePortableFakeFS extends FakeFS {
640 | constructor() {
641 | super(ppath);
642 | }
643 | }
644 | function getEndOfLine(content) {
645 | const matches = content.match(/\r?\n/g);
646 | if (matches === null)
647 | return EOL;
648 | const crlf = matches.filter((nl) => nl === `\r
649 | `).length;
650 | const lf = matches.length - crlf;
651 | return crlf > lf ? `\r
652 | ` : `
653 | `;
654 | }
655 | function normalizeLineEndings(originalContent, newContent) {
656 | return newContent.replace(/\r?\n/g, getEndOfLine(originalContent));
657 | }
658 |
659 | class ProxiedFS extends FakeFS {
660 | getExtractHint(hints) {
661 | return this.baseFs.getExtractHint(hints);
662 | }
663 | resolve(path) {
664 | return this.mapFromBase(this.baseFs.resolve(this.mapToBase(path)));
665 | }
666 | getRealPath() {
667 | return this.mapFromBase(this.baseFs.getRealPath());
668 | }
669 | async openPromise(p, flags, mode) {
670 | return this.baseFs.openPromise(this.mapToBase(p), flags, mode);
671 | }
672 | openSync(p, flags, mode) {
673 | return this.baseFs.openSync(this.mapToBase(p), flags, mode);
674 | }
675 | async opendirPromise(p, opts) {
676 | return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(p), opts), { path: p });
677 | }
678 | opendirSync(p, opts) {
679 | return Object.assign(this.baseFs.opendirSync(this.mapToBase(p), opts), { path: p });
680 | }
681 | async readPromise(fd, buffer, offset, length, position) {
682 | return await this.baseFs.readPromise(fd, buffer, offset, length, position);
683 | }
684 | readSync(fd, buffer, offset, length, position) {
685 | return this.baseFs.readSync(fd, buffer, offset, length, position);
686 | }
687 | async writePromise(fd, buffer, offset, length, position) {
688 | if (typeof buffer === `string`) {
689 | return await this.baseFs.writePromise(fd, buffer, offset);
690 | } else {
691 | return await this.baseFs.writePromise(fd, buffer, offset, length, position);
692 | }
693 | }
694 | writeSync(fd, buffer, offset, length, position) {
695 | if (typeof buffer === `string`) {
696 | return this.baseFs.writeSync(fd, buffer, offset);
697 | } else {
698 | return this.baseFs.writeSync(fd, buffer, offset, length, position);
699 | }
700 | }
701 | async closePromise(fd) {
702 | return this.baseFs.closePromise(fd);
703 | }
704 | closeSync(fd) {
705 | this.baseFs.closeSync(fd);
706 | }
707 | createReadStream(p, opts) {
708 | return this.baseFs.createReadStream(p !== null ? this.mapToBase(p) : p, opts);
709 | }
710 | createWriteStream(p, opts) {
711 | return this.baseFs.createWriteStream(p !== null ? this.mapToBase(p) : p, opts);
712 | }
713 | async realpathPromise(p) {
714 | return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(p)));
715 | }
716 | realpathSync(p) {
717 | return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(p)));
718 | }
719 | async existsPromise(p) {
720 | return this.baseFs.existsPromise(this.mapToBase(p));
721 | }
722 | existsSync(p) {
723 | return this.baseFs.existsSync(this.mapToBase(p));
724 | }
725 | accessSync(p, mode) {
726 | return this.baseFs.accessSync(this.mapToBase(p), mode);
727 | }
728 | async accessPromise(p, mode) {
729 | return this.baseFs.accessPromise(this.mapToBase(p), mode);
730 | }
731 | async statPromise(p, opts) {
732 | return this.baseFs.statPromise(this.mapToBase(p), opts);
733 | }
734 | statSync(p, opts) {
735 | return this.baseFs.statSync(this.mapToBase(p), opts);
736 | }
737 | async fstatPromise(fd, opts) {
738 | return this.baseFs.fstatPromise(fd, opts);
739 | }
740 | fstatSync(fd, opts) {
741 | return this.baseFs.fstatSync(fd, opts);
742 | }
743 | lstatPromise(p, opts) {
744 | return this.baseFs.lstatPromise(this.mapToBase(p), opts);
745 | }
746 | lstatSync(p, opts) {
747 | return this.baseFs.lstatSync(this.mapToBase(p), opts);
748 | }
749 | async fchmodPromise(fd, mask) {
750 | return this.baseFs.fchmodPromise(fd, mask);
751 | }
752 | fchmodSync(fd, mask) {
753 | return this.baseFs.fchmodSync(fd, mask);
754 | }
755 | async chmodPromise(p, mask) {
756 | return this.baseFs.chmodPromise(this.mapToBase(p), mask);
757 | }
758 | chmodSync(p, mask) {
759 | return this.baseFs.chmodSync(this.mapToBase(p), mask);
760 | }
761 | async fchownPromise(fd, uid, gid) {
762 | return this.baseFs.fchownPromise(fd, uid, gid);
763 | }
764 | fchownSync(fd, uid, gid) {
765 | return this.baseFs.fchownSync(fd, uid, gid);
766 | }
767 | async chownPromise(p, uid, gid) {
768 | return this.baseFs.chownPromise(this.mapToBase(p), uid, gid);
769 | }
770 | chownSync(p, uid, gid) {
771 | return this.baseFs.chownSync(this.mapToBase(p), uid, gid);
772 | }
773 | async renamePromise(oldP, newP) {
774 | return this.baseFs.renamePromise(this.mapToBase(oldP), this.mapToBase(newP));
775 | }
776 | renameSync(oldP, newP) {
777 | return this.baseFs.renameSync(this.mapToBase(oldP), this.mapToBase(newP));
778 | }
779 | async copyFilePromise(sourceP, destP, flags = 0) {
780 | return this.baseFs.copyFilePromise(this.mapToBase(sourceP), this.mapToBase(destP), flags);
781 | }
782 | copyFileSync(sourceP, destP, flags = 0) {
783 | return this.baseFs.copyFileSync(this.mapToBase(sourceP), this.mapToBase(destP), flags);
784 | }
785 | async appendFilePromise(p, content, opts) {
786 | return this.baseFs.appendFilePromise(this.fsMapToBase(p), content, opts);
787 | }
788 | appendFileSync(p, content, opts) {
789 | return this.baseFs.appendFileSync(this.fsMapToBase(p), content, opts);
790 | }
791 | async writeFilePromise(p, content, opts) {
792 | return this.baseFs.writeFilePromise(this.fsMapToBase(p), content, opts);
793 | }
794 | writeFileSync(p, content, opts) {
795 | return this.baseFs.writeFileSync(this.fsMapToBase(p), content, opts);
796 | }
797 | async unlinkPromise(p) {
798 | return this.baseFs.unlinkPromise(this.mapToBase(p));
799 | }
800 | unlinkSync(p) {
801 | return this.baseFs.unlinkSync(this.mapToBase(p));
802 | }
803 | async utimesPromise(p, atime, mtime) {
804 | return this.baseFs.utimesPromise(this.mapToBase(p), atime, mtime);
805 | }
806 | utimesSync(p, atime, mtime) {
807 | return this.baseFs.utimesSync(this.mapToBase(p), atime, mtime);
808 | }
809 | async lutimesPromise(p, atime, mtime) {
810 | return this.baseFs.lutimesPromise(this.mapToBase(p), atime, mtime);
811 | }
812 | lutimesSync(p, atime, mtime) {
813 | return this.baseFs.lutimesSync(this.mapToBase(p), atime, mtime);
814 | }
815 | async mkdirPromise(p, opts) {
816 | return this.baseFs.mkdirPromise(this.mapToBase(p), opts);
817 | }
818 | mkdirSync(p, opts) {
819 | return this.baseFs.mkdirSync(this.mapToBase(p), opts);
820 | }
821 | async rmdirPromise(p, opts) {
822 | return this.baseFs.rmdirPromise(this.mapToBase(p), opts);
823 | }
824 | rmdirSync(p, opts) {
825 | return this.baseFs.rmdirSync(this.mapToBase(p), opts);
826 | }
827 | async linkPromise(existingP, newP) {
828 | return this.baseFs.linkPromise(this.mapToBase(existingP), this.mapToBase(newP));
829 | }
830 | linkSync(existingP, newP) {
831 | return this.baseFs.linkSync(this.mapToBase(existingP), this.mapToBase(newP));
832 | }
833 | async symlinkPromise(target, p, type) {
834 | const mappedP = this.mapToBase(p);
835 | if (this.pathUtils.isAbsolute(target))
836 | return this.baseFs.symlinkPromise(this.mapToBase(target), mappedP, type);
837 | const mappedAbsoluteTarget = this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(p), target));
838 | const mappedTarget = this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(mappedP), mappedAbsoluteTarget);
839 | return this.baseFs.symlinkPromise(mappedTarget, mappedP, type);
840 | }
841 | symlinkSync(target, p, type) {
842 | const mappedP = this.mapToBase(p);
843 | if (this.pathUtils.isAbsolute(target))
844 | return this.baseFs.symlinkSync(this.mapToBase(target), mappedP, type);
845 | const mappedAbsoluteTarget = this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(p), target));
846 | const mappedTarget = this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(mappedP), mappedAbsoluteTarget);
847 | return this.baseFs.symlinkSync(mappedTarget, mappedP, type);
848 | }
849 | async readFilePromise(p, encoding) {
850 | return this.baseFs.readFilePromise(this.fsMapToBase(p), encoding);
851 | }
852 | readFileSync(p, encoding) {
853 | return this.baseFs.readFileSync(this.fsMapToBase(p), encoding);
854 | }
855 | readdirPromise(p, opts) {
856 | return this.baseFs.readdirPromise(this.mapToBase(p), opts);
857 | }
858 | readdirSync(p, opts) {
859 | return this.baseFs.readdirSync(this.mapToBase(p), opts);
860 | }
861 | async readlinkPromise(p) {
862 | return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(p)));
863 | }
864 | readlinkSync(p) {
865 | return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(p)));
866 | }
867 | async truncatePromise(p, len) {
868 | return this.baseFs.truncatePromise(this.mapToBase(p), len);
869 | }
870 | truncateSync(p, len) {
871 | return this.baseFs.truncateSync(this.mapToBase(p), len);
872 | }
873 | async ftruncatePromise(fd, len) {
874 | return this.baseFs.ftruncatePromise(fd, len);
875 | }
876 | ftruncateSync(fd, len) {
877 | return this.baseFs.ftruncateSync(fd, len);
878 | }
879 | watch(p, a, b) {
880 | return this.baseFs.watch(
881 | this.mapToBase(p),
882 | a,
883 | b
884 | );
885 | }
886 | watchFile(p, a, b) {
887 | return this.baseFs.watchFile(
888 | this.mapToBase(p),
889 | a,
890 | b
891 | );
892 | }
893 | unwatchFile(p, cb) {
894 | return this.baseFs.unwatchFile(this.mapToBase(p), cb);
895 | }
896 | fsMapToBase(p) {
897 | if (typeof p === `number`) {
898 | return p;
899 | } else {
900 | return this.mapToBase(p);
901 | }
902 | }
903 | }
904 |
905 | class NodeFS extends BasePortableFakeFS {
906 | constructor(realFs = fs) {
907 | super();
908 | this.realFs = realFs;
909 | }
910 | getExtractHint() {
911 | return false;
912 | }
913 | getRealPath() {
914 | return PortablePath.root;
915 | }
916 | resolve(p) {
917 | return ppath.resolve(p);
918 | }
919 | async openPromise(p, flags, mode) {
920 | return await new Promise((resolve, reject) => {
921 | this.realFs.open(npath.fromPortablePath(p), flags, mode, this.makeCallback(resolve, reject));
922 | });
923 | }
924 | openSync(p, flags, mode) {
925 | return this.realFs.openSync(npath.fromPortablePath(p), flags, mode);
926 | }
927 | async opendirPromise(p, opts) {
928 | return await new Promise((resolve, reject) => {
929 | if (typeof opts !== `undefined`) {
930 | this.realFs.opendir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
931 | } else {
932 | this.realFs.opendir(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
933 | }
934 | }).then((dir) => {
935 | const dirWithFixedPath = dir;
936 | Object.defineProperty(dirWithFixedPath, `path`, {
937 | value: p,
938 | configurable: true,
939 | writable: true
940 | });
941 | return dirWithFixedPath;
942 | });
943 | }
944 | opendirSync(p, opts) {
945 | const dir = typeof opts !== `undefined` ? this.realFs.opendirSync(npath.fromPortablePath(p), opts) : this.realFs.opendirSync(npath.fromPortablePath(p));
946 | const dirWithFixedPath = dir;
947 | Object.defineProperty(dirWithFixedPath, `path`, {
948 | value: p,
949 | configurable: true,
950 | writable: true
951 | });
952 | return dirWithFixedPath;
953 | }
954 | async readPromise(fd, buffer, offset = 0, length = 0, position = -1) {
955 | return await new Promise((resolve, reject) => {
956 | this.realFs.read(fd, buffer, offset, length, position, (error, bytesRead) => {
957 | if (error) {
958 | reject(error);
959 | } else {
960 | resolve(bytesRead);
961 | }
962 | });
963 | });
964 | }
965 | readSync(fd, buffer, offset, length, position) {
966 | return this.realFs.readSync(fd, buffer, offset, length, position);
967 | }
968 | async writePromise(fd, buffer, offset, length, position) {
969 | return await new Promise((resolve, reject) => {
970 | if (typeof buffer === `string`) {
971 | return this.realFs.write(fd, buffer, offset, this.makeCallback(resolve, reject));
972 | } else {
973 | return this.realFs.write(fd, buffer, offset, length, position, this.makeCallback(resolve, reject));
974 | }
975 | });
976 | }
977 | writeSync(fd, buffer, offset, length, position) {
978 | if (typeof buffer === `string`) {
979 | return this.realFs.writeSync(fd, buffer, offset);
980 | } else {
981 | return this.realFs.writeSync(fd, buffer, offset, length, position);
982 | }
983 | }
984 | async closePromise(fd) {
985 | await new Promise((resolve, reject) => {
986 | this.realFs.close(fd, this.makeCallback(resolve, reject));
987 | });
988 | }
989 | closeSync(fd) {
990 | this.realFs.closeSync(fd);
991 | }
992 | createReadStream(p, opts) {
993 | const realPath = p !== null ? npath.fromPortablePath(p) : p;
994 | return this.realFs.createReadStream(realPath, opts);
995 | }
996 | createWriteStream(p, opts) {
997 | const realPath = p !== null ? npath.fromPortablePath(p) : p;
998 | return this.realFs.createWriteStream(realPath, opts);
999 | }
1000 | async realpathPromise(p) {
1001 | return await new Promise((resolve, reject) => {
1002 | this.realFs.realpath(npath.fromPortablePath(p), {}, this.makeCallback(resolve, reject));
1003 | }).then((path) => {
1004 | return npath.toPortablePath(path);
1005 | });
1006 | }
1007 | realpathSync(p) {
1008 | return npath.toPortablePath(this.realFs.realpathSync(npath.fromPortablePath(p), {}));
1009 | }
1010 | async existsPromise(p) {
1011 | return await new Promise((resolve) => {
1012 | this.realFs.exists(npath.fromPortablePath(p), resolve);
1013 | });
1014 | }
1015 | accessSync(p, mode) {
1016 | return this.realFs.accessSync(npath.fromPortablePath(p), mode);
1017 | }
1018 | async accessPromise(p, mode) {
1019 | return await new Promise((resolve, reject) => {
1020 | this.realFs.access(npath.fromPortablePath(p), mode, this.makeCallback(resolve, reject));
1021 | });
1022 | }
1023 | existsSync(p) {
1024 | return this.realFs.existsSync(npath.fromPortablePath(p));
1025 | }
1026 | async statPromise(p, opts) {
1027 | return await new Promise((resolve, reject) => {
1028 | if (opts) {
1029 | this.realFs.stat(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
1030 | } else {
1031 | this.realFs.stat(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
1032 | }
1033 | });
1034 | }
1035 | statSync(p, opts) {
1036 | if (opts) {
1037 | return this.realFs.statSync(npath.fromPortablePath(p), opts);
1038 | } else {
1039 | return this.realFs.statSync(npath.fromPortablePath(p));
1040 | }
1041 | }
1042 | async fstatPromise(fd, opts) {
1043 | return await new Promise((resolve, reject) => {
1044 | if (opts) {
1045 | this.realFs.fstat(fd, opts, this.makeCallback(resolve, reject));
1046 | } else {
1047 | this.realFs.fstat(fd, this.makeCallback(resolve, reject));
1048 | }
1049 | });
1050 | }
1051 | fstatSync(fd, opts) {
1052 | if (opts) {
1053 | return this.realFs.fstatSync(fd, opts);
1054 | } else {
1055 | return this.realFs.fstatSync(fd);
1056 | }
1057 | }
1058 | async lstatPromise(p, opts) {
1059 | return await new Promise((resolve, reject) => {
1060 | if (opts) {
1061 | this.realFs.lstat(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
1062 | } else {
1063 | this.realFs.lstat(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
1064 | }
1065 | });
1066 | }
1067 | lstatSync(p, opts) {
1068 | if (opts) {
1069 | return this.realFs.lstatSync(npath.fromPortablePath(p), opts);
1070 | } else {
1071 | return this.realFs.lstatSync(npath.fromPortablePath(p));
1072 | }
1073 | }
1074 | async fchmodPromise(fd, mask) {
1075 | return await new Promise((resolve, reject) => {
1076 | this.realFs.fchmod(fd, mask, this.makeCallback(resolve, reject));
1077 | });
1078 | }
1079 | fchmodSync(fd, mask) {
1080 | return this.realFs.fchmodSync(fd, mask);
1081 | }
1082 | async chmodPromise(p, mask) {
1083 | return await new Promise((resolve, reject) => {
1084 | this.realFs.chmod(npath.fromPortablePath(p), mask, this.makeCallback(resolve, reject));
1085 | });
1086 | }
1087 | chmodSync(p, mask) {
1088 | return this.realFs.chmodSync(npath.fromPortablePath(p), mask);
1089 | }
1090 | async fchownPromise(fd, uid, gid) {
1091 | return await new Promise((resolve, reject) => {
1092 | this.realFs.fchown(fd, uid, gid, this.makeCallback(resolve, reject));
1093 | });
1094 | }
1095 | fchownSync(fd, uid, gid) {
1096 | return this.realFs.fchownSync(fd, uid, gid);
1097 | }
1098 | async chownPromise(p, uid, gid) {
1099 | return await new Promise((resolve, reject) => {
1100 | this.realFs.chown(npath.fromPortablePath(p), uid, gid, this.makeCallback(resolve, reject));
1101 | });
1102 | }
1103 | chownSync(p, uid, gid) {
1104 | return this.realFs.chownSync(npath.fromPortablePath(p), uid, gid);
1105 | }
1106 | async renamePromise(oldP, newP) {
1107 | return await new Promise((resolve, reject) => {
1108 | this.realFs.rename(npath.fromPortablePath(oldP), npath.fromPortablePath(newP), this.makeCallback(resolve, reject));
1109 | });
1110 | }
1111 | renameSync(oldP, newP) {
1112 | return this.realFs.renameSync(npath.fromPortablePath(oldP), npath.fromPortablePath(newP));
1113 | }
1114 | async copyFilePromise(sourceP, destP, flags = 0) {
1115 | return await new Promise((resolve, reject) => {
1116 | this.realFs.copyFile(npath.fromPortablePath(sourceP), npath.fromPortablePath(destP), flags, this.makeCallback(resolve, reject));
1117 | });
1118 | }
1119 | copyFileSync(sourceP, destP, flags = 0) {
1120 | return this.realFs.copyFileSync(npath.fromPortablePath(sourceP), npath.fromPortablePath(destP), flags);
1121 | }
1122 | async appendFilePromise(p, content, opts) {
1123 | return await new Promise((resolve, reject) => {
1124 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1125 | if (opts) {
1126 | this.realFs.appendFile(fsNativePath, content, opts, this.makeCallback(resolve, reject));
1127 | } else {
1128 | this.realFs.appendFile(fsNativePath, content, this.makeCallback(resolve, reject));
1129 | }
1130 | });
1131 | }
1132 | appendFileSync(p, content, opts) {
1133 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1134 | if (opts) {
1135 | this.realFs.appendFileSync(fsNativePath, content, opts);
1136 | } else {
1137 | this.realFs.appendFileSync(fsNativePath, content);
1138 | }
1139 | }
1140 | async writeFilePromise(p, content, opts) {
1141 | return await new Promise((resolve, reject) => {
1142 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1143 | if (opts) {
1144 | this.realFs.writeFile(fsNativePath, content, opts, this.makeCallback(resolve, reject));
1145 | } else {
1146 | this.realFs.writeFile(fsNativePath, content, this.makeCallback(resolve, reject));
1147 | }
1148 | });
1149 | }
1150 | writeFileSync(p, content, opts) {
1151 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1152 | if (opts) {
1153 | this.realFs.writeFileSync(fsNativePath, content, opts);
1154 | } else {
1155 | this.realFs.writeFileSync(fsNativePath, content);
1156 | }
1157 | }
1158 | async unlinkPromise(p) {
1159 | return await new Promise((resolve, reject) => {
1160 | this.realFs.unlink(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
1161 | });
1162 | }
1163 | unlinkSync(p) {
1164 | return this.realFs.unlinkSync(npath.fromPortablePath(p));
1165 | }
1166 | async utimesPromise(p, atime, mtime) {
1167 | return await new Promise((resolve, reject) => {
1168 | this.realFs.utimes(npath.fromPortablePath(p), atime, mtime, this.makeCallback(resolve, reject));
1169 | });
1170 | }
1171 | utimesSync(p, atime, mtime) {
1172 | this.realFs.utimesSync(npath.fromPortablePath(p), atime, mtime);
1173 | }
1174 | async lutimesPromise(p, atime, mtime) {
1175 | return await new Promise((resolve, reject) => {
1176 | this.realFs.lutimes(npath.fromPortablePath(p), atime, mtime, this.makeCallback(resolve, reject));
1177 | });
1178 | }
1179 | lutimesSync(p, atime, mtime) {
1180 | this.realFs.lutimesSync(npath.fromPortablePath(p), atime, mtime);
1181 | }
1182 | async mkdirPromise(p, opts) {
1183 | return await new Promise((resolve, reject) => {
1184 | this.realFs.mkdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
1185 | });
1186 | }
1187 | mkdirSync(p, opts) {
1188 | return this.realFs.mkdirSync(npath.fromPortablePath(p), opts);
1189 | }
1190 | async rmdirPromise(p, opts) {
1191 | return await new Promise((resolve, reject) => {
1192 | if (opts) {
1193 | this.realFs.rmdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
1194 | } else {
1195 | this.realFs.rmdir(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
1196 | }
1197 | });
1198 | }
1199 | rmdirSync(p, opts) {
1200 | return this.realFs.rmdirSync(npath.fromPortablePath(p), opts);
1201 | }
1202 | async linkPromise(existingP, newP) {
1203 | return await new Promise((resolve, reject) => {
1204 | this.realFs.link(npath.fromPortablePath(existingP), npath.fromPortablePath(newP), this.makeCallback(resolve, reject));
1205 | });
1206 | }
1207 | linkSync(existingP, newP) {
1208 | return this.realFs.linkSync(npath.fromPortablePath(existingP), npath.fromPortablePath(newP));
1209 | }
1210 | async symlinkPromise(target, p, type) {
1211 | return await new Promise((resolve, reject) => {
1212 | this.realFs.symlink(npath.fromPortablePath(target.replace(/\/+$/, ``)), npath.fromPortablePath(p), type, this.makeCallback(resolve, reject));
1213 | });
1214 | }
1215 | symlinkSync(target, p, type) {
1216 | return this.realFs.symlinkSync(npath.fromPortablePath(target.replace(/\/+$/, ``)), npath.fromPortablePath(p), type);
1217 | }
1218 | async readFilePromise(p, encoding) {
1219 | return await new Promise((resolve, reject) => {
1220 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1221 | this.realFs.readFile(fsNativePath, encoding, this.makeCallback(resolve, reject));
1222 | });
1223 | }
1224 | readFileSync(p, encoding) {
1225 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p;
1226 | return this.realFs.readFileSync(fsNativePath, encoding);
1227 | }
1228 | async readdirPromise(p, opts) {
1229 | return await new Promise((resolve, reject) => {
1230 | if (opts) {
1231 | this.realFs.readdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject));
1232 | } else {
1233 | this.realFs.readdir(npath.fromPortablePath(p), this.makeCallback((value) => resolve(value), reject));
1234 | }
1235 | });
1236 | }
1237 | readdirSync(p, opts) {
1238 | if (opts) {
1239 | return this.realFs.readdirSync(npath.fromPortablePath(p), opts);
1240 | } else {
1241 | return this.realFs.readdirSync(npath.fromPortablePath(p));
1242 | }
1243 | }
1244 | async readlinkPromise(p) {
1245 | return await new Promise((resolve, reject) => {
1246 | this.realFs.readlink(npath.fromPortablePath(p), this.makeCallback(resolve, reject));
1247 | }).then((path) => {
1248 | return npath.toPortablePath(path);
1249 | });
1250 | }
1251 | readlinkSync(p) {
1252 | return npath.toPortablePath(this.realFs.readlinkSync(npath.fromPortablePath(p)));
1253 | }
1254 | async truncatePromise(p, len) {
1255 | return await new Promise((resolve, reject) => {
1256 | this.realFs.truncate(npath.fromPortablePath(p), len, this.makeCallback(resolve, reject));
1257 | });
1258 | }
1259 | truncateSync(p, len) {
1260 | return this.realFs.truncateSync(npath.fromPortablePath(p), len);
1261 | }
1262 | async ftruncatePromise(fd, len) {
1263 | return await new Promise((resolve, reject) => {
1264 | this.realFs.ftruncate(fd, len, this.makeCallback(resolve, reject));
1265 | });
1266 | }
1267 | ftruncateSync(fd, len) {
1268 | return this.realFs.ftruncateSync(fd, len);
1269 | }
1270 | watch(p, a, b) {
1271 | return this.realFs.watch(
1272 | npath.fromPortablePath(p),
1273 | a,
1274 | b
1275 | );
1276 | }
1277 | watchFile(p, a, b) {
1278 | return this.realFs.watchFile(
1279 | npath.fromPortablePath(p),
1280 | a,
1281 | b
1282 | );
1283 | }
1284 | unwatchFile(p, cb) {
1285 | return this.realFs.unwatchFile(npath.fromPortablePath(p), cb);
1286 | }
1287 | makeCallback(resolve, reject) {
1288 | return (err, result) => {
1289 | if (err) {
1290 | reject(err);
1291 | } else {
1292 | resolve(result);
1293 | }
1294 | };
1295 | }
1296 | }
1297 |
1298 | const NUMBER_REGEXP = /^[0-9]+$/;
1299 | const VIRTUAL_REGEXP = /^(\/(?:[^/]+\/)*?(?:\$\$virtual|__virtual__))((?:\/((?:[^/]+-)?[a-f0-9]+)(?:\/([^/]+))?)?((?:\/.*)?))$/;
1300 | const VALID_COMPONENT = /^([^/]+-)?[a-f0-9]+$/;
1301 | class VirtualFS extends ProxiedFS {
1302 | constructor({ baseFs = new NodeFS() } = {}) {
1303 | super(ppath);
1304 | this.baseFs = baseFs;
1305 | }
1306 | static makeVirtualPath(base, component, to) {
1307 | if (ppath.basename(base) !== `__virtual__`)
1308 | throw new Error(`Assertion failed: Virtual folders must be named "__virtual__"`);
1309 | if (!ppath.basename(component).match(VALID_COMPONENT))
1310 | throw new Error(`Assertion failed: Virtual components must be ended by an hexadecimal hash`);
1311 | const target = ppath.relative(ppath.dirname(base), to);
1312 | const segments = target.split(`/`);
1313 | let depth = 0;
1314 | while (depth < segments.length && segments[depth] === `..`)
1315 | depth += 1;
1316 | const finalSegments = segments.slice(depth);
1317 | const fullVirtualPath = ppath.join(base, component, String(depth), ...finalSegments);
1318 | return fullVirtualPath;
1319 | }
1320 | static resolveVirtual(p) {
1321 | const match = p.match(VIRTUAL_REGEXP);
1322 | if (!match || !match[3] && match[5])
1323 | return p;
1324 | const target = ppath.dirname(match[1]);
1325 | if (!match[3] || !match[4])
1326 | return target;
1327 | const isnum = NUMBER_REGEXP.test(match[4]);
1328 | if (!isnum)
1329 | return p;
1330 | const depth = Number(match[4]);
1331 | const backstep = `../`.repeat(depth);
1332 | const subpath = match[5] || `.`;
1333 | return VirtualFS.resolveVirtual(ppath.join(target, backstep, subpath));
1334 | }
1335 | getExtractHint(hints) {
1336 | return this.baseFs.getExtractHint(hints);
1337 | }
1338 | getRealPath() {
1339 | return this.baseFs.getRealPath();
1340 | }
1341 | realpathSync(p) {
1342 | const match = p.match(VIRTUAL_REGEXP);
1343 | if (!match)
1344 | return this.baseFs.realpathSync(p);
1345 | if (!match[5])
1346 | return p;
1347 | const realpath = this.baseFs.realpathSync(this.mapToBase(p));
1348 | return VirtualFS.makeVirtualPath(match[1], match[3], realpath);
1349 | }
1350 | async realpathPromise(p) {
1351 | const match = p.match(VIRTUAL_REGEXP);
1352 | if (!match)
1353 | return await this.baseFs.realpathPromise(p);
1354 | if (!match[5])
1355 | return p;
1356 | const realpath = await this.baseFs.realpathPromise(this.mapToBase(p));
1357 | return VirtualFS.makeVirtualPath(match[1], match[3], realpath);
1358 | }
1359 | mapToBase(p) {
1360 | if (p === ``)
1361 | return p;
1362 | if (this.pathUtils.isAbsolute(p))
1363 | return VirtualFS.resolveVirtual(p);
1364 | const resolvedRoot = VirtualFS.resolveVirtual(this.baseFs.resolve(PortablePath.dot));
1365 | const resolvedP = VirtualFS.resolveVirtual(this.baseFs.resolve(p));
1366 | return ppath.relative(resolvedRoot, resolvedP) || PortablePath.dot;
1367 | }
1368 | mapFromBase(p) {
1369 | return p;
1370 | }
1371 | }
1372 |
1373 | const [major, minor] = process.versions.node.split(`.`).map((value) => parseInt(value, 10));
1374 | const WATCH_MODE_MESSAGE_USES_ARRAYS = major > 19 || major === 19 && minor >= 2 || major === 18 && minor >= 13;
1375 | const HAS_LAZY_LOADED_TRANSLATORS = major > 19 || major === 19 && minor >= 3;
1376 |
1377 | function readPackageScope(checkPath) {
1378 | const rootSeparatorIndex = checkPath.indexOf(npath.sep);
1379 | let separatorIndex;
1380 | do {
1381 | separatorIndex = checkPath.lastIndexOf(npath.sep);
1382 | checkPath = checkPath.slice(0, separatorIndex);
1383 | if (checkPath.endsWith(`${npath.sep}node_modules`))
1384 | return false;
1385 | const pjson = readPackage(checkPath + npath.sep);
1386 | if (pjson) {
1387 | return {
1388 | data: pjson,
1389 | path: checkPath
1390 | };
1391 | }
1392 | } while (separatorIndex > rootSeparatorIndex);
1393 | return false;
1394 | }
1395 | function readPackage(requestPath) {
1396 | const jsonPath = npath.resolve(requestPath, `package.json`);
1397 | if (!fs.existsSync(jsonPath))
1398 | return null;
1399 | return JSON.parse(fs.readFileSync(jsonPath, `utf8`));
1400 | }
1401 |
1402 | async function tryReadFile$1(path2) {
1403 | try {
1404 | return await fs.promises.readFile(path2, `utf8`);
1405 | } catch (error) {
1406 | if (error.code === `ENOENT`)
1407 | return null;
1408 | throw error;
1409 | }
1410 | }
1411 | function tryParseURL(str, base) {
1412 | try {
1413 | return new URL$1(str, base);
1414 | } catch {
1415 | return null;
1416 | }
1417 | }
1418 | let entrypointPath = null;
1419 | function setEntrypointPath(file) {
1420 | entrypointPath = file;
1421 | }
1422 | function getFileFormat(filepath) {
1423 | const ext = path.extname(filepath);
1424 | switch (ext) {
1425 | case `.mjs`: {
1426 | return `module`;
1427 | }
1428 | case `.cjs`: {
1429 | return `commonjs`;
1430 | }
1431 | case `.wasm`: {
1432 | throw new Error(
1433 | `Unknown file extension ".wasm" for ${filepath}`
1434 | );
1435 | }
1436 | case `.json`: {
1437 | return `json`;
1438 | }
1439 | case `.js`: {
1440 | const pkg = readPackageScope(filepath);
1441 | if (!pkg)
1442 | return `commonjs`;
1443 | return pkg.data.type ?? `commonjs`;
1444 | }
1445 | default: {
1446 | if (entrypointPath !== filepath)
1447 | return null;
1448 | const pkg = readPackageScope(filepath);
1449 | if (!pkg)
1450 | return `commonjs`;
1451 | if (pkg.data.type === `module`)
1452 | return null;
1453 | return pkg.data.type ?? `commonjs`;
1454 | }
1455 | }
1456 | }
1457 |
1458 | async function load$1(urlString, context, nextLoad) {
1459 | const url = tryParseURL(urlString);
1460 | if (url?.protocol !== `file:`)
1461 | return nextLoad(urlString, context, nextLoad);
1462 | const filePath = fileURLToPath(url);
1463 | const format = getFileFormat(filePath);
1464 | if (!format)
1465 | return nextLoad(urlString, context, nextLoad);
1466 | if (format === `json` && context.importAssertions?.type !== `json`) {
1467 | const err = new TypeError(`[ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module "${urlString}" needs an import assertion of type "json"`);
1468 | err.code = `ERR_IMPORT_ASSERTION_TYPE_MISSING`;
1469 | throw err;
1470 | }
1471 | if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
1472 | const pathToSend = pathToFileURL(
1473 | npath.fromPortablePath(
1474 | VirtualFS.resolveVirtual(npath.toPortablePath(filePath))
1475 | )
1476 | ).href;
1477 | process.send({
1478 | "watch:import": WATCH_MODE_MESSAGE_USES_ARRAYS ? [pathToSend] : pathToSend
1479 | });
1480 | }
1481 | return {
1482 | format,
1483 | source: format === `commonjs` ? void 0 : await fs.promises.readFile(filePath, `utf8`),
1484 | shortCircuit: true
1485 | };
1486 | }
1487 |
1488 | const ArrayIsArray = Array.isArray;
1489 | const JSONStringify = JSON.stringify;
1490 | const ObjectGetOwnPropertyNames = Object.getOwnPropertyNames;
1491 | const ObjectPrototypeHasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
1492 | const RegExpPrototypeExec = (obj, string) => RegExp.prototype.exec.call(obj, string);
1493 | const RegExpPrototypeSymbolReplace = (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest);
1494 | const StringPrototypeEndsWith = (str, ...rest) => String.prototype.endsWith.apply(str, rest);
1495 | const StringPrototypeIncludes = (str, ...rest) => String.prototype.includes.apply(str, rest);
1496 | const StringPrototypeLastIndexOf = (str, ...rest) => String.prototype.lastIndexOf.apply(str, rest);
1497 | const StringPrototypeIndexOf = (str, ...rest) => String.prototype.indexOf.apply(str, rest);
1498 | const StringPrototypeReplace = (str, ...rest) => String.prototype.replace.apply(str, rest);
1499 | const StringPrototypeSlice = (str, ...rest) => String.prototype.slice.apply(str, rest);
1500 | const StringPrototypeStartsWith = (str, ...rest) => String.prototype.startsWith.apply(str, rest);
1501 | const SafeMap = Map;
1502 | const JSONParse = JSON.parse;
1503 |
1504 | function createErrorType(code, messageCreator, errorType) {
1505 | return class extends errorType {
1506 | constructor(...args) {
1507 | super(messageCreator(...args));
1508 | this.code = code;
1509 | this.name = `${errorType.name} [${code}]`;
1510 | }
1511 | };
1512 | }
1513 | const ERR_PACKAGE_IMPORT_NOT_DEFINED = createErrorType(
1514 | `ERR_PACKAGE_IMPORT_NOT_DEFINED`,
1515 | (specifier, packagePath, base) => {
1516 | return `Package import specifier "${specifier}" is not defined${packagePath ? ` in package ${packagePath}package.json` : ``} imported from ${base}`;
1517 | },
1518 | TypeError
1519 | );
1520 | const ERR_INVALID_MODULE_SPECIFIER = createErrorType(
1521 | `ERR_INVALID_MODULE_SPECIFIER`,
1522 | (request, reason, base = void 0) => {
1523 | return `Invalid module "${request}" ${reason}${base ? ` imported from ${base}` : ``}`;
1524 | },
1525 | TypeError
1526 | );
1527 | const ERR_INVALID_PACKAGE_TARGET = createErrorType(
1528 | `ERR_INVALID_PACKAGE_TARGET`,
1529 | (pkgPath, key, target, isImport = false, base = void 0) => {
1530 | const relError = typeof target === `string` && !isImport && target.length && !StringPrototypeStartsWith(target, `./`);
1531 | if (key === `.`) {
1532 | assert(isImport === false);
1533 | return `Invalid "exports" main target ${JSONStringify(target)} defined in the package config ${pkgPath}package.json${base ? ` imported from ${base}` : ``}${relError ? `; targets must start with "./"` : ``}`;
1534 | }
1535 | return `Invalid "${isImport ? `imports` : `exports`}" target ${JSONStringify(
1536 | target
1537 | )} defined for '${key}' in the package config ${pkgPath}package.json${base ? ` imported from ${base}` : ``}${relError ? `; targets must start with "./"` : ``}`;
1538 | },
1539 | Error
1540 | );
1541 | const ERR_INVALID_PACKAGE_CONFIG = createErrorType(
1542 | `ERR_INVALID_PACKAGE_CONFIG`,
1543 | (path, base, message) => {
1544 | return `Invalid package config ${path}${base ? ` while importing ${base}` : ``}${message ? `. ${message}` : ``}`;
1545 | },
1546 | Error
1547 | );
1548 |
1549 | function filterOwnProperties(source, keys) {
1550 | const filtered = /* @__PURE__ */ Object.create(null);
1551 | for (let i = 0; i < keys.length; i++) {
1552 | const key = keys[i];
1553 | if (ObjectPrototypeHasOwnProperty(source, key)) {
1554 | filtered[key] = source[key];
1555 | }
1556 | }
1557 | return filtered;
1558 | }
1559 |
1560 | const packageJSONCache = new SafeMap();
1561 | function getPackageConfig(path, specifier, base, readFileSyncFn) {
1562 | const existing = packageJSONCache.get(path);
1563 | if (existing !== void 0) {
1564 | return existing;
1565 | }
1566 | const source = readFileSyncFn(path);
1567 | if (source === void 0) {
1568 | const packageConfig2 = {
1569 | pjsonPath: path,
1570 | exists: false,
1571 | main: void 0,
1572 | name: void 0,
1573 | type: "none",
1574 | exports: void 0,
1575 | imports: void 0
1576 | };
1577 | packageJSONCache.set(path, packageConfig2);
1578 | return packageConfig2;
1579 | }
1580 | let packageJSON;
1581 | try {
1582 | packageJSON = JSONParse(source);
1583 | } catch (error) {
1584 | throw new ERR_INVALID_PACKAGE_CONFIG(
1585 | path,
1586 | (base ? `"${specifier}" from ` : "") + fileURLToPath(base || specifier),
1587 | error.message
1588 | );
1589 | }
1590 | let { imports, main, name, type } = filterOwnProperties(packageJSON, [
1591 | "imports",
1592 | "main",
1593 | "name",
1594 | "type"
1595 | ]);
1596 | const exports = ObjectPrototypeHasOwnProperty(packageJSON, "exports") ? packageJSON.exports : void 0;
1597 | if (typeof imports !== "object" || imports === null) {
1598 | imports = void 0;
1599 | }
1600 | if (typeof main !== "string") {
1601 | main = void 0;
1602 | }
1603 | if (typeof name !== "string") {
1604 | name = void 0;
1605 | }
1606 | if (type !== "module" && type !== "commonjs") {
1607 | type = "none";
1608 | }
1609 | const packageConfig = {
1610 | pjsonPath: path,
1611 | exists: true,
1612 | main,
1613 | name,
1614 | type,
1615 | exports,
1616 | imports
1617 | };
1618 | packageJSONCache.set(path, packageConfig);
1619 | return packageConfig;
1620 | }
1621 | function getPackageScopeConfig(resolved, readFileSyncFn) {
1622 | let packageJSONUrl = new URL("./package.json", resolved);
1623 | while (true) {
1624 | const packageJSONPath2 = packageJSONUrl.pathname;
1625 | if (StringPrototypeEndsWith(packageJSONPath2, "node_modules/package.json")) {
1626 | break;
1627 | }
1628 | const packageConfig2 = getPackageConfig(
1629 | fileURLToPath(packageJSONUrl),
1630 | resolved,
1631 | void 0,
1632 | readFileSyncFn
1633 | );
1634 | if (packageConfig2.exists) {
1635 | return packageConfig2;
1636 | }
1637 | const lastPackageJSONUrl = packageJSONUrl;
1638 | packageJSONUrl = new URL("../package.json", packageJSONUrl);
1639 | if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) {
1640 | break;
1641 | }
1642 | }
1643 | const packageJSONPath = fileURLToPath(packageJSONUrl);
1644 | const packageConfig = {
1645 | pjsonPath: packageJSONPath,
1646 | exists: false,
1647 | main: void 0,
1648 | name: void 0,
1649 | type: "none",
1650 | exports: void 0,
1651 | imports: void 0
1652 | };
1653 | packageJSONCache.set(packageJSONPath, packageConfig);
1654 | return packageConfig;
1655 | }
1656 |
1657 | /**
1658 | @license
1659 | Copyright Node.js contributors. All rights reserved.
1660 |
1661 | Permission is hereby granted, free of charge, to any person obtaining a copy
1662 | of this software and associated documentation files (the "Software"), to
1663 | deal in the Software without restriction, including without limitation the
1664 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
1665 | sell copies of the Software, and to permit persons to whom the Software is
1666 | furnished to do so, subject to the following conditions:
1667 |
1668 | The above copyright notice and this permission notice shall be included in
1669 | all copies or substantial portions of the Software.
1670 |
1671 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1672 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1673 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1674 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1675 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1676 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
1677 | IN THE SOFTWARE.
1678 | */
1679 | function throwImportNotDefined(specifier, packageJSONUrl, base) {
1680 | throw new ERR_PACKAGE_IMPORT_NOT_DEFINED(
1681 | specifier,
1682 | packageJSONUrl && fileURLToPath(new URL(".", packageJSONUrl)),
1683 | fileURLToPath(base)
1684 | );
1685 | }
1686 | function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) {
1687 | const reason = `request is not a valid subpath for the "${internal ? "imports" : "exports"}" resolution of ${fileURLToPath(packageJSONUrl)}`;
1688 | throw new ERR_INVALID_MODULE_SPECIFIER(
1689 | subpath,
1690 | reason,
1691 | base && fileURLToPath(base)
1692 | );
1693 | }
1694 | function throwInvalidPackageTarget(subpath, target, packageJSONUrl, internal, base) {
1695 | if (typeof target === "object" && target !== null) {
1696 | target = JSONStringify(target, null, "");
1697 | } else {
1698 | target = `${target}`;
1699 | }
1700 | throw new ERR_INVALID_PACKAGE_TARGET(
1701 | fileURLToPath(new URL(".", packageJSONUrl)),
1702 | subpath,
1703 | target,
1704 | internal,
1705 | base && fileURLToPath(base)
1706 | );
1707 | }
1708 | const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
1709 | const patternRegEx = /\*/g;
1710 | function resolvePackageTargetString(target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
1711 | if (subpath !== "" && !pattern && target[target.length - 1] !== "/")
1712 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
1713 | if (!StringPrototypeStartsWith(target, "./")) {
1714 | if (internal && !StringPrototypeStartsWith(target, "../") && !StringPrototypeStartsWith(target, "/")) {
1715 | let isURL = false;
1716 | try {
1717 | new URL(target);
1718 | isURL = true;
1719 | } catch {
1720 | }
1721 | if (!isURL) {
1722 | const exportTarget = pattern ? RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : target + subpath;
1723 | return exportTarget;
1724 | }
1725 | }
1726 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
1727 | }
1728 | if (RegExpPrototypeExec(
1729 | invalidSegmentRegEx,
1730 | StringPrototypeSlice(target, 2)
1731 | ) !== null)
1732 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
1733 | const resolved = new URL(target, packageJSONUrl);
1734 | const resolvedPath = resolved.pathname;
1735 | const packagePath = new URL(".", packageJSONUrl).pathname;
1736 | if (!StringPrototypeStartsWith(resolvedPath, packagePath))
1737 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
1738 | if (subpath === "")
1739 | return resolved;
1740 | if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) {
1741 | const request = pattern ? StringPrototypeReplace(match, "*", () => subpath) : match + subpath;
1742 | throwInvalidSubpath(request, packageJSONUrl, internal, base);
1743 | }
1744 | if (pattern) {
1745 | return new URL(
1746 | RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath)
1747 | );
1748 | }
1749 | return new URL(subpath, resolved);
1750 | }
1751 | function isArrayIndex(key) {
1752 | const keyNum = +key;
1753 | if (`${keyNum}` !== key)
1754 | return false;
1755 | return keyNum >= 0 && keyNum < 4294967295;
1756 | }
1757 | function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, base, pattern, internal, conditions) {
1758 | if (typeof target === "string") {
1759 | return resolvePackageTargetString(
1760 | target,
1761 | subpath,
1762 | packageSubpath,
1763 | packageJSONUrl,
1764 | base,
1765 | pattern,
1766 | internal);
1767 | } else if (ArrayIsArray(target)) {
1768 | if (target.length === 0) {
1769 | return null;
1770 | }
1771 | let lastException;
1772 | for (let i = 0; i < target.length; i++) {
1773 | const targetItem = target[i];
1774 | let resolveResult;
1775 | try {
1776 | resolveResult = resolvePackageTarget(
1777 | packageJSONUrl,
1778 | targetItem,
1779 | subpath,
1780 | packageSubpath,
1781 | base,
1782 | pattern,
1783 | internal,
1784 | conditions
1785 | );
1786 | } catch (e) {
1787 | lastException = e;
1788 | if (e.code === "ERR_INVALID_PACKAGE_TARGET") {
1789 | continue;
1790 | }
1791 | throw e;
1792 | }
1793 | if (resolveResult === void 0) {
1794 | continue;
1795 | }
1796 | if (resolveResult === null) {
1797 | lastException = null;
1798 | continue;
1799 | }
1800 | return resolveResult;
1801 | }
1802 | if (lastException === void 0 || lastException === null)
1803 | return lastException;
1804 | throw lastException;
1805 | } else if (typeof target === "object" && target !== null) {
1806 | const keys = ObjectGetOwnPropertyNames(target);
1807 | for (let i = 0; i < keys.length; i++) {
1808 | const key = keys[i];
1809 | if (isArrayIndex(key)) {
1810 | throw new ERR_INVALID_PACKAGE_CONFIG(
1811 | fileURLToPath(packageJSONUrl),
1812 | base,
1813 | '"exports" cannot contain numeric property keys.'
1814 | );
1815 | }
1816 | }
1817 | for (let i = 0; i < keys.length; i++) {
1818 | const key = keys[i];
1819 | if (key === "default" || conditions.has(key)) {
1820 | const conditionalTarget = target[key];
1821 | const resolveResult = resolvePackageTarget(
1822 | packageJSONUrl,
1823 | conditionalTarget,
1824 | subpath,
1825 | packageSubpath,
1826 | base,
1827 | pattern,
1828 | internal,
1829 | conditions
1830 | );
1831 | if (resolveResult === void 0)
1832 | continue;
1833 | return resolveResult;
1834 | }
1835 | }
1836 | return void 0;
1837 | } else if (target === null) {
1838 | return null;
1839 | }
1840 | throwInvalidPackageTarget(
1841 | packageSubpath,
1842 | target,
1843 | packageJSONUrl,
1844 | internal,
1845 | base
1846 | );
1847 | }
1848 | function patternKeyCompare(a, b) {
1849 | const aPatternIndex = StringPrototypeIndexOf(a, "*");
1850 | const bPatternIndex = StringPrototypeIndexOf(b, "*");
1851 | const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
1852 | const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
1853 | if (baseLenA > baseLenB)
1854 | return -1;
1855 | if (baseLenB > baseLenA)
1856 | return 1;
1857 | if (aPatternIndex === -1)
1858 | return 1;
1859 | if (bPatternIndex === -1)
1860 | return -1;
1861 | if (a.length > b.length)
1862 | return -1;
1863 | if (b.length > a.length)
1864 | return 1;
1865 | return 0;
1866 | }
1867 | function packageImportsResolve({ name, base, conditions, readFileSyncFn }) {
1868 | if (name === "#" || StringPrototypeStartsWith(name, "#/") || StringPrototypeEndsWith(name, "/")) {
1869 | const reason = "is not a valid internal imports specifier name";
1870 | throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base));
1871 | }
1872 | let packageJSONUrl;
1873 | const packageConfig = getPackageScopeConfig(base, readFileSyncFn);
1874 | if (packageConfig.exists) {
1875 | packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
1876 | const imports = packageConfig.imports;
1877 | if (imports) {
1878 | if (ObjectPrototypeHasOwnProperty(imports, name) && !StringPrototypeIncludes(name, "*")) {
1879 | const resolveResult = resolvePackageTarget(
1880 | packageJSONUrl,
1881 | imports[name],
1882 | "",
1883 | name,
1884 | base,
1885 | false,
1886 | true,
1887 | conditions
1888 | );
1889 | if (resolveResult != null) {
1890 | return resolveResult;
1891 | }
1892 | } else {
1893 | let bestMatch = "";
1894 | let bestMatchSubpath;
1895 | const keys = ObjectGetOwnPropertyNames(imports);
1896 | for (let i = 0; i < keys.length; i++) {
1897 | const key = keys[i];
1898 | const patternIndex = StringPrototypeIndexOf(key, "*");
1899 | if (patternIndex !== -1 && StringPrototypeStartsWith(
1900 | name,
1901 | StringPrototypeSlice(key, 0, patternIndex)
1902 | )) {
1903 | const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
1904 | if (name.length >= key.length && StringPrototypeEndsWith(name, patternTrailer) && patternKeyCompare(bestMatch, key) === 1 && StringPrototypeLastIndexOf(key, "*") === patternIndex) {
1905 | bestMatch = key;
1906 | bestMatchSubpath = StringPrototypeSlice(
1907 | name,
1908 | patternIndex,
1909 | name.length - patternTrailer.length
1910 | );
1911 | }
1912 | }
1913 | }
1914 | if (bestMatch) {
1915 | const target = imports[bestMatch];
1916 | const resolveResult = resolvePackageTarget(
1917 | packageJSONUrl,
1918 | target,
1919 | bestMatchSubpath,
1920 | bestMatch,
1921 | base,
1922 | true,
1923 | true,
1924 | conditions
1925 | );
1926 | if (resolveResult != null) {
1927 | return resolveResult;
1928 | }
1929 | }
1930 | }
1931 | }
1932 | }
1933 | throwImportNotDefined(name, packageJSONUrl, base);
1934 | }
1935 |
1936 | const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/;
1937 | const isRelativeRegexp = /^\.{0,2}\//;
1938 | function tryReadFile(filePath) {
1939 | try {
1940 | return fs.readFileSync(filePath, `utf8`);
1941 | } catch (err) {
1942 | if (err.code === `ENOENT`)
1943 | return void 0;
1944 | throw err;
1945 | }
1946 | }
1947 | async function resolvePrivateRequest(specifier, issuer, context, nextResolve) {
1948 | const resolved = packageImportsResolve({
1949 | name: specifier,
1950 | base: pathToFileURL(issuer),
1951 | conditions: new Set(context.conditions),
1952 | readFileSyncFn: tryReadFile
1953 | });
1954 | if (resolved instanceof URL) {
1955 | return { url: resolved.href, shortCircuit: true };
1956 | } else {
1957 | if (resolved.startsWith(`#`))
1958 | throw new Error(`Mapping from one private import to another isn't allowed`);
1959 | return resolve$1(resolved, context, nextResolve);
1960 | }
1961 | }
1962 | async function resolve$1(originalSpecifier, context, nextResolve) {
1963 | const { findPnpApi } = moduleExports;
1964 | if (!findPnpApi || isBuiltin(originalSpecifier))
1965 | return nextResolve(originalSpecifier, context, nextResolve);
1966 | let specifier = originalSpecifier;
1967 | const url = tryParseURL(specifier, isRelativeRegexp.test(specifier) ? context.parentURL : void 0);
1968 | if (url) {
1969 | if (url.protocol !== `file:`)
1970 | return nextResolve(originalSpecifier, context, nextResolve);
1971 | specifier = fileURLToPath(url);
1972 | }
1973 | const { parentURL, conditions = [] } = context;
1974 | const issuer = parentURL && tryParseURL(parentURL)?.protocol === `file:` ? fileURLToPath(parentURL) : process.cwd();
1975 | const pnpapi = findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null);
1976 | if (!pnpapi)
1977 | return nextResolve(originalSpecifier, context, nextResolve);
1978 | if (specifier.startsWith(`#`))
1979 | return resolvePrivateRequest(specifier, issuer, context, nextResolve);
1980 | const dependencyNameMatch = specifier.match(pathRegExp);
1981 | let allowLegacyResolve = false;
1982 | if (dependencyNameMatch) {
1983 | const [, dependencyName, subPath] = dependencyNameMatch;
1984 | if (subPath === `` && dependencyName !== `pnpapi`) {
1985 | const resolved = pnpapi.resolveToUnqualified(`${dependencyName}/package.json`, issuer);
1986 | if (resolved) {
1987 | const content = await tryReadFile$1(resolved);
1988 | if (content) {
1989 | const pkg = JSON.parse(content);
1990 | allowLegacyResolve = pkg.exports == null;
1991 | }
1992 | }
1993 | }
1994 | }
1995 | let result;
1996 | try {
1997 | result = pnpapi.resolveRequest(specifier, issuer, {
1998 | conditions: new Set(conditions),
1999 | extensions: allowLegacyResolve ? void 0 : []
2000 | });
2001 | } catch (err) {
2002 | if (err instanceof Error && `code` in err && err.code === `MODULE_NOT_FOUND`)
2003 | err.code = `ERR_MODULE_NOT_FOUND`;
2004 | throw err;
2005 | }
2006 | if (!result)
2007 | throw new Error(`Resolving '${specifier}' from '${issuer}' failed`);
2008 | const resultURL = pathToFileURL(result);
2009 | if (url) {
2010 | resultURL.search = url.search;
2011 | resultURL.hash = url.hash;
2012 | }
2013 | if (!parentURL)
2014 | setEntrypointPath(fileURLToPath(resultURL));
2015 | return {
2016 | url: resultURL.href,
2017 | shortCircuit: true
2018 | };
2019 | }
2020 |
2021 | if (!HAS_LAZY_LOADED_TRANSLATORS) {
2022 | const binding = process.binding(`fs`);
2023 | const originalfstat = binding.fstat;
2024 | const ZIP_MASK = 4278190080;
2025 | const ZIP_MAGIC = 704643072;
2026 | binding.fstat = function(...args) {
2027 | const [fd, useBigint, req] = args;
2028 | if ((fd & ZIP_MASK) === ZIP_MAGIC && useBigint === false && req === void 0) {
2029 | try {
2030 | const stats = fs.fstatSync(fd);
2031 | return new Float64Array([
2032 | stats.dev,
2033 | stats.mode,
2034 | stats.nlink,
2035 | stats.uid,
2036 | stats.gid,
2037 | stats.rdev,
2038 | stats.blksize,
2039 | stats.ino,
2040 | stats.size,
2041 | stats.blocks
2042 | ]);
2043 | } catch {
2044 | }
2045 | }
2046 | return originalfstat.apply(this, args);
2047 | };
2048 | }
2049 |
2050 | const resolve = resolve$1;
2051 | const load = load$1;
2052 |
2053 | export { load, resolve };
2054 |
--------------------------------------------------------------------------------