miro-export 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,298 @@
1
+ type TextAlignment = "left" | "center" | "right";
2
+ type TextVerticalAlignment = "top" | "middle" | "bottom";
3
+
4
+ export type BoardObjectType =
5
+ | "text"
6
+ | "sticky_note"
7
+ | "shape"
8
+ | "image"
9
+ | "frame"
10
+ | "preview"
11
+ | "card"
12
+ | "app_card"
13
+ | "usm"
14
+ | "kanban"
15
+ | "document"
16
+ | "mockup"
17
+ | "curve"
18
+ | "webscreen"
19
+ | "table"
20
+ | "svg"
21
+ | "emoji"
22
+ | "embed"
23
+ | "connector"
24
+ | "unsupported"
25
+ | "table_text"
26
+ | "rallycard"
27
+ | "stencil"
28
+ | "tag"
29
+ | "code"
30
+ | "red"
31
+ | "stamp"
32
+ | "pipmatrix"
33
+ | "demo_1d_layout"
34
+ | "page"
35
+ | "action_button"
36
+ | "external_diagram"
37
+ | "slide_container"
38
+ | "sdk_custom_widget"
39
+ | "group"
40
+ | "struct_doc"
41
+ | "mindmap";
42
+
43
+ export interface BoardItemBase {
44
+ id?: string;
45
+ type: BoardObjectType;
46
+ }
47
+
48
+ interface BoardObjectBase {
49
+ origin: "center";
50
+ linkedTo?: string;
51
+ connectorIds?: string[];
52
+ groupId?: string;
53
+ relativeTo: "canvas_center" | "parent_top_left" | "parent_center";
54
+ parentId?: string | null;
55
+ createdAt: string;
56
+ createdBy: string;
57
+ modifiedAt: string;
58
+ modifiedBy: string;
59
+ }
60
+
61
+ interface BoardObjectWithCoordinates {
62
+ x: number;
63
+ y: number;
64
+ }
65
+
66
+ interface BoardObjectWithDimensions {
67
+ width: number;
68
+ height: number;
69
+ }
70
+
71
+ export interface PreviewBoardObject
72
+ extends BoardItemBase,
73
+ BoardObjectBase,
74
+ BoardObjectWithDimensions {
75
+ type: "preview";
76
+ url: string;
77
+ }
78
+
79
+ export interface FrameBoardObject
80
+ extends Omit<BoardItemBase, "connectorIds">,
81
+ BoardObjectBase,
82
+ BoardObjectWithDimensions,
83
+ BoardObjectWithCoordinates {
84
+ type: "frame";
85
+ title: string;
86
+ childrenIds: string[];
87
+ showContent: boolean;
88
+ style: {
89
+ fillColor: string;
90
+ };
91
+ }
92
+
93
+ export interface GroupBoardItem extends BoardItemBase {
94
+ type: "group";
95
+ itemsIds: string[];
96
+ }
97
+
98
+ export interface StickyNoteBoardObject
99
+ extends BoardItemBase,
100
+ BoardObjectBase,
101
+ BoardObjectWithCoordinates,
102
+ BoardObjectWithDimensions {
103
+ type: "sticky_note";
104
+ tagIds: string[];
105
+ content: string;
106
+ shape: string;
107
+ style: {
108
+ fillColor: string;
109
+ textAlign: TextAlignment;
110
+ textAlignVertical: TextVerticalAlignment;
111
+ };
112
+ }
113
+
114
+ export interface TextBoardObject
115
+ extends BoardItemBase,
116
+ BoardObjectBase,
117
+ BoardObjectWithCoordinates,
118
+ BoardObjectWithDimensions {
119
+ type: "text";
120
+ content: string;
121
+ rotation: number;
122
+ style: {
123
+ fillColor: string;
124
+ fillOpacity: number;
125
+ fontFamily: string;
126
+ fontSize: number;
127
+ textAlign: TextAlignment;
128
+ color: string;
129
+ };
130
+ }
131
+
132
+ export interface ImageBoardObject
133
+ extends BoardItemBase,
134
+ BoardObjectBase,
135
+ BoardObjectWithDimensions,
136
+ BoardObjectWithCoordinates {
137
+ type: "image";
138
+ rotation: number;
139
+ title: string;
140
+ url: string;
141
+ alt?: string;
142
+ }
143
+
144
+ interface TableStyle {
145
+ borderColor?: string;
146
+ backgroundColor?: string;
147
+ backgroundOpacity?: number;
148
+ textColor?: string;
149
+ textAlign: TextAlignment;
150
+ textAlignVertical: TextVerticalAlignment;
151
+ textBold?: boolean;
152
+ textItalic?: boolean;
153
+ textUnderline?: boolean;
154
+ textStrike?: boolean;
155
+ textHighlight: null | string;
156
+ fontFamily?: string;
157
+ fontSize?: number;
158
+ writingMode?: "sideways" | "horizontal";
159
+ }
160
+
161
+ export interface TableBoardObject
162
+ extends BoardItemBase,
163
+ BoardObjectWithDimensions,
164
+ BoardObjectWithCoordinates {
165
+ type: "image";
166
+ rotation: number;
167
+ cols?: number | { width: number }[];
168
+ rows?: number | { height: number }[];
169
+ cells?: {
170
+ text: string;
171
+ rowspan?: number;
172
+ colspan?: number;
173
+ style: TableStyle;
174
+ }[][];
175
+ style: TableStyle;
176
+ }
177
+
178
+ export interface StructDocBoardObject
179
+ extends BoardItemBase,
180
+ BoardObjectBase,
181
+ BoardObjectWithDimensions,
182
+ BoardObjectWithCoordinates {
183
+ type: "struct_doc";
184
+ title: string;
185
+ content: string;
186
+ }
187
+
188
+ export interface EmbedBoardObject
189
+ extends BoardItemBase,
190
+ BoardObjectBase,
191
+ BoardObjectWithCoordinates {
192
+ type: "struct_doc";
193
+ url: string;
194
+ previewUrl: string;
195
+ mode: "inline" | "modal";
196
+ width?: number;
197
+ height?: number;
198
+ }
199
+
200
+ export interface ShapeBoardObject
201
+ extends BoardItemBase,
202
+ BoardObjectBase,
203
+ BoardObjectWithCoordinates,
204
+ BoardObjectWithDimensions {
205
+ type: "shape";
206
+ content: string;
207
+ shape: string;
208
+ rotation: number;
209
+ style: {
210
+ fillColor: string;
211
+ fontFamily: string;
212
+ fontSize: number;
213
+ textAlign: TextAlignment;
214
+ textAlignVertical: TextVerticalAlignment;
215
+ borderStyle: "dashed" | "normal" | "dotted";
216
+ borderOpacity: number;
217
+ borderColor: string;
218
+ borderWidth: number;
219
+ fillOpacity: number;
220
+ color: string;
221
+ };
222
+ }
223
+
224
+ interface CardBase extends BoardObjectBase, BoardObjectWithDimensions {
225
+ rotation: number;
226
+ title: string;
227
+ description: string;
228
+ fields: {
229
+ fillColor?: string;
230
+ textColor?: string;
231
+ iconUrl?: string;
232
+ iconShape?: "round" | "square";
233
+ tooltip?: string;
234
+ value: string;
235
+ }[];
236
+ style: {
237
+ cardTheme: string;
238
+ fillBackground: boolean;
239
+ };
240
+
241
+ /** coordinates may be null if card is part of a Kanban board */
242
+ x: number | null;
243
+ y: number | null;
244
+ }
245
+
246
+ export interface CardBoardObject extends BoardItemBase, CardBase {
247
+ type: "card";
248
+ assignee?: { userId: string };
249
+ dueDate?: string;
250
+ startDate?: string;
251
+ taskStatus: "to-do" | "in-progress" | "done" | "none";
252
+ tagIds: string[];
253
+ }
254
+
255
+ export interface AppCardBoardObject extends BoardItemBase, CardBase {
256
+ type: "app_card";
257
+ owned: boolean;
258
+ status: "disabled" | "disconnected" | "connected";
259
+ }
260
+
261
+ export interface TagBoardItem extends BoardItemBase {
262
+ type: "tag";
263
+ title: string;
264
+ color: string;
265
+ }
266
+
267
+ export interface KanbanBoardObject
268
+ extends BoardItemBase,
269
+ BoardObjectBase,
270
+ BoardObjectWithCoordinates,
271
+ BoardObjectWithDimensions {
272
+ type: "kanban";
273
+ }
274
+
275
+ export interface UnsupportedBoardObject
276
+ extends BoardItemBase,
277
+ BoardObjectBase,
278
+ BoardObjectWithCoordinates,
279
+ BoardObjectWithDimensions {
280
+ type: "unsupported";
281
+ }
282
+
283
+ export type BoardObject =
284
+ | PreviewBoardObject
285
+ | FrameBoardObject
286
+ | GroupBoardItem
287
+ | StickyNoteBoardObject
288
+ | TextBoardObject
289
+ | ImageBoardObject
290
+ | TableBoardObject
291
+ | StructDocBoardObject
292
+ | EmbedBoardObject
293
+ | ShapeBoardObject
294
+ | CardBoardObject
295
+ | AppCardBoardObject
296
+ | TagBoardItem
297
+ | KanbanBoardObject
298
+ | UnsupportedBoardObject;
@@ -0,0 +1,68 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { MiroBoard } from "../src";
4
+
5
+ const boardId = process.env.TEST_BOARD_ID;
6
+
7
+ if (!boardId) {
8
+ console.error("TEST_BOARD_ID environment variable is required.");
9
+ process.exit(1);
10
+ }
11
+
12
+ describe("Miro integration", async () => {
13
+ const miroBoard = new MiroBoard({ boardId });
14
+
15
+ after(async () => {
16
+ await miroBoard.dispose();
17
+ });
18
+
19
+ await it("should get all objects", async () => {
20
+ const objects = await miroBoard.getBoardObjects({});
21
+
22
+ assert.ok(
23
+ objects.find(
24
+ (obj) => obj.type === "sticky_note" && obj.content === "<p>Test 1</p>"
25
+ )
26
+ );
27
+ });
28
+
29
+ await it("should filter objects by type = 'frame'", async () => {
30
+ const frames = await miroBoard.getBoardObjects({ type: "frame" });
31
+
32
+ assert.ok(frames.length > 0);
33
+ assert.ok(frames.every((frame) => frame.type === "frame"));
34
+ });
35
+
36
+ await it("should filter objects by title = 'Frame 1'", async () => {
37
+ const objects = await miroBoard.getBoardObjects({}, { title: "Frame 1" });
38
+
39
+ assert.ok(
40
+ objects.find((obj) => obj.type === "frame" && obj.title === "Frame 1")
41
+ );
42
+ });
43
+
44
+ await it("should get SVG for entire board", async () => {
45
+ const svg = await miroBoard.getSvg();
46
+
47
+ assert.ok(svg.includes("<svg"));
48
+ assert.ok(svg.includes("Card text"));
49
+ assert.ok(svg.includes("STAR"));
50
+ assert.ok(svg.length > 50_000);
51
+ });
52
+
53
+ await it("should get SVG for specific frame", async () => {
54
+ const frames = await miroBoard.getBoardObjects(
55
+ { type: "frame" },
56
+ { title: "Frame 2" }
57
+ );
58
+ const frameId = frames[0].id;
59
+ assert.ok(frameId);
60
+
61
+ const svg = await miroBoard.getSvg([frameId]);
62
+
63
+ assert.ok(svg.includes("<svg"));
64
+ assert.ok(!svg.includes("Card text"));
65
+ assert.ok(svg.includes("STAR"));
66
+ assert.ok(svg.length > 10_000);
67
+ });
68
+ });
@@ -0,0 +1,54 @@
1
+ import { resolve } from "path";
2
+ import { MiroBoard } from "../src";
3
+ import * as tsj from "ts-json-schema-generator";
4
+ import Ajv from "ajv";
5
+
6
+ (async () => {
7
+ const config = {
8
+ path: resolve(import.meta.dirname, "../src/miro-types.ts"),
9
+ tsconfig: resolve(import.meta.dirname, "../tsconfig.json"),
10
+ type: "BoardObject"
11
+ };
12
+ const schema = tsj.createGenerator(config).createSchema(config.type);
13
+
14
+ const boardId = process.env.TEST_BOARD_ID;
15
+
16
+ if (!boardId) {
17
+ console.error("TEST_BOARD_ID environment variable is required.");
18
+ process.exit(1);
19
+ }
20
+
21
+ const miroBoard = new MiroBoard({ boardId });
22
+ const objects = await miroBoard.getBoardObjects({});
23
+ await miroBoard.dispose();
24
+
25
+ for (const object of objects) {
26
+ const ajv = new Ajv();
27
+ if (!ajv.validate(schema, object)) {
28
+ console.log(
29
+ "Failed to validate object: ",
30
+ JSON.stringify(
31
+ object,
32
+ process.env.CI
33
+ ? (key, value) => {
34
+ if (key === "type") {
35
+ return value;
36
+ }
37
+
38
+ if (typeof value === "string") {
39
+ return "string";
40
+ } else if (typeof value === "number") {
41
+ return 0;
42
+ } else {
43
+ return value;
44
+ }
45
+ }
46
+ : undefined,
47
+ 2
48
+ )
49
+ );
50
+ console.error(ajv.errors);
51
+ process.exit(1);
52
+ }
53
+ }
54
+ })();
package/tsconfig.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ESNext",
4
- "lib": ["DOM"],
5
- "module": "commonjs",
6
- "declaration": false,
3
+ "target": "ES2023",
4
+ "lib": ["DOM", "ESNext"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "nodenext",
7
+ "declaration": true,
7
8
  "outDir": "./build",
8
9
  "esModuleInterop": true,
9
10
  "forceConsistentCasingInFileNames": true,
10
11
  "strict": true,
11
- "skipLibCheck": true
12
- }
12
+ "skipLibCheck": true,
13
+ "verbatimModuleSyntax": true
14
+ },
15
+ "include": ["src", "src/miro-runtime.d.ts"]
13
16
  }
package/index.ts DELETED
@@ -1,124 +0,0 @@
1
- import { writeFile } from "fs/promises";
2
- import puppeteer from "puppeteer";
3
- import { program } from "@commander-js/extra-typings";
4
-
5
- declare global {
6
- interface Window {
7
- miro: {
8
- board: {
9
- get(opts: {
10
- type: "frame"[];
11
- }): Promise<{ title: string; id: string }[]>;
12
- select(opts: { id: string }): Promise<void>;
13
- deselect(): Promise<void>;
14
- };
15
- };
16
- cmd: {
17
- board: {
18
- api: {
19
- export: {
20
- makeVector: () => Promise<string>;
21
- };
22
- };
23
- };
24
- };
25
- }
26
- }
27
-
28
- const { token, boardId, frameNames, outputFile } = program
29
- .requiredOption("-t, --token <token>", "Miro token")
30
- .requiredOption("-b, --board-id <boardId>", "The board ID")
31
- .option(
32
- "-f, --frame-names <frameNames...>",
33
- "The frame name(s), leave empty to export entire board"
34
- )
35
- .option(
36
- "-o, --output-file <filename>",
37
- "A file to output the SVG to (stdout if not supplied)"
38
- )
39
- .parse()
40
- .opts();
41
-
42
- (async () => {
43
- const browser = await puppeteer.launch({ headless: true });
44
-
45
- const page = await browser.newPage();
46
-
47
- await page.setCookie({
48
- name: "token",
49
- value: token,
50
- domain: "miro.com"
51
- });
52
-
53
- await page.setViewport({ width: 1080, height: 1024 });
54
-
55
- await page.goto(`https://miro.com/app/board/${boardId}/`, {
56
- waitUntil: "domcontentloaded"
57
- });
58
-
59
- await page.evaluate(
60
- () =>
61
- new Promise<void>((resolve) => {
62
- const interval = setInterval(() => {
63
- try {
64
- if (typeof window.miro?.board !== "undefined") {
65
- resolve();
66
- clearInterval(interval);
67
- }
68
- } catch (e) {
69
- // ignored
70
- }
71
- }, 100);
72
- })
73
- );
74
-
75
- const getSvgForFrames = (frameNames: string[] | undefined) =>
76
- page.evaluate(async (frameNames) => {
77
- if (frameNames) {
78
- const frames = await window.miro.board.get({ type: ["frame"] });
79
-
80
- const selectedFrames = frames.filter((frame) =>
81
- frameNames.includes(frame.title)
82
- );
83
-
84
- if (selectedFrames.length !== frameNames.length) {
85
- throw Error(
86
- `${
87
- frameNames.length - selectedFrames.length
88
- } frame(s) could not be found on the board.`
89
- );
90
- }
91
-
92
- await window.miro.board.deselect();
93
-
94
- for (const { id } of selectedFrames) {
95
- await window.miro.board.select({ id });
96
- }
97
- }
98
-
99
- return await window.cmd.board.api.export.makeVector();
100
- }, frameNames);
101
-
102
- if (outputFile?.includes("{frameName}")) {
103
- if (!frameNames) {
104
- throw Error(
105
- "Expected frame names to be given when the output file name format expects a frame name."
106
- );
107
- }
108
-
109
- for (const frameName of frameNames) {
110
- const svg = await getSvgForFrames([frameName]);
111
- await writeFile(outputFile.replace("{frameName}", frameName), svg);
112
- }
113
- } else {
114
- const svg = await getSvgForFrames(frameNames);
115
- if (outputFile) {
116
- await writeFile(outputFile, svg);
117
- } else {
118
- process.stdout.write(svg);
119
- }
120
- }
121
-
122
- await page.close();
123
- await browser.close();
124
- })();