miro-export 1.0.1 → 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,192 +0,0 @@
1
- import { writeFile } from "fs/promises";
2
- import puppeteer from "puppeteer";
3
- import { program } from "@commander-js/extra-typings";
4
-
5
- type BoardObjectType = "frame" | "group" | "sticky_note" | "text";
6
- interface BoardObjectBase {
7
- title: string;
8
- id: string;
9
- type: BoardObjectType;
10
- }
11
- interface FrameBoardObject extends BoardObjectBase {
12
- type: "frame";
13
- title: string;
14
- childrenIds: string[];
15
- }
16
- interface GroupBoardObject extends BoardObjectBase {
17
- type: "group";
18
- itemsIds: string[];
19
- }
20
- interface StickyNoteBoardObject extends BoardObjectBase {
21
- type: "sticky_note";
22
- }
23
- interface TextBoardObject extends BoardObjectBase {
24
- type: "text";
25
- }
26
- type BoardObject =
27
- | FrameBoardObject
28
- | GroupBoardObject
29
- | StickyNoteBoardObject
30
- | TextBoardObject;
31
-
32
- declare global {
33
- interface Window {
34
- miro: {
35
- board: {
36
- get(opts: {
37
- type?: BoardObjectType[];
38
- id?: string[];
39
- }): Promise<BoardObject[]>;
40
- select(opts: { id: string }): Promise<void>;
41
- deselect(): Promise<void>;
42
- };
43
- };
44
- cmd: {
45
- board: {
46
- api: {
47
- export: {
48
- makeVector: () => Promise<string>;
49
- };
50
- };
51
- };
52
- };
53
- }
54
- }
55
-
56
- const { token, boardId, frameNames, outputFile, exportFormat } = program
57
- .requiredOption("-t, --token <token>", "Miro token")
58
- .requiredOption("-b, --board-id <boardId>", "The board ID")
59
- .option(
60
- "-f, --frame-names <frameNames...>",
61
- "The frame name(s), leave empty to export entire board"
62
- )
63
- .option(
64
- "-o, --output-file <filename>",
65
- "A file to output the SVG to (stdout if not supplied)"
66
- )
67
- .option("-e, --export-format <format>", "'svg' or 'json' (default: 'svg')")
68
- .parse()
69
- .opts();
70
-
71
- (async () => {
72
- const browser = await puppeteer.launch({ headless: true });
73
-
74
- const page = await browser.newPage();
75
-
76
- await page.setCookie({
77
- name: "token",
78
- value: token,
79
- domain: "miro.com"
80
- });
81
-
82
- await page.setViewport({ width: 1080, height: 1024 });
83
-
84
- await page.goto(`https://miro.com/app/board/${boardId}/`, {
85
- waitUntil: "domcontentloaded"
86
- });
87
-
88
- await page.evaluate(
89
- () =>
90
- new Promise<void>((resolve) => {
91
- const interval = setInterval(() => {
92
- try {
93
- if (typeof window.miro?.board !== "undefined") {
94
- resolve();
95
- clearInterval(interval);
96
- }
97
- } catch (e) {
98
- // ignored
99
- }
100
- }, 100);
101
- })
102
- );
103
-
104
- const getSvgForFrames = (frameNames: string[] | undefined) =>
105
- page.evaluate(async (frameNames) => {
106
- if (frameNames) {
107
- const frames = await window.miro.board.get({ type: ["frame"] });
108
-
109
- const selectedFrames = frames.filter((frame) =>
110
- frameNames.includes(frame.title)
111
- );
112
-
113
- if (selectedFrames.length !== frameNames.length) {
114
- throw Error(
115
- `${
116
- frameNames.length - selectedFrames.length
117
- } frame(s) could not be found on the board.`
118
- );
119
- }
120
-
121
- await window.miro.board.deselect();
122
-
123
- for (const { id } of selectedFrames) {
124
- await window.miro.board.select({ id });
125
- }
126
- }
127
-
128
- return await window.cmd.board.api.export.makeVector();
129
- }, frameNames);
130
-
131
- const getJsonForFrames = (frameNames: string[] | undefined) =>
132
- page.evaluate(async (frameNames) => {
133
- if (frameNames) {
134
- const frames = await window.miro.board.get({ type: ["frame"] });
135
-
136
- const selectedFrames = frames.filter((frame) =>
137
- frameNames.includes(frame.title)
138
- );
139
-
140
- if (selectedFrames.length !== frameNames.length) {
141
- throw Error(
142
- `${
143
- frameNames.length - selectedFrames.length
144
- } frame(s) could not be found on the board.`
145
- );
146
- }
147
-
148
- const children = await window.miro.board.get({
149
- id: selectedFrames.flatMap(
150
- (frame) => (frame as FrameBoardObject).childrenIds
151
- )
152
- });
153
-
154
- const groupChildren = await window.miro.board.get({
155
- id: children
156
- .filter(
157
- (child): child is GroupBoardObject => child.type === "group"
158
- )
159
- .flatMap((child) => child.itemsIds)
160
- });
161
-
162
- return JSON.stringify([...frames, ...children, ...groupChildren]);
163
- }
164
-
165
- return JSON.stringify(await window.miro.board.get({}));
166
- }, frameNames);
167
-
168
- const getFn = exportFormat === "json" ? getJsonForFrames : getSvgForFrames;
169
-
170
- if (outputFile?.includes("{frameName}")) {
171
- if (!frameNames) {
172
- throw Error(
173
- "Expected frame names to be given when the output file name format expects a frame name."
174
- );
175
- }
176
-
177
- for (const frameName of frameNames) {
178
- const svg = await getFn([frameName]);
179
- await writeFile(outputFile.replace("{frameName}", frameName), svg);
180
- }
181
- } else {
182
- const svg = await getFn(frameNames);
183
- if (outputFile) {
184
- await writeFile(outputFile, svg);
185
- } else {
186
- process.stdout.write(svg);
187
- }
188
- }
189
-
190
- await page.close();
191
- await browser.close();
192
- })();