mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Master merge first attempt
This commit is contained in:
parent
1e819a2acf
commit
550c874c9b
419 changed files with 24252 additions and 3446 deletions
|
@ -126,6 +126,8 @@ import DebugCanvas, {
|
|||
loadSavedDebugState,
|
||||
} from "./components/DebugCanvas";
|
||||
import { AIComponents } from "./components/AI";
|
||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||
import { isElementLink } from "../packages/excalidraw/element/elementLink";
|
||||
|
||||
polyfill();
|
||||
|
||||
|
@ -847,6 +849,12 @@ const ExcalidrawWrapper = () => {
|
|||
</div>
|
||||
);
|
||||
}}
|
||||
onLinkOpen={(element, event) => {
|
||||
if (element.link && isElementLink(element.link)) {
|
||||
event.preventDefault();
|
||||
excalidrawAPI?.scrollToContent(element.link, { animate: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppMainMenu
|
||||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
|
@ -1125,6 +1133,12 @@ const ExcalidrawWrapper = () => {
|
|||
};
|
||||
|
||||
const ExcalidrawApp = () => {
|
||||
const isCloudExportWindow =
|
||||
window.location.pathname === "/excalidraw-plus-export";
|
||||
if (isCloudExportWindow) {
|
||||
return <ExcalidrawPlusIframeExport />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<Provider unstable_createStore={() => appJotaiStore}>
|
||||
|
|
222
excalidraw-app/ExcalidrawPlusIframeExport.tsx
Normal file
222
excalidraw-app/ExcalidrawPlusIframeExport.tsx
Normal file
|
@ -0,0 +1,222 @@
|
|||
import { useLayoutEffect, useRef } from "react";
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import type {
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../packages/excalidraw/element/types";
|
||||
import type { AppState, BinaryFileData } from "../packages/excalidraw/types";
|
||||
import { ExcalidrawError } from "../packages/excalidraw/errors";
|
||||
import { base64urlToString } from "../packages/excalidraw/data/encode";
|
||||
|
||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||
|
||||
const EXCALIDRAW_PLUS_ORIGIN = import.meta.env.VITE_APP_PLUS_APP;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// outgoing message
|
||||
// -----------------------------------------------------------------------------
|
||||
type MESSAGE_REQUEST_SCENE = {
|
||||
type: "REQUEST_SCENE";
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
type MESSAGE_FROM_PLUS = MESSAGE_REQUEST_SCENE;
|
||||
|
||||
// incoming messages
|
||||
// -----------------------------------------------------------------------------
|
||||
type MESSAGE_READY = { type: "READY" };
|
||||
type MESSAGE_ERROR = { type: "ERROR"; message: string };
|
||||
type MESSAGE_SCENE_DATA = {
|
||||
type: "SCENE_DATA";
|
||||
elements: OrderedExcalidrawElement[];
|
||||
appState: Pick<AppState, "viewBackgroundColor">;
|
||||
files: { loadedFiles: BinaryFileData[]; erroredFiles: Map<FileId, true> };
|
||||
};
|
||||
|
||||
type MESSAGE_FROM_EDITOR = MESSAGE_ERROR | MESSAGE_SCENE_DATA | MESSAGE_READY;
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const parseSceneData = async ({
|
||||
rawElementsString,
|
||||
rawAppStateString,
|
||||
}: {
|
||||
rawElementsString: string | null;
|
||||
rawAppStateString: string | null;
|
||||
}): Promise<MESSAGE_SCENE_DATA> => {
|
||||
if (!rawElementsString || !rawAppStateString) {
|
||||
throw new ExcalidrawError("Elements or appstate is missing.");
|
||||
}
|
||||
|
||||
try {
|
||||
const elements = JSON.parse(
|
||||
rawElementsString,
|
||||
) as OrderedExcalidrawElement[];
|
||||
|
||||
if (!elements.length) {
|
||||
throw new ExcalidrawError("Scene is empty, nothing to export.");
|
||||
}
|
||||
|
||||
const appState = JSON.parse(rawAppStateString) as Pick<
|
||||
AppState,
|
||||
"viewBackgroundColor"
|
||||
>;
|
||||
|
||||
const fileIds = elements.reduce((acc, el) => {
|
||||
if ("fileId" in el && el.fileId) {
|
||||
acc.push(el.fileId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as FileId[]);
|
||||
|
||||
const files = await LocalData.fileStorage.getFiles(fileIds);
|
||||
|
||||
return {
|
||||
type: "SCENE_DATA",
|
||||
elements,
|
||||
appState,
|
||||
files,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw error instanceof ExcalidrawError
|
||||
? error
|
||||
: new ExcalidrawError("Failed to parse scene data.");
|
||||
}
|
||||
};
|
||||
|
||||
const verifyJWT = async ({
|
||||
token,
|
||||
publicKey,
|
||||
}: {
|
||||
token: string;
|
||||
publicKey: string;
|
||||
}) => {
|
||||
try {
|
||||
if (!publicKey) {
|
||||
throw new ExcalidrawError("Public key is undefined");
|
||||
}
|
||||
|
||||
const [header, payload, signature] = token.split(".");
|
||||
|
||||
if (!header || !payload || !signature) {
|
||||
throw new ExcalidrawError("Invalid JWT format");
|
||||
}
|
||||
|
||||
// JWT is using Base64URL encoding
|
||||
const decodedPayload = base64urlToString(payload);
|
||||
const decodedSignature = base64urlToString(signature);
|
||||
|
||||
const data = `${header}.${payload}`;
|
||||
const signatureArrayBuffer = Uint8Array.from(decodedSignature, (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
const keyData = publicKey.replace(/-----\w+ PUBLIC KEY-----/g, "");
|
||||
const keyArrayBuffer = Uint8Array.from(atob(keyData), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
keyArrayBuffer,
|
||||
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"RSASSA-PKCS1-v1_5",
|
||||
key,
|
||||
signatureArrayBuffer,
|
||||
new TextEncoder().encode(data),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid JWT");
|
||||
}
|
||||
|
||||
const parsedPayload = JSON.parse(decodedPayload);
|
||||
|
||||
// Check for expiration
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (parsedPayload.exp && parsedPayload.exp < currentTime) {
|
||||
throw new Error("JWT has expired");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to verify JWT:", error);
|
||||
throw new Error(error instanceof Error ? error.message : "Invalid JWT");
|
||||
}
|
||||
};
|
||||
|
||||
export const ExcalidrawPlusIframeExport = () => {
|
||||
const readyRef = useRef(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleMessage = async (event: MessageEvent<MESSAGE_FROM_PLUS>) => {
|
||||
if (event.origin !== EXCALIDRAW_PLUS_ORIGIN) {
|
||||
throw new ExcalidrawError("Invalid origin");
|
||||
}
|
||||
|
||||
if (event.data.type === EVENT_REQUEST_SCENE) {
|
||||
if (!event.data.jwt) {
|
||||
throw new ExcalidrawError("JWT is missing");
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
await verifyJWT({
|
||||
token: event.data.jwt,
|
||||
publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to verify JWT: ${error.message}`);
|
||||
throw new ExcalidrawError("Failed to verify JWT");
|
||||
}
|
||||
|
||||
const parsedSceneData: MESSAGE_SCENE_DATA = await parseSceneData({
|
||||
rawAppStateString: localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||
),
|
||||
rawElementsString: localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||
),
|
||||
});
|
||||
|
||||
event.source!.postMessage(parsedSceneData, {
|
||||
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||
});
|
||||
} catch (error) {
|
||||
const responseData: MESSAGE_ERROR = {
|
||||
type: "ERROR",
|
||||
message:
|
||||
error instanceof ExcalidrawError
|
||||
? error.message
|
||||
: "Failed to export scene data",
|
||||
};
|
||||
event.source!.postMessage(responseData, {
|
||||
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
// so we don't send twice in StrictMode
|
||||
if (!readyRef.current) {
|
||||
readyRef.current = true;
|
||||
const message: MESSAGE_FROM_EDITOR = { type: "READY" };
|
||||
window.parent.postMessage(message, EXCALIDRAW_PLUS_ORIGIN);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Since this component is expected to run in a hidden iframe on Excaildraw+,
|
||||
// it doesn't need to render anything. All the data we need is available in
|
||||
// LocalStorage and IndexedDB. It only needs to handle the messaging between
|
||||
// the parent window and the iframe with the relevant data.
|
||||
return null;
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
import type {
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
SocketId,
|
||||
} from "../../packages/excalidraw/types";
|
||||
|
@ -9,6 +10,7 @@ import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
|||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
|
@ -157,7 +159,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
throw new AbortError();
|
||||
}
|
||||
|
||||
return saveFilesToFirebase({
|
||||
const { savedFiles, erroredFiles } = await saveFilesToFirebase({
|
||||
prefix: `${FIREBASE_STORAGE_PREFIXES.collabFiles}/${roomId}`,
|
||||
files: await encodeFilesForUpload({
|
||||
files: addedFiles,
|
||||
|
@ -165,6 +167,29 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
maxBytes: FILE_UPLOAD_MAX_BYTES,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
savedFiles: savedFiles.reduce(
|
||||
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||
const fileData = addedFiles.get(id);
|
||||
if (fileData) {
|
||||
acc.set(id, fileData);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Map(),
|
||||
),
|
||||
erroredFiles: erroredFiles.reduce(
|
||||
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||
const fileData = addedFiles.get(id);
|
||||
if (fileData) {
|
||||
acc.set(id, fileData);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Map(),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
this.excalidrawAPI = props.excalidrawAPI;
|
||||
|
@ -394,7 +419,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
.filter((element) => {
|
||||
return (
|
||||
isInitializedImageElement(element) &&
|
||||
!this.fileManager.isFileHandled(element.fileId) &&
|
||||
!this.fileManager.isFileTracked(element.fileId) &&
|
||||
!element.isDeleted &&
|
||||
(opts.forceFetchFiles
|
||||
? element.status !== "pending" ||
|
||||
|
|
|
@ -8,7 +8,7 @@ export const EncryptedIcon = () => {
|
|||
return (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
|
|
|
@ -16,14 +16,26 @@ import type {
|
|||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
|
||||
type FileVersion = Required<BinaryFileData>["version"];
|
||||
|
||||
export class FileManager {
|
||||
/** files being fetched */
|
||||
private fetchingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
||||
private erroredFiles_fetch = new Map<
|
||||
ExcalidrawImageElement["fileId"],
|
||||
true
|
||||
>();
|
||||
/** files being saved */
|
||||
private savingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
||||
private savingFiles = new Map<
|
||||
ExcalidrawImageElement["fileId"],
|
||||
FileVersion
|
||||
>();
|
||||
/* files already saved to persistent storage */
|
||||
private savedFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
||||
private erroredFiles = new Map<ExcalidrawImageElement["fileId"], true>();
|
||||
private savedFiles = new Map<ExcalidrawImageElement["fileId"], FileVersion>();
|
||||
private erroredFiles_save = new Map<
|
||||
ExcalidrawImageElement["fileId"],
|
||||
FileVersion
|
||||
>();
|
||||
|
||||
private _getFiles;
|
||||
private _saveFiles;
|
||||
|
@ -37,8 +49,8 @@ export class FileManager {
|
|||
erroredFiles: Map<FileId, true>;
|
||||
}>;
|
||||
saveFiles: (data: { addedFiles: Map<FileId, BinaryFileData> }) => Promise<{
|
||||
savedFiles: Map<FileId, true>;
|
||||
erroredFiles: Map<FileId, true>;
|
||||
savedFiles: Map<FileId, BinaryFileData>;
|
||||
erroredFiles: Map<FileId, BinaryFileData>;
|
||||
}>;
|
||||
}) {
|
||||
this._getFiles = getFiles;
|
||||
|
@ -46,19 +58,28 @@ export class FileManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* returns whether file is already saved or being processed
|
||||
* returns whether file is saved/errored, or being processed
|
||||
*/
|
||||
isFileHandled = (id: FileId) => {
|
||||
isFileTracked = (id: FileId) => {
|
||||
return (
|
||||
this.savedFiles.has(id) ||
|
||||
this.fetchingFiles.has(id) ||
|
||||
this.savingFiles.has(id) ||
|
||||
this.erroredFiles.has(id)
|
||||
this.fetchingFiles.has(id) ||
|
||||
this.erroredFiles_fetch.has(id) ||
|
||||
this.erroredFiles_save.has(id)
|
||||
);
|
||||
};
|
||||
|
||||
isFileSaved = (id: FileId) => {
|
||||
return this.savedFiles.has(id);
|
||||
isFileSavedOrBeingSaved = (file: BinaryFileData) => {
|
||||
const fileVersion = this.getFileVersion(file);
|
||||
return (
|
||||
this.savedFiles.get(file.id) === fileVersion ||
|
||||
this.savingFiles.get(file.id) === fileVersion
|
||||
);
|
||||
};
|
||||
|
||||
getFileVersion = (file: BinaryFileData) => {
|
||||
return file.version ?? 1;
|
||||
};
|
||||
|
||||
saveFiles = async ({
|
||||
|
@ -71,13 +92,16 @@ export class FileManager {
|
|||
const addedFiles: Map<FileId, BinaryFileData> = new Map();
|
||||
|
||||
for (const element of elements) {
|
||||
const fileData =
|
||||
isInitializedImageElement(element) && files[element.fileId];
|
||||
|
||||
if (
|
||||
isInitializedImageElement(element) &&
|
||||
files[element.fileId] &&
|
||||
!this.isFileHandled(element.fileId)
|
||||
fileData &&
|
||||
// NOTE if errored during save, won't retry due to this check
|
||||
!this.isFileSavedOrBeingSaved(fileData)
|
||||
) {
|
||||
addedFiles.set(element.fileId, files[element.fileId]);
|
||||
this.savingFiles.set(element.fileId, true);
|
||||
this.savingFiles.set(element.fileId, this.getFileVersion(fileData));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,8 +110,12 @@ export class FileManager {
|
|||
addedFiles,
|
||||
});
|
||||
|
||||
for (const [fileId] of savedFiles) {
|
||||
this.savedFiles.set(fileId, true);
|
||||
for (const [fileId, fileData] of savedFiles) {
|
||||
this.savedFiles.set(fileId, this.getFileVersion(fileData));
|
||||
}
|
||||
|
||||
for (const [fileId, fileData] of erroredFiles) {
|
||||
this.erroredFiles_save.set(fileId, this.getFileVersion(fileData));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -121,10 +149,10 @@ export class FileManager {
|
|||
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
|
||||
|
||||
for (const file of loadedFiles) {
|
||||
this.savedFiles.set(file.id, true);
|
||||
this.savedFiles.set(file.id, this.getFileVersion(file));
|
||||
}
|
||||
for (const [fileId] of erroredFiles) {
|
||||
this.erroredFiles.set(fileId, true);
|
||||
this.erroredFiles_fetch.set(fileId, true);
|
||||
}
|
||||
|
||||
return { loadedFiles, erroredFiles };
|
||||
|
@ -160,7 +188,7 @@ export class FileManager {
|
|||
): element is InitializedExcalidrawImageElement => {
|
||||
return (
|
||||
isInitializedImageElement(element) &&
|
||||
this.isFileSaved(element.fileId) &&
|
||||
this.savedFiles.has(element.fileId) &&
|
||||
element.status === "pending"
|
||||
);
|
||||
};
|
||||
|
@ -169,7 +197,8 @@ export class FileManager {
|
|||
this.fetchingFiles.clear();
|
||||
this.savingFiles.clear();
|
||||
this.savedFiles.clear();
|
||||
this.erroredFiles.clear();
|
||||
this.erroredFiles_fetch.clear();
|
||||
this.erroredFiles_save.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -183,8 +183,8 @@ export class LocalData {
|
|||
);
|
||||
},
|
||||
async saveFiles({ addedFiles }) {
|
||||
const savedFiles = new Map<FileId, true>();
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
const savedFiles = new Map<FileId, BinaryFileData>();
|
||||
const erroredFiles = new Map<FileId, BinaryFileData>();
|
||||
|
||||
// before we use `storage` event synchronization, let's update the flag
|
||||
// optimistically. Hopefully nothing fails, and an IDB read executed
|
||||
|
@ -195,10 +195,10 @@ export class LocalData {
|
|||
[...addedFiles].map(async ([id, fileData]) => {
|
||||
try {
|
||||
await set(id, fileData, filesStore);
|
||||
savedFiles.set(id, true);
|
||||
savedFiles.set(id, fileData);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
erroredFiles.set(id, true);
|
||||
erroredFiles.set(id, fileData);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -177,8 +177,8 @@ export const saveFilesToFirebase = async ({
|
|||
}) => {
|
||||
const firebase = await loadFirebaseStorage();
|
||||
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
const savedFiles = new Map<FileId, true>();
|
||||
const erroredFiles: FileId[] = [];
|
||||
const savedFiles: FileId[] = [];
|
||||
|
||||
await Promise.all(
|
||||
files.map(async ({ id, buffer }) => {
|
||||
|
@ -194,9 +194,9 @@ export const saveFilesToFirebase = async ({
|
|||
cacheControl: `public, max-age=${FILE_CACHE_MAX_AGE_SEC}`,
|
||||
},
|
||||
);
|
||||
savedFiles.set(id, true);
|
||||
savedFiles.push(id);
|
||||
} catch (error: any) {
|
||||
erroredFiles.set(id, true);
|
||||
erroredFiles.push(id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -54,11 +54,7 @@
|
|||
content="https://excalidraw.com/og-image-3.png"
|
||||
/>
|
||||
|
||||
<!-- General tags -->
|
||||
<meta
|
||||
name="description"
|
||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||
/>
|
||||
<link rel="canonical" href="https://excalidraw.com" />
|
||||
|
||||
<!------------------------------------------------------------------------->
|
||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||
|
@ -124,19 +120,19 @@
|
|||
<!-- Following placeholder is replaced during the build step -->
|
||||
<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->
|
||||
|
||||
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../packages/excalidraw/fonts/fonts.css"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<% } else { %>
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../packages/excalidraw/fonts/assets/fonts.css"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
|
@ -218,6 +214,7 @@
|
|||
</header>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="index.tsx"></script>
|
||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||
<!-- 100% privacy friendly analytics -->
|
||||
<script>
|
||||
// need to load this script dynamically bcs. of iframe embed tracking
|
||||
|
@ -250,5 +247,6 @@
|
|||
}
|
||||
</script>
|
||||
<!-- end LEGACY GOOGLE ANALYTICS -->
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -23,20 +23,20 @@
|
|||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": "18.0.0 - 22.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@sentry/browser": "6.2.5",
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"firebase": "8.3.3",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"jotai": "1.13.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"vite-plugin-html": "3.2.2",
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@sentry/browser": "6.2.5",
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"socket.io-client": "4.7.2"
|
||||
"socket.io-client": "4.7.2",
|
||||
"vite-plugin-html": "3.2.2"
|
||||
},
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"scripts": {
|
||||
|
@ -49,5 +49,8 @@
|
|||
"start:production": "yarn build && yarn serve",
|
||||
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||
"build:preview": "yarn build && vite preview --port 5000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite-plugin-sitemap": "0.7.1"
|
||||
}
|
||||
}
|
||||
|
|
3
excalidraw-app/vite-env.d.ts
vendored
3
excalidraw-app/vite-env.d.ts
vendored
|
@ -29,6 +29,9 @@ interface ImportMetaEnv {
|
|||
// Enable eslint in dev server
|
||||
VITE_APP_ENABLE_ESLINT: string;
|
||||
|
||||
// Enable PWA in dev server
|
||||
VITE_APP_ENABLE_PWA: string;
|
||||
|
||||
VITE_APP_PLUS_LP: string;
|
||||
|
||||
VITE_APP_PLUS_APP: string;
|
||||
|
|
|
@ -5,217 +5,237 @@ import { ViteEjsPlugin } from "vite-plugin-ejs";
|
|||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import checker from "vite-plugin-checker";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
import Sitemap from "vite-plugin-sitemap";
|
||||
import { woff2BrowserPlugin } from "../scripts/woff2/woff2-vite-plugins";
|
||||
|
||||
// To load .env.local variables
|
||||
const envVars = loadEnv("", `../`);
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: Number(envVars.VITE_APP_PORT || 3000),
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
// We need to specify the envDir since now there are no
|
||||
//more located in parallel with the vite.config.ts file but in parent dir
|
||||
envDir: "../",
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames(chunkInfo) {
|
||||
if (chunkInfo?.name?.endsWith(".woff2")) {
|
||||
// put on root so we are flexible about the CDN path
|
||||
return "[name]-[hash][extname]";
|
||||
}
|
||||
|
||||
return "assets/[name]-[hash][extname]";
|
||||
},
|
||||
// Creating separate chunk for locales except for en and percentages.json so they
|
||||
// can be cached at runtime and not merged with
|
||||
// app precache. en.json and percentages.json are needed for first load
|
||||
// or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
|
||||
manualChunks(id) {
|
||||
if (
|
||||
id.includes("packages/excalidraw/locales") &&
|
||||
id.match(/en.json|percentages.json/) === null
|
||||
) {
|
||||
const index = id.indexOf("locales/");
|
||||
// Taking the substring after "locales/"
|
||||
return `locales/${id.substring(index + 8)}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
export default defineConfig(({ mode }) => {
|
||||
// To load .env variables
|
||||
const envVars = loadEnv(mode, `../`);
|
||||
// https://vitejs.dev/config/
|
||||
return {
|
||||
server: {
|
||||
port: Number(envVars.VITE_APP_PORT || 3000),
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
sourcemap: true,
|
||||
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
plugins: [
|
||||
woff2BrowserPlugin(),
|
||||
react(),
|
||||
checker({
|
||||
typescript: true,
|
||||
eslint:
|
||||
envVars.VITE_APP_ENABLE_ESLINT === "false"
|
||||
? undefined
|
||||
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
|
||||
overlay: {
|
||||
initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
|
||||
badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
|
||||
},
|
||||
}),
|
||||
svgrPlugin(),
|
||||
ViteEjsPlugin(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
devOptions: {
|
||||
/* set this flag to true to enable in Development mode */
|
||||
enabled: false,
|
||||
},
|
||||
// We need to specify the envDir since now there are no
|
||||
//more located in parallel with the vite.config.ts file but in parent dir
|
||||
envDir: "../",
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames(chunkInfo) {
|
||||
if (chunkInfo?.name?.endsWith(".woff2")) {
|
||||
const family = chunkInfo.name.split("-")[0];
|
||||
return `fonts/${family}/[name][extname]`;
|
||||
}
|
||||
|
||||
workbox: {
|
||||
// Don't push fonts, locales and wasm to app precache
|
||||
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js", "**/*.wasm-*.js"],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "fonts",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||
},
|
||||
},
|
||||
return "assets/[name]-[hash][extname]";
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp("fonts.css"),
|
||||
handler: "StaleWhileRevalidate",
|
||||
options: {
|
||||
cacheName: "fonts",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp("locales/[^/]+.js"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "locales",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp(".wasm-.+.js"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "wasm",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
short_name: "Excalidraw",
|
||||
name: "Excalidraw",
|
||||
description:
|
||||
"Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
|
||||
icons: [
|
||||
{
|
||||
src: "android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "apple-touch-icon.png",
|
||||
type: "image/png",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "favicon-16x16.png",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
theme_color: "#121212",
|
||||
background_color: "#ffffff",
|
||||
file_handlers: [
|
||||
{
|
||||
action: "/",
|
||||
accept: {
|
||||
"application/vnd.excalidraw+json": [".excalidraw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
share_target: {
|
||||
action: "/web-share-target",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data",
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
name: "file",
|
||||
accept: [
|
||||
"application/vnd.excalidraw+json",
|
||||
"application/json",
|
||||
".excalidraw",
|
||||
],
|
||||
},
|
||||
],
|
||||
// Creating separate chunk for locales except for en and percentages.json so they
|
||||
// can be cached at runtime and not merged with
|
||||
// app precache. en.json and percentages.json are needed for first load
|
||||
// or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
|
||||
manualChunks(id) {
|
||||
if (
|
||||
id.includes("packages/excalidraw/locales") &&
|
||||
id.match(/en.json|percentages.json/) === null
|
||||
) {
|
||||
const index = id.indexOf("locales/");
|
||||
// Taking the substring after "locales/"
|
||||
return `locales/${id.substring(index + 8)}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
screenshots: [
|
||||
{
|
||||
src: "/screenshots/virtual-whiteboard.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/wireframe.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/illustration.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/shapes.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/collaboration.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/export.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
}),
|
||||
],
|
||||
publicDir: "../public",
|
||||
sourcemap: true,
|
||||
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
plugins: [
|
||||
Sitemap({
|
||||
hostname: "https://excalidraw.com",
|
||||
outDir: "build",
|
||||
changefreq: "monthly",
|
||||
// its static in public folder
|
||||
generateRobotsTxt: false,
|
||||
}),
|
||||
woff2BrowserPlugin(),
|
||||
react(),
|
||||
checker({
|
||||
typescript: true,
|
||||
eslint:
|
||||
envVars.VITE_APP_ENABLE_ESLINT === "false"
|
||||
? undefined
|
||||
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
|
||||
overlay: {
|
||||
initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
|
||||
badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
|
||||
},
|
||||
}),
|
||||
svgrPlugin(),
|
||||
ViteEjsPlugin(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
devOptions: {
|
||||
/* set this flag to true to enable in Development mode */
|
||||
enabled: envVars.VITE_APP_ENABLE_PWA === "true",
|
||||
},
|
||||
|
||||
workbox: {
|
||||
// don't precache fonts, locales and separate chunks
|
||||
globIgnores: [
|
||||
"fonts.css",
|
||||
"**/locales/**",
|
||||
"service-worker.js",
|
||||
"**/*.chunk-*.js",
|
||||
],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: new RegExp(".+.woff2"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "fonts",
|
||||
expiration: {
|
||||
maxEntries: 1000,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 90, // 90 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
// 0 to cache "opaque" responses from cross-origin requests (i.e. CDN)
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp("fonts.css"),
|
||||
handler: "StaleWhileRevalidate",
|
||||
options: {
|
||||
cacheName: "fonts",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp("locales/[^/]+.js"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "locales",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp(".chunk-.+.js"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "chunk",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
short_name: "Excalidraw",
|
||||
name: "Excalidraw",
|
||||
description:
|
||||
"Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
|
||||
icons: [
|
||||
{
|
||||
src: "android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "apple-touch-icon.png",
|
||||
type: "image/png",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "favicon-16x16.png",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
start_url: "/",
|
||||
id:"excalidraw",
|
||||
display: "standalone",
|
||||
theme_color: "#121212",
|
||||
background_color: "#ffffff",
|
||||
file_handlers: [
|
||||
{
|
||||
action: "/",
|
||||
accept: {
|
||||
"application/vnd.excalidraw+json": [".excalidraw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
share_target: {
|
||||
action: "/web-share-target",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data",
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
name: "file",
|
||||
accept: [
|
||||
"application/vnd.excalidraw+json",
|
||||
"application/json",
|
||||
".excalidraw",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
screenshots: [
|
||||
{
|
||||
src: "/screenshots/virtual-whiteboard.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/wireframe.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/illustration.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/shapes.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/collaboration.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
{
|
||||
src: "/screenshots/export.png",
|
||||
type: "image/png",
|
||||
sizes: "462x945",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
}),
|
||||
],
|
||||
publicDir: "../public",
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue