build: decouple package deps and introduce yarn workspaces (#7415)

* feat: decouple package deps and introduce yarn workspaces

* update root directory

* fix

* fix scripts

* fix lint

* update path in scripts

* remove yarn.lock files from packages

* ignore workspace

* dummy

* dummy

* remove comment check

* revert workflow changes

* ignore ws when installing gh actions

* remove log

* update path

* fix

* fix typo
This commit is contained in:
Aakansha Doshi 2023-12-12 11:32:51 +05:30 committed by GitHub
parent b7d7ccc929
commit d6cd8b78f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
567 changed files with 5066 additions and 8648 deletions

View file

@ -0,0 +1,11 @@
# Changelog
## [Unreleased]
First release of `@excalidraw/utils` to provide utilities functions.
- Added `exportToBlob` and `exportToSvg` to export an Excalidraw diagram definition, respectively, to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and to a [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) ([#2246](https://github.com/excalidraw/excalidraw/pull/2246))
### Features
- Flip single elements horizontally or vertically [#2520](https://github.com/excalidraw/excalidraw/pull/2520)

99
packages/utils/README.md Normal file
View file

@ -0,0 +1,99 @@
# @excalidraw/utils
## Install
```bash
npm install @excalidraw/utils
```
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
```bash
yarn add @excalidraw/utils
```
## API
### `serializeAsJSON`
See [`serializeAsJSON`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#serializeAsJSON) for API and description.
### `exportToBlob` (async)
Export an Excalidraw diagram to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
### `exportToSvg`
Export an Excalidraw diagram to a [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
## Usage
Excalidraw utils is published as a UMD (Universal Module Definition). If you are using a module bundler (for instance, Webpack), you can import it as an ES6 module:
```js
import { exportToSvg, exportToBlob } from "@excalidraw/utils";
```
To use it in a browser directly:
```html
<script src="https://unpkg.com/@excalidraw/utils@0.1.0/dist/excalidraw-utils.min.js"></script>
<script>
// ExcalidrawUtils is a global variable defined by excalidraw.min.js
const { exportToSvg, exportToBlob } = ExcalidrawUtils;
</script>
```
Here's the `exportToBlob` and `exportToSvg` functions in action:
```js
const excalidrawDiagram = {
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: [
{
id: "vWrqOAfkind2qcm7LDAGZ",
type: "ellipse",
x: 414,
y: 237,
width: 214,
height: 214,
angle: 0,
strokeColor: "#000000",
backgroundColor: "#15aabf",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
groupIds: [],
roundness: null,
seed: 1041657908,
version: 120,
versionNonce: 1188004276,
isDeleted: false,
boundElementIds: null,
},
],
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
},
};
// Export the Excalidraw diagram as SVG string
const svg = exportToSvg(excalidrawDiagram);
console.log(svg.outerHTML);
// Export the Excalidraw diagram as PNG Blob URL
(async () => {
const blob = await exportToBlob({
...excalidrawDiagram,
mimeType: "image/png",
});
const urlCreator = window.URL || window.webkitURL;
console.log(urlCreator.createObjectURL(blob));
})();
```

View file

@ -0,0 +1,100 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exportToSvg > with default arguments 1`] = `
{
"activeEmbeddable": null,
"activeTool": {
"customType": null,
"lastActiveTool": null,
"locked": false,
"type": "selection",
},
"collaborators": Map {},
"contextMenu": null,
"currentChartType": "bar",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFontFamily": 1,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemRoundness": "round",
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"elementsToHighlight": null,
"errorMessage": null,
"exportBackground": true,
"exportEmbedScene": false,
"exportPadding": undefined,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"isBindingEnabled": true,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "name",
"objectsSnapModeEnabled": false,
"openDialog": null,
"openMenu": null,
"openPopup": null,
"openSidebar": null,
"originSnapOffset": {
"x": 0,
"y": 0,
},
"pasteDialog": {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElementId": null,
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": false,
"snapLines": [],
"startBoundElement": null,
"suggestedBindings": [],
"theme": "light",
"toast": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"zenModeEnabled": false,
"zoom": {
"value": 1,
},
}
`;

5
packages/utils/index.js Normal file
View file

@ -0,0 +1,5 @@
export {
exportToBlob,
exportToSvg,
exportToCanvas,
} from "../excalidraw/packages/utils.ts";

View file

@ -0,0 +1,62 @@
{
"name": "@excalidraw/utils",
"version": "0.1.2",
"main": "dist/excalidraw-utils.min.js",
"files": [
"dist/*"
],
"description": "Excalidraw utilities functions",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"keywords": [
"excalidraw",
"excalidraw-utils"
],
"browserslist": {
"production": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all",
"not safari < 12",
"not kaios <= 2.5",
"not edge < 79",
"not chrome < 70",
"not and_uc < 13",
"not samsung < 10"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "7.18.9",
"@babel/plugin-transform-arrow-functions": "7.18.6",
"@babel/plugin-transform-async-to-generator": "7.18.6",
"@babel/plugin-transform-runtime": "7.18.6",
"@babel/plugin-transform-typescript": "7.18.8",
"@babel/preset-env": "7.18.9",
"@babel/preset-typescript": "7.18.6",
"babel-loader": "8.2.5",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "6.7.1",
"file-loader": "6.2.0",
"sass-loader": "13.0.2",
"ts-loader": "9.3.1",
"webpack": "5.76.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw",
"scripts": {
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack"
}
}

View file

@ -0,0 +1,133 @@
import * as utils from "../utils";
import { diagramFactory } from "../excalidraw/tests/fixtures/diagramFixture";
import { vi } from "vitest";
import * as mockedSceneExportUtils from "../excalidraw/scene/export";
import { MIME_TYPES } from "../excalidraw/constants";
const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
describe("exportToCanvas", async () => {
const EXPORT_PADDING = 10;
it("with default arguments", async () => {
const canvas = await utils.exportToCanvas({
...diagramFactory({ elementOverrides: { width: 100, height: 100 } }),
});
expect(canvas.width).toBe(100 + 2 * EXPORT_PADDING);
expect(canvas.height).toBe(100 + 2 * EXPORT_PADDING);
});
it("when custom width and height", async () => {
const canvas = await utils.exportToCanvas({
...diagramFactory({ elementOverrides: { width: 100, height: 100 } }),
getDimensions: () => ({ width: 200, height: 200, scale: 1 }),
});
expect(canvas.width).toBe(200);
expect(canvas.height).toBe(200);
});
});
describe("exportToBlob", async () => {
describe("mime type", () => {
// afterEach(vi.restoreAllMocks);
it("should change image/jpg to image/jpeg", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
getDimensions: (width, height) => ({ width, height, scale: 1 }),
// testing typo in MIME type (jpg → jpeg)
mimeType: "image/jpg",
appState: {
exportBackground: true,
},
});
expect(blob?.type).toBe(MIME_TYPES.jpg);
});
it("should default to image/png", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
});
expect(blob?.type).toBe(MIME_TYPES.png);
});
it("should warn when using quality with image/png", async () => {
const consoleSpy = vi
.spyOn(console, "warn")
.mockImplementationOnce(() => void 0);
await utils.exportToBlob({
...diagramFactory(),
mimeType: MIME_TYPES.png,
quality: 1,
});
expect(consoleSpy).toHaveBeenCalledWith(
`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
);
});
});
});
describe("exportToSvg", () => {
const passedElements = () => exportToSvgSpy.mock.calls[0][0];
const passedOptions = () => exportToSvgSpy.mock.calls[0][1];
afterEach(() => {
vi.clearAllMocks();
});
it("with default arguments", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: { appState: void 0 },
}),
});
const passedOptionsWhenDefault = {
...passedOptions(),
// To avoid varying snapshots
name: "name",
};
expect(passedElements().length).toBe(3);
expect(passedOptionsWhenDefault).toMatchSnapshot();
});
// FIXME the utils.exportToSvg no longer filters out deleted elements.
// It's already supposed to be passed non-deleted elements by we're not
// type-checking for it correctly.
it.skip("with deleted elements", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: { appState: void 0 },
elementOverrides: { isDeleted: true },
}),
});
expect(passedElements().length).toBe(0);
});
it("with exportPadding", async () => {
await utils.exportToSvg({
...diagramFactory({ overrides: { appState: { name: "diagram name" } } }),
exportPadding: 0,
});
expect(passedElements().length).toBe(3);
expect(passedOptions()).toEqual(
expect.objectContaining({ exportPadding: 0 }),
);
});
it("with exportEmbedScene", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: {
appState: { name: "diagram name", exportEmbedScene: true },
},
}),
});
expect(passedElements().length).toBe(3);
expect(passedOptions().exportEmbedScene).toBe(true);
});
});

View file

@ -0,0 +1,67 @@
import { decodePngMetadata, decodeSvgMetadata } from "../excalidraw/data/image";
import { ImportedDataState } from "../excalidraw/data/types";
import * as utils from "../utils";
import { API } from "../excalidraw/tests/helpers/api";
// NOTE this test file is using the actual API, unmocked. Hence splitting it
// from the other test file, because I couldn't figure out how to test
// mocked and unmocked API in the same file.
describe("embedding scene data", () => {
describe("exportToSvg", () => {
it("embedding scene data shouldn't modify them", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const ellipse = API.createElement({ type: "ellipse" });
const sourceElements = [rectangle, ellipse];
const svgNode = await utils.exportToSvg({
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
exportEmbedScene: true,
},
files: null,
});
const svg = svgNode.outerHTML;
const parsedString = await decodeSvgMetadata({ svg });
const importedData: ImportedDataState = JSON.parse(parsedString);
expect(sourceElements.map((x) => x.id)).toEqual(
importedData.elements?.map((el) => el.id),
);
});
});
// skipped because we can't test png encoding right now
// (canvas.toBlob not supported in jsdom)
describe.skip("exportToBlob", () => {
it("embedding scene data shouldn't modify them", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const ellipse = API.createElement({ type: "ellipse" });
const sourceElements = [rectangle, ellipse];
const blob = await utils.exportToBlob({
mimeType: "image/png",
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
exportEmbedScene: true,
},
files: null,
});
const parsedString = await decodePngMetadata(blob);
const importedData: ImportedDataState = JSON.parse(parsedString);
expect(sourceElements.map((x) => x.id)).toEqual(
importedData.elements?.map((el) => el.id),
);
});
});
});

View file

@ -0,0 +1,60 @@
const webpack = require("webpack");
const path = require("path");
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: { "excalidraw-utils.min": "./index.js" },
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
library: "ExcalidrawUtils",
libraryTarget: "umd",
},
resolve: {
extensions: [".tsx", ".ts", ".js", ".css", ".scss"],
},
optimization: {
runtimeChunk: false,
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
exclude: /node_modules/,
use: ["style-loader", { loader: "css-loader" }, "sass-loader"],
},
{
test: /\.(ts|tsx|js)$/,
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, "../tsconfig.prod.json"),
},
},
{
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
plugins: [["@babel/plugin-transform-runtime"]],
},
},
],
},
],
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []),
],
};