miro-export 1.1.0 → 1.2.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.
@@ -8,11 +8,11 @@ jobs:
8
8
  build:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
  - uses: pnpm/action-setup@v3
13
13
  with:
14
14
  version: 8
15
- - uses: actions/setup-node@v4
15
+ - uses: actions/setup-node@v6
16
16
  with:
17
17
  node-version: "22.x"
18
18
  - name: Install dependencies
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Build
23
23
  run: pnpm run build
24
24
  - name: Upload Artifact
25
- uses: actions/upload-artifact@v4
25
+ uses: actions/upload-artifact@v6
26
26
  with:
27
27
  name: build
28
28
  path: build/
@@ -8,11 +8,11 @@ jobs:
8
8
  publish:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
  - uses: pnpm/action-setup@v3
13
13
  with:
14
14
  version: 8
15
- - uses: actions/setup-node@v4
15
+ - uses: actions/setup-node@v6
16
16
  with:
17
17
  node-version: "22.x"
18
18
  registry-url: "https://registry.npmjs.org"
@@ -6,13 +6,13 @@ on:
6
6
 
7
7
  jobs:
8
8
  test:
9
- runs-on: ubuntu-latest
9
+ runs-on: ubuntu-22.04
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
  - uses: pnpm/action-setup@v3
13
13
  with:
14
14
  version: 8
15
- - uses: actions/setup-node@v4
15
+ - uses: actions/setup-node@v6
16
16
  with:
17
17
  node-version: "22.x"
18
18
  - name: Install dependencies
@@ -21,3 +21,4 @@ jobs:
21
21
  run: pnpm run test
22
22
  env:
23
23
  TEST_BOARD_ID: ${{secrets.TEST_BOARD_ID}}
24
+ PRIVATE_TEST_BOARD_ID: ${{secrets.PRIVATE_TEST_BOARD_ID}}
package/README.md CHANGED
@@ -12,6 +12,17 @@ If accessing a private board, a personal token is required. To get a token, log
12
12
 
13
13
  ## CLI
14
14
 
15
+ You can use this tool as a command-line tool.
16
+
17
+ ### Prerequisites
18
+
19
+ - [Node.js >=22](https://nodejs.org/en/download)
20
+ - npm (built-in to Node.js), yarn, or pnpm
21
+
22
+ ### Installation
23
+
24
+ The CLI can be ran using [npx](https://docs.npmjs.com/cli/v8/commands/npx) with `npx miro-export [options]` (see options below). Alternatively, it's possible to install the package to the global scope with, for example, `npm i -g miro-export`.
25
+
15
26
  ### Usage
16
27
 
17
28
  ```
@@ -19,8 +30,9 @@ Options:
19
30
  -t, --token <token> Miro token
20
31
  -b, --board-id <boardId> The board ID
21
32
  -f, --frame-names <frameNames...> The frame name(s), leave empty to export entire board
22
- -o, --output-file <filename> A file to output the SVG to (stdout if not supplied)
23
- -e, --export-format <format> 'svg' or 'json' (default: 'svg')
33
+ -o, --output-file <filename> A file to output the SVG to (stdout if not specified)
34
+ -e, --export-format <format> 'svg' or 'json' (default: "svg")
35
+ -l, --load-timeout <milliseconds> Timeout for loading the board in milliseconds (default: 15000)
24
36
  -h, --help display help for command
25
37
  ```
26
38
 
@@ -30,6 +42,9 @@ Options:
30
42
  # export "Frame 2" to the file "My Frame 2.svg"
31
43
  miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" -o "My Frame 2.svg"
32
44
 
45
+ # using npx
46
+ npx miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" -o "My Frame 2.svg"
47
+
33
48
  # export entire board to stdout
34
49
  miro-export -t XYZ -b uMoVLkx8gIc=
35
50
 
@@ -86,13 +101,13 @@ await using miroBoard = new MiroBoard({
86
101
  });
87
102
 
88
103
  // get all board objects of type frame and with title "Frame 1"
89
- const framesWithTitleFrame1 = miroBoard.getBoardObjects(
104
+ const framesWithTitleFrame1 = await miroBoard.getBoardObjects(
90
105
  { type: "frame" }, // required (but empty object is OK too), limited field support
91
106
  { title: "Frame 1" } // optional additional filters
92
107
  );
93
108
 
94
109
  // get SVG of the first frame found above
95
- const svgOfFrame1 = miroBoard.getSvg([framesWithTitleFrame1[0].id]);
110
+ const svgOfFrame1 = await miroBoard.getSvg([framesWithTitleFrame1[0].id]);
96
111
 
97
112
  // if you can't use "await using" for disposal, you can also dispose manually:
98
113
  // await miroBoard.dispose()
package/build/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
3
  if (value !== null && value !== void 0) {
3
4
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
@@ -53,69 +54,85 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
54
  import { writeFile } from "fs/promises";
54
55
  import { program } from "@commander-js/extra-typings";
55
56
  import { MiroBoard } from "./index.js";
56
- const { token, boardId, frameNames, outputFile, exportFormat } = program
57
+ const { token, boardId, frameNames, outputFile, exportFormat, loadTimeout } = program
57
58
  .option("-t, --token <token>", "Miro token")
58
59
  .requiredOption("-b, --board-id <boardId>", "The board ID")
59
60
  .option("-f, --frame-names <frameNames...>", "The frame name(s), leave empty to export entire board")
60
- .option("-o, --output-file <filename>", "A file to output the SVG to (stdout if not supplied)")
61
- .option("-e, --export-format <format>", "'svg' or 'json' (default: 'svg')")
61
+ .option("-o, --output-file <filename>", "A file to output the SVG to (stdout if not specified)")
62
+ .option("-e, --export-format <format>", "'svg' or 'json'", "svg")
63
+ .option("-l, --load-timeout <milliseconds>", "Timeout for loading the board in milliseconds", (arg) => parseInt(arg, 10), 15000)
62
64
  .parse()
63
65
  .opts();
64
66
  (async () => {
65
- const env_1 = { stack: [], error: void 0, hasError: false };
66
67
  try {
67
- const miroBoard = __addDisposableResource(env_1, new MiroBoard({ token, boardId }), true);
68
- async function getFrames(frameNames) {
69
- const frames = await miroBoard.getBoardObjects({ type: "frame" }, { title: frameNames });
70
- if (frames && frames.length !== frameNames.length) {
71
- throw Error(`${frameNames.length - frames.length} frame(s) could not be found on the board.`);
68
+ const env_1 = { stack: [], error: void 0, hasError: false };
69
+ try {
70
+ const miroBoard = __addDisposableResource(env_1, new MiroBoard({
71
+ token,
72
+ boardId,
73
+ boardLoadTimeoutMs: loadTimeout
74
+ }), true);
75
+ async function getFrames(frameNames) {
76
+ const frames = await miroBoard.getBoardObjects({ type: "frame" }, { title: frameNames });
77
+ if (frames && frames.length !== frameNames.length) {
78
+ throw Error(`${frameNames.length - frames.length} frame(s) could not be found on the board.`);
79
+ }
80
+ return frames;
72
81
  }
73
- return frames;
74
- }
75
- async function getSvg(frames) {
76
- return await miroBoard.getSvg(frames?.map(({ id }) => id).filter((id) => !!id));
77
- }
78
- async function getJson(frames) {
79
- if (frames) {
80
- const frameChildren = await miroBoard.getBoardObjects({
81
- id: frames.flatMap((frame) => frame.childrenIds)
82
- });
83
- const groupChildren = await miroBoard.getBoardObjects({
84
- id: frameChildren
85
- .filter((child) => child.type === "group")
86
- .flatMap((child) => child.itemsIds)
87
- });
88
- return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
82
+ async function getSvg(frames) {
83
+ return await miroBoard.getSvg(frames?.map(({ id }) => id).filter((id) => !!id));
89
84
  }
90
- return JSON.stringify(await miroBoard.getBoardObjects({}));
91
- }
92
- const getFn = exportFormat === "json" ? getJson : getSvg;
93
- if (outputFile?.includes("{frameName}")) {
94
- if (!frameNames) {
95
- throw Error("Expected frame names to be given when the output file name format expects a frame name.");
96
- }
97
- for (const frameName of frameNames) {
98
- const output = await getFn(await getFrames([frameName]));
99
- await writeFile(outputFile.replace("{frameName}", frameName), output);
85
+ async function getJson(frames) {
86
+ if (frames) {
87
+ const frameChildren = await miroBoard.getBoardObjects({
88
+ id: frames.flatMap((frame) => frame.childrenIds)
89
+ });
90
+ const groupChildren = await miroBoard.getBoardObjects({
91
+ id: frameChildren
92
+ .filter((child) => child.type === "group")
93
+ .flatMap((child) => child.itemsIds)
94
+ });
95
+ return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
96
+ }
97
+ return JSON.stringify(await miroBoard.getBoardObjects({}));
100
98
  }
101
- }
102
- else {
103
- const svg = await getFn(frameNames && (await getFrames(frameNames)));
104
- if (outputFile) {
105
- await writeFile(outputFile, svg);
99
+ const getFn = exportFormat === "json" ? getJson : getSvg;
100
+ if (outputFile?.includes("{frameName}")) {
101
+ if (!frameNames) {
102
+ throw Error("Expected frame names to be given when the output file name format expects a frame name.");
103
+ }
104
+ for (const frameName of frameNames) {
105
+ const output = await getFn(await getFrames([frameName]));
106
+ await writeFile(outputFile.replace("{frameName}", frameName), output);
107
+ }
106
108
  }
107
109
  else {
108
- process.stdout.write(svg);
110
+ const svg = await getFn(frameNames && (await getFrames(frameNames)));
111
+ if (outputFile) {
112
+ await writeFile(outputFile, svg);
113
+ }
114
+ else {
115
+ process.stdout.write(svg);
116
+ }
109
117
  }
110
118
  }
119
+ catch (e_1) {
120
+ env_1.error = e_1;
121
+ env_1.hasError = true;
122
+ }
123
+ finally {
124
+ const result_1 = __disposeResources(env_1);
125
+ if (result_1)
126
+ await result_1;
127
+ }
111
128
  }
112
- catch (e_1) {
113
- env_1.error = e_1;
114
- env_1.hasError = true;
115
- }
116
- finally {
117
- const result_1 = __disposeResources(env_1);
118
- if (result_1)
119
- await result_1;
129
+ catch (err) {
130
+ const RED = "\x1b[31m";
131
+ const RESET = "\x1b[0m";
132
+ const GRAY = "\x1b[38;5;248m";
133
+ console.error(`❌ ${RED}Error:${RESET} ${err instanceof Error ? err.message : err}`);
134
+ console.error(GRAY);
135
+ console.error(err);
136
+ console.error(RESET);
120
137
  }
121
138
  })();
package/build/index.d.ts CHANGED
@@ -1,8 +1,27 @@
1
+ import { type LaunchOptions } from "puppeteer";
1
2
  import type { BoardObject } from "./miro-types.ts";
2
3
  import type { GetBoardsFilter } from "./miro-runtime.ts";
3
4
  interface InitialMiroBoardOptions {
5
+ /**
6
+ * The Miro authentication token with access to load the board.
7
+ * Optional if anonymous users may access the board without
8
+ * logging in.
9
+ */
4
10
  token?: string;
11
+ /**
12
+ * The Miro board ID.
13
+ */
5
14
  boardId: string;
15
+ /**
16
+ * Optional custom Puppeteer launch options.
17
+ */
18
+ puppeteerOptions?: LaunchOptions;
19
+ /**
20
+ * Timeout until it is determined that the Miro board could
21
+ * not be loaded for some reason, in milliseconds. Default
22
+ * is 15 seconds (15,000 milliseconds).
23
+ */
24
+ boardLoadTimeoutMs?: number;
6
25
  }
7
26
  type AdditionalFilter<T> = Partial<T> | Partial<{
8
27
  [K in keyof T]: T[K][];
@@ -12,6 +31,7 @@ type FilteredResultsByType<F extends string | string[] | undefined, T> = F exten
12
31
  }>[] : T[];
13
32
  export declare class MiroBoard {
14
33
  private context;
34
+ private isDisposed;
15
35
  constructor(options: InitialMiroBoardOptions);
16
36
  private initialize;
17
37
  dispose(): Promise<void>;
package/build/index.js CHANGED
@@ -1,12 +1,16 @@
1
- #!/usr/bin/env node
2
1
  import puppeteer, { Browser, Page } from "puppeteer";
2
+ const DEFAULT_BOARD_LOAD_TIMEOUT_MS = 15_000;
3
3
  export class MiroBoard {
4
4
  context = Promise.withResolvers();
5
+ isDisposed = false;
5
6
  constructor(options) {
6
7
  this.initialize(options);
7
8
  }
8
9
  async initialize(options) {
9
- const browser = await puppeteer.launch({ headless: true });
10
+ const browser = await puppeteer.launch({
11
+ headless: true,
12
+ ...(options.puppeteerOptions ?? {})
13
+ });
10
14
  const page = await browser.newPage();
11
15
  if (options.token) {
12
16
  await browser.browserContexts()[0].setCookie({
@@ -24,24 +28,46 @@ export class MiroBoard {
24
28
  await page.goto(`https://miro.com/app/board/${options.boardId}/`, {
25
29
  waitUntil: "domcontentloaded"
26
30
  });
27
- await page.evaluate(() => new Promise((resolve) => {
28
- if (window.miro) {
29
- resolve();
30
- }
31
- let miroValue;
32
- Object.defineProperty(window, "miro", {
33
- get() {
34
- return miroValue;
35
- },
36
- set(value) {
37
- miroValue = value;
31
+ try {
32
+ await page.evaluate((timeoutDuration) => new Promise((resolve, reject) => {
33
+ if (window.miro) {
38
34
  resolve();
39
35
  }
40
- });
41
- }));
36
+ const timeout = setTimeout(() => {
37
+ reject(new Error(`Miro board could not be loaded: application instance not available after ${timeoutDuration} ms. Check your network connection, access token and board access.`));
38
+ }, timeoutDuration);
39
+ const authModalChecker = setInterval(() => {
40
+ if (document.querySelector('[data-testid="signup-popup-container"]')) {
41
+ reject(new Error(`Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`));
42
+ }
43
+ }, 250);
44
+ let miroValue;
45
+ Object.defineProperty(window, "miro", {
46
+ get() {
47
+ return miroValue;
48
+ },
49
+ set(value) {
50
+ clearTimeout(timeout);
51
+ clearInterval(authModalChecker);
52
+ miroValue = value;
53
+ resolve();
54
+ }
55
+ });
56
+ }), options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS);
57
+ }
58
+ catch (err) {
59
+ await browser.close();
60
+ this.context.reject(err);
61
+ this.isDisposed = true;
62
+ return;
63
+ }
42
64
  this.context.resolve({ browser, page });
43
65
  }
44
66
  async dispose() {
67
+ if (this.isDisposed) {
68
+ return;
69
+ }
70
+ this.isDisposed = true;
45
71
  await (await this.browser).close();
46
72
  }
47
73
  async [Symbol.asyncDispose]() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miro-export",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "author": "jolle <npm-contact@jolle.io>",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,32 +23,32 @@
23
23
  "miro-export": "./build/cli.js"
24
24
  },
25
25
  "devDependencies": {
26
- "@eslint/eslintrc": "^3.2.0",
27
- "@eslint/js": "^9.17.0",
28
- "@stylistic/eslint-plugin-ts": "^2.12.1",
29
- "@types/node": "^22.10.2",
26
+ "@eslint/eslintrc": "^3.3.3",
27
+ "@eslint/js": "^9.39.2",
28
+ "@stylistic/eslint-plugin-ts": "^4.4.1",
29
+ "@types/node": "^25.0.3",
30
30
  "ajv": "^8.17.1",
31
- "eslint": "^9.17.0",
32
- "eslint-config-prettier": "^9.1.0",
33
- "eslint-plugin-prettier": "^5.2.1",
31
+ "eslint": "^9.39.2",
32
+ "eslint-config-prettier": "^10.1.8",
33
+ "eslint-plugin-prettier": "^5.5.4",
34
34
  "eslint-plugin-promise": "^7.2.1",
35
- "prettier": "^3.4.2",
36
- "ts-json-schema-generator": "^2.3.0",
37
- "tsx": "^4.19.2",
38
- "typescript": "^5.7.2",
39
- "typescript-eslint": "^8.18.1"
35
+ "prettier": "^3.7.4",
36
+ "ts-json-schema-generator": "^2.4.0",
37
+ "tsx": "^4.21.0",
38
+ "typescript": "^5.9.3",
39
+ "typescript-eslint": "^8.50.1"
40
40
  },
41
41
  "dependencies": {
42
- "@commander-js/extra-typings": "^12.1.0",
43
- "commander": "^12.1.0",
44
- "puppeteer": "^23.11.1"
42
+ "@commander-js/extra-typings": "^14.0.0",
43
+ "commander": "^14.0.2",
44
+ "puppeteer": "^24.34.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22.0.0"
48
48
  },
49
49
  "scripts": {
50
50
  "lint": "eslint src/*.ts && prettier -c src/*.ts",
51
- "build": "tsc && echo \"#!/usr/bin/env node\n$(cat ./build/index.js)\" > ./build/index.js",
51
+ "build": "tsc && echo \"#!/usr/bin/env node\n$(cat ./build/cli.js)\" > ./build/cli.js",
52
52
  "test": "npm run test:board-object-types && npm run test:api",
53
53
  "test:board-object-types": "tsx tests/board-object-types.ts",
54
54
  "test:api": "tsx tests/api.test.ts"
package/src/cli.ts CHANGED
@@ -3,84 +3,107 @@ import { program } from "@commander-js/extra-typings";
3
3
  import { MiroBoard } from "./index.js";
4
4
  import type { FrameBoardObject } from "./miro-types.ts";
5
5
 
6
- const { token, boardId, frameNames, outputFile, exportFormat } = program
7
- .option("-t, --token <token>", "Miro token")
8
- .requiredOption("-b, --board-id <boardId>", "The board ID")
9
- .option(
10
- "-f, --frame-names <frameNames...>",
11
- "The frame name(s), leave empty to export entire board"
12
- )
13
- .option(
14
- "-o, --output-file <filename>",
15
- "A file to output the SVG to (stdout if not supplied)"
16
- )
17
- .option("-e, --export-format <format>", "'svg' or 'json' (default: 'svg')")
18
- .parse()
19
- .opts();
6
+ const { token, boardId, frameNames, outputFile, exportFormat, loadTimeout } =
7
+ program
8
+ .option("-t, --token <token>", "Miro token")
9
+ .requiredOption("-b, --board-id <boardId>", "The board ID")
10
+ .option(
11
+ "-f, --frame-names <frameNames...>",
12
+ "The frame name(s), leave empty to export entire board"
13
+ )
14
+ .option(
15
+ "-o, --output-file <filename>",
16
+ "A file to output the SVG to (stdout if not specified)"
17
+ )
18
+ .option("-e, --export-format <format>", "'svg' or 'json'", "svg")
19
+ .option(
20
+ "-l, --load-timeout <milliseconds>",
21
+ "Timeout for loading the board in milliseconds",
22
+ (arg) => parseInt(arg, 10),
23
+ 15000
24
+ )
25
+ .parse()
26
+ .opts();
20
27
 
21
28
  (async () => {
22
- await using miroBoard = new MiroBoard({ token, boardId });
29
+ try {
30
+ await using miroBoard = new MiroBoard({
31
+ token,
32
+ boardId,
33
+ boardLoadTimeoutMs: loadTimeout
34
+ });
23
35
 
24
- async function getFrames(frameNames: string[]) {
25
- const frames = await miroBoard.getBoardObjects(
26
- { type: "frame" as const },
27
- { title: frameNames }
28
- );
36
+ async function getFrames(frameNames: string[]) {
37
+ const frames = await miroBoard.getBoardObjects(
38
+ { type: "frame" as const },
39
+ { title: frameNames }
40
+ );
41
+
42
+ if (frames && frames.length !== frameNames.length) {
43
+ throw Error(
44
+ `${
45
+ frameNames.length - frames.length
46
+ } frame(s) could not be found on the board.`
47
+ );
48
+ }
49
+
50
+ return frames;
51
+ }
29
52
 
30
- if (frames && frames.length !== frameNames.length) {
31
- throw Error(
32
- `${
33
- frameNames.length - frames.length
34
- } frame(s) could not be found on the board.`
53
+ async function getSvg(frames?: FrameBoardObject[]) {
54
+ return await miroBoard.getSvg(
55
+ frames?.map(({ id }) => id).filter((id): id is string => !!id)
35
56
  );
36
57
  }
37
58
 
38
- return frames;
39
- }
59
+ async function getJson(frames?: FrameBoardObject[]) {
60
+ if (frames) {
61
+ const frameChildren = await miroBoard.getBoardObjects({
62
+ id: frames.flatMap((frame) => frame.childrenIds)
63
+ });
40
64
 
41
- async function getSvg(frames?: FrameBoardObject[]) {
42
- return await miroBoard.getSvg(
43
- frames?.map(({ id }) => id).filter((id): id is string => !!id)
44
- );
45
- }
65
+ const groupChildren = await miroBoard.getBoardObjects({
66
+ id: frameChildren
67
+ .filter((child) => child.type === "group")
68
+ .flatMap((child) => child.itemsIds)
69
+ });
46
70
 
47
- async function getJson(frames?: FrameBoardObject[]) {
48
- if (frames) {
49
- const frameChildren = await miroBoard.getBoardObjects({
50
- id: frames.flatMap((frame) => frame.childrenIds)
51
- });
71
+ return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
72
+ }
52
73
 
53
- const groupChildren = await miroBoard.getBoardObjects({
54
- id: frameChildren
55
- .filter((child) => child.type === "group")
56
- .flatMap((child) => child.itemsIds)
57
- });
58
-
59
- return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
74
+ return JSON.stringify(await miroBoard.getBoardObjects({}));
60
75
  }
61
76
 
62
- return JSON.stringify(await miroBoard.getBoardObjects({}));
63
- }
77
+ const getFn = exportFormat === "json" ? getJson : getSvg;
64
78
 
65
- const getFn = exportFormat === "json" ? getJson : getSvg;
79
+ if (outputFile?.includes("{frameName}")) {
80
+ if (!frameNames) {
81
+ throw Error(
82
+ "Expected frame names to be given when the output file name format expects a frame name."
83
+ );
84
+ }
66
85
 
67
- if (outputFile?.includes("{frameName}")) {
68
- if (!frameNames) {
69
- throw Error(
70
- "Expected frame names to be given when the output file name format expects a frame name."
71
- );
72
- }
73
-
74
- for (const frameName of frameNames) {
75
- const output = await getFn(await getFrames([frameName]));
76
- await writeFile(outputFile.replace("{frameName}", frameName), output);
77
- }
78
- } else {
79
- const svg = await getFn(frameNames && (await getFrames(frameNames)));
80
- if (outputFile) {
81
- await writeFile(outputFile, svg);
86
+ for (const frameName of frameNames) {
87
+ const output = await getFn(await getFrames([frameName]));
88
+ await writeFile(outputFile.replace("{frameName}", frameName), output);
89
+ }
82
90
  } else {
83
- process.stdout.write(svg);
91
+ const svg = await getFn(frameNames && (await getFrames(frameNames)));
92
+ if (outputFile) {
93
+ await writeFile(outputFile, svg);
94
+ } else {
95
+ process.stdout.write(svg);
96
+ }
84
97
  }
98
+ } catch (err) {
99
+ const RED = "\x1b[31m";
100
+ const RESET = "\x1b[0m";
101
+ const GRAY = "\x1b[38;5;248m";
102
+ console.error(
103
+ `❌ ${RED}Error:${RESET} ${err instanceof Error ? err.message : err}`
104
+ );
105
+ console.error(GRAY);
106
+ console.error(err);
107
+ console.error(RESET);
85
108
  }
86
109
  })();
package/src/index.ts CHANGED
@@ -1,12 +1,32 @@
1
- import puppeteer, { Browser, Page } from "puppeteer";
1
+ import puppeteer, { Browser, Page, type LaunchOptions } from "puppeteer";
2
2
  import type { BoardObject } from "./miro-types.ts";
3
3
  import type { GetBoardsFilter } from "./miro-runtime.ts";
4
4
 
5
5
  interface InitialMiroBoardOptions {
6
+ /**
7
+ * The Miro authentication token with access to load the board.
8
+ * Optional if anonymous users may access the board without
9
+ * logging in.
10
+ */
6
11
  token?: string;
12
+ /**
13
+ * The Miro board ID.
14
+ */
7
15
  boardId: string;
16
+ /**
17
+ * Optional custom Puppeteer launch options.
18
+ */
19
+ puppeteerOptions?: LaunchOptions;
20
+ /**
21
+ * Timeout until it is determined that the Miro board could
22
+ * not be loaded for some reason, in milliseconds. Default
23
+ * is 15 seconds (15,000 milliseconds).
24
+ */
25
+ boardLoadTimeoutMs?: number;
8
26
  }
9
27
 
28
+ const DEFAULT_BOARD_LOAD_TIMEOUT_MS = 15_000;
29
+
10
30
  type AdditionalFilter<T> = Partial<T> | Partial<{ [K in keyof T]: T[K][] }>;
11
31
 
12
32
  type FilteredResultsByType<
@@ -16,13 +36,17 @@ type FilteredResultsByType<
16
36
 
17
37
  export class MiroBoard {
18
38
  private context = Promise.withResolvers<{ browser: Browser; page: Page }>();
39
+ private isDisposed = false;
19
40
 
20
41
  constructor(options: InitialMiroBoardOptions) {
21
42
  this.initialize(options);
22
43
  }
23
44
 
24
45
  private async initialize(options: InitialMiroBoardOptions) {
25
- const browser = await puppeteer.launch({ headless: true });
46
+ const browser = await puppeteer.launch({
47
+ headless: true,
48
+ ...(options.puppeteerOptions ?? {})
49
+ });
26
50
  const page = await browser.newPage();
27
51
 
28
52
  if (options.token) {
@@ -44,30 +68,64 @@ export class MiroBoard {
44
68
  waitUntil: "domcontentloaded"
45
69
  });
46
70
 
47
- await page.evaluate(
48
- () =>
49
- new Promise<void>((resolve) => {
50
- if (window.miro) {
51
- resolve();
52
- }
53
-
54
- let miroValue: (typeof window)["miro"];
55
- Object.defineProperty(window, "miro", {
56
- get() {
57
- return miroValue;
58
- },
59
- set(value) {
60
- miroValue = value;
71
+ try {
72
+ await page.evaluate(
73
+ (timeoutDuration) =>
74
+ new Promise<void>((resolve, reject) => {
75
+ if (window.miro) {
61
76
  resolve();
62
77
  }
63
- });
64
- })
65
- );
78
+
79
+ const timeout = setTimeout(() => {
80
+ reject(
81
+ new Error(
82
+ `Miro board could not be loaded: application instance not available after ${timeoutDuration} ms. Check your network connection, access token and board access.`
83
+ )
84
+ );
85
+ }, timeoutDuration);
86
+
87
+ const authModalChecker = setInterval(() => {
88
+ if (
89
+ document.querySelector('[data-testid="signup-popup-container"]')
90
+ ) {
91
+ reject(
92
+ new Error(
93
+ `Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`
94
+ )
95
+ );
96
+ }
97
+ }, 250);
98
+
99
+ let miroValue: (typeof window)["miro"];
100
+ Object.defineProperty(window, "miro", {
101
+ get() {
102
+ return miroValue;
103
+ },
104
+ set(value) {
105
+ clearTimeout(timeout);
106
+ clearInterval(authModalChecker);
107
+ miroValue = value;
108
+ resolve();
109
+ }
110
+ });
111
+ }),
112
+ options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS
113
+ );
114
+ } catch (err) {
115
+ await browser.close();
116
+ this.context.reject(err);
117
+ this.isDisposed = true;
118
+ return;
119
+ }
66
120
 
67
121
  this.context.resolve({ browser, page });
68
122
  }
69
123
 
70
124
  async dispose() {
125
+ if (this.isDisposed) {
126
+ return;
127
+ }
128
+ this.isDisposed = true;
71
129
  await (await this.browser).close();
72
130
  }
73
131
 
package/src/miro-types.ts CHANGED
@@ -69,15 +69,14 @@ interface BoardObjectWithDimensions {
69
69
  }
70
70
 
71
71
  export interface PreviewBoardObject
72
- extends BoardItemBase,
73
- BoardObjectBase,
74
- BoardObjectWithDimensions {
72
+ extends BoardItemBase, BoardObjectBase, BoardObjectWithDimensions {
75
73
  type: "preview";
76
74
  url: string;
77
75
  }
78
76
 
79
77
  export interface FrameBoardObject
80
- extends Omit<BoardItemBase, "connectorIds">,
78
+ extends
79
+ Omit<BoardItemBase, "connectorIds">,
81
80
  BoardObjectBase,
82
81
  BoardObjectWithDimensions,
83
82
  BoardObjectWithCoordinates {
@@ -96,7 +95,8 @@ export interface GroupBoardItem extends BoardItemBase {
96
95
  }
97
96
 
98
97
  export interface StickyNoteBoardObject
99
- extends BoardItemBase,
98
+ extends
99
+ BoardItemBase,
100
100
  BoardObjectBase,
101
101
  BoardObjectWithCoordinates,
102
102
  BoardObjectWithDimensions {
@@ -112,7 +112,8 @@ export interface StickyNoteBoardObject
112
112
  }
113
113
 
114
114
  export interface TextBoardObject
115
- extends BoardItemBase,
115
+ extends
116
+ BoardItemBase,
116
117
  BoardObjectBase,
117
118
  BoardObjectWithCoordinates,
118
119
  BoardObjectWithDimensions {
@@ -130,7 +131,8 @@ export interface TextBoardObject
130
131
  }
131
132
 
132
133
  export interface ImageBoardObject
133
- extends BoardItemBase,
134
+ extends
135
+ BoardItemBase,
134
136
  BoardObjectBase,
135
137
  BoardObjectWithDimensions,
136
138
  BoardObjectWithCoordinates {
@@ -159,9 +161,7 @@ interface TableStyle {
159
161
  }
160
162
 
161
163
  export interface TableBoardObject
162
- extends BoardItemBase,
163
- BoardObjectWithDimensions,
164
- BoardObjectWithCoordinates {
164
+ extends BoardItemBase, BoardObjectWithDimensions, BoardObjectWithCoordinates {
165
165
  type: "image";
166
166
  rotation: number;
167
167
  cols?: number | { width: number }[];
@@ -176,7 +176,8 @@ export interface TableBoardObject
176
176
  }
177
177
 
178
178
  export interface StructDocBoardObject
179
- extends BoardItemBase,
179
+ extends
180
+ BoardItemBase,
180
181
  BoardObjectBase,
181
182
  BoardObjectWithDimensions,
182
183
  BoardObjectWithCoordinates {
@@ -186,9 +187,7 @@ export interface StructDocBoardObject
186
187
  }
187
188
 
188
189
  export interface EmbedBoardObject
189
- extends BoardItemBase,
190
- BoardObjectBase,
191
- BoardObjectWithCoordinates {
190
+ extends BoardItemBase, BoardObjectBase, BoardObjectWithCoordinates {
192
191
  type: "struct_doc";
193
192
  url: string;
194
193
  previewUrl: string;
@@ -198,7 +197,8 @@ export interface EmbedBoardObject
198
197
  }
199
198
 
200
199
  export interface ShapeBoardObject
201
- extends BoardItemBase,
200
+ extends
201
+ BoardItemBase,
202
202
  BoardObjectBase,
203
203
  BoardObjectWithCoordinates,
204
204
  BoardObjectWithDimensions {
@@ -265,7 +265,8 @@ export interface TagBoardItem extends BoardItemBase {
265
265
  }
266
266
 
267
267
  export interface KanbanBoardObject
268
- extends BoardItemBase,
268
+ extends
269
+ BoardItemBase,
269
270
  BoardObjectBase,
270
271
  BoardObjectWithCoordinates,
271
272
  BoardObjectWithDimensions {
@@ -273,7 +274,8 @@ export interface KanbanBoardObject
273
274
  }
274
275
 
275
276
  export interface UnsupportedBoardObject
276
- extends BoardItemBase,
277
+ extends
278
+ BoardItemBase,
277
279
  BoardObjectBase,
278
280
  BoardObjectWithCoordinates,
279
281
  BoardObjectWithDimensions {
package/tests/api.test.ts CHANGED
@@ -3,12 +3,18 @@ import assert from "node:assert";
3
3
  import { MiroBoard } from "../src";
4
4
 
5
5
  const boardId = process.env.TEST_BOARD_ID;
6
+ const inaccessibleBoardId = process.env.PRIVATE_TEST_BOARD_ID;
6
7
 
7
8
  if (!boardId) {
8
9
  console.error("TEST_BOARD_ID environment variable is required.");
9
10
  process.exit(1);
10
11
  }
11
12
 
13
+ if (!inaccessibleBoardId) {
14
+ console.error("PRIVATE_TEST_BOARD_ID environment variable is required.");
15
+ process.exit(1);
16
+ }
17
+
12
18
  describe("Miro integration", async () => {
13
19
  const miroBoard = new MiroBoard({ boardId });
14
20
 
@@ -66,3 +72,17 @@ describe("Miro integration", async () => {
66
72
  assert.ok(svg.length > 10_000);
67
73
  });
68
74
  });
75
+
76
+ await it("should throw error for a non-public board", async () => {
77
+ try {
78
+ const miroBoard = new MiroBoard({ boardId: inaccessibleBoardId });
79
+ await miroBoard.getBoardObjects({});
80
+ assert.fail();
81
+ } catch (err) {
82
+ assert.ok(err instanceof Error);
83
+ assert.equal(
84
+ err.message,
85
+ "Miro board requires authentication. Check board access settings to allow anonymous access or supply a token."
86
+ );
87
+ }
88
+ });