miro-export 1.0.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.
- package/.eslintrc.json +24 -0
- package/.github/dependabot.yml +10 -0
- package/.github/workflows/build.yml +28 -0
- package/.github/workflows/dependabot-merge.yml +21 -0
- package/.github/workflows/publish.yml +23 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/build/index.js +76 -0
- package/index.ts +124 -0
- package/package.json +41 -0
- package/tsconfig.json +13 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"parser": "@typescript-eslint/parser",
|
|
4
|
+
"parserOptions": {
|
|
5
|
+
"ecmaVersion": 6,
|
|
6
|
+
"sourceType": "module"
|
|
7
|
+
},
|
|
8
|
+
"plugins": ["@typescript-eslint", "promise"],
|
|
9
|
+
"rules": {
|
|
10
|
+
"@typescript-eslint/naming-convention": "warn",
|
|
11
|
+
"@typescript-eslint/semi": "warn",
|
|
12
|
+
"curly": "warn",
|
|
13
|
+
"eqeqeq": "warn",
|
|
14
|
+
"no-throw-literal": "warn",
|
|
15
|
+
"semi": "off"
|
|
16
|
+
},
|
|
17
|
+
"extends": [
|
|
18
|
+
"eslint:recommended",
|
|
19
|
+
"plugin:@typescript-eslint/recommended",
|
|
20
|
+
"plugin:promise/recommended",
|
|
21
|
+
"prettier"
|
|
22
|
+
],
|
|
23
|
+
"ignorePatterns": ["build"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: pnpm/action-setup@v3
|
|
13
|
+
with:
|
|
14
|
+
version: 8
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: "20.x"
|
|
18
|
+
- name: Install dependencies
|
|
19
|
+
run: pnpm i --frozen-lockfile
|
|
20
|
+
- name: Lint
|
|
21
|
+
run: pnpm run lint
|
|
22
|
+
- name: Build
|
|
23
|
+
run: pnpm run build
|
|
24
|
+
- name: Upload Artifact
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: build
|
|
28
|
+
path: build/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Dependabot auto-merge
|
|
2
|
+
on: pull_request
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
pull-requests: write
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
dependabot:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
if: github.actor == 'dependabot[bot]'
|
|
11
|
+
steps:
|
|
12
|
+
- name: Dependabot metadata
|
|
13
|
+
id: metadata
|
|
14
|
+
uses: dependabot/fetch-metadata@v1
|
|
15
|
+
with:
|
|
16
|
+
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
17
|
+
- name: Merge a PR
|
|
18
|
+
run: gh pr merge --auto --merge "$PR_URL"
|
|
19
|
+
env:
|
|
20
|
+
PR_URL: ${{github.event.pull_request.html_url}}
|
|
21
|
+
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish package to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: pnpm/action-setup@v3
|
|
13
|
+
with:
|
|
14
|
+
version: 8
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: "20.x"
|
|
18
|
+
registry-url: "https://registry.npmjs.org"
|
|
19
|
+
- run: pnpm install --frozen-lockfile
|
|
20
|
+
- run: pnpm run build
|
|
21
|
+
- run: pnpm publish --no-git-checks
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 jolle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Miro board exporter
|
|
2
|
+
|
|
3
|
+
Exports Miro frames as full-detail SVGs using a headless Puppeteer browser. Requires a personal Miro token.
|
|
4
|
+
|
|
5
|
+
## Getting the Miro token
|
|
6
|
+
|
|
7
|
+
Log in to Miro using your regular web browser, and then copy the value of the "token" cookie from developer tools. This is the token that this tool requires.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Options:
|
|
13
|
+
-t, --token <token> Miro token
|
|
14
|
+
-b, --board-id <boardId> The board ID
|
|
15
|
+
-f, --frame-names <frameNames...> The frame name(s), leave empty to export entire board
|
|
16
|
+
-o, --output-file <filename> A file to output the SVG to (stdout if not supplied)
|
|
17
|
+
-h, --help display help for command
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# export "Frame 2" to the file "My Frame 2.svg"
|
|
24
|
+
miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" -o "My Frame 2.svg"
|
|
25
|
+
|
|
26
|
+
# export entire board to stdout
|
|
27
|
+
miro-export -t XYZ -b uMoVLkx8gIc=
|
|
28
|
+
|
|
29
|
+
# export "Frame 2" and "Frame 3" to "Frame 2.svg" and "Frame 3.svg" respectively
|
|
30
|
+
miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" "Frame 3" -o "{frameName}.svg"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Capturing multiple frames at once
|
|
34
|
+
|
|
35
|
+
It is possible to supply multiple frames to the `-f` switch, e.g., `-f "Frame 2" "Frame 3"`. However, this will capture all content that is within the outer bounding box when all frames have been selected, so content between the frames will be captured as well. If you want separate SVGs for each frame, use the output file switch with `{frameName}` in the file name, e.g., `-o "Export - {frameName}.svg"`. It is not possible to export separate SVGs without the output file specified (i.e., to stdout).
|
package/build/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const promises_1 = require("fs/promises");
|
|
8
|
+
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
9
|
+
const extra_typings_1 = require("@commander-js/extra-typings");
|
|
10
|
+
const { token, boardId, frameNames, outputFile } = extra_typings_1.program
|
|
11
|
+
.requiredOption("-t, --token <token>", "Miro token")
|
|
12
|
+
.requiredOption("-b, --board-id <boardId>", "The board ID")
|
|
13
|
+
.option("-f, --frame-names <frameNames...>", "The frame name(s), leave empty to export entire board")
|
|
14
|
+
.option("-o, --output-file <filename>", "A file to output the SVG to (stdout if not supplied)")
|
|
15
|
+
.parse()
|
|
16
|
+
.opts();
|
|
17
|
+
(async () => {
|
|
18
|
+
const browser = await puppeteer_1.default.launch({ headless: true });
|
|
19
|
+
const page = await browser.newPage();
|
|
20
|
+
await page.setCookie({
|
|
21
|
+
name: "token",
|
|
22
|
+
value: token,
|
|
23
|
+
domain: "miro.com"
|
|
24
|
+
});
|
|
25
|
+
await page.setViewport({ width: 1080, height: 1024 });
|
|
26
|
+
await page.goto(`https://miro.com/app/board/${boardId}/`, {
|
|
27
|
+
waitUntil: "domcontentloaded"
|
|
28
|
+
});
|
|
29
|
+
await page.evaluate(() => new Promise((resolve) => {
|
|
30
|
+
const interval = setInterval(() => {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof window.miro?.board !== "undefined") {
|
|
33
|
+
resolve();
|
|
34
|
+
clearInterval(interval);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// ignored
|
|
39
|
+
}
|
|
40
|
+
}, 100);
|
|
41
|
+
}));
|
|
42
|
+
const getSvgForFrames = (frameNames) => page.evaluate(async (frameNames) => {
|
|
43
|
+
if (frameNames) {
|
|
44
|
+
const frames = await window.miro.board.get({ type: ["frame"] });
|
|
45
|
+
const selectedFrames = frames.filter((frame) => frameNames.includes(frame.title));
|
|
46
|
+
if (selectedFrames.length !== frameNames.length) {
|
|
47
|
+
throw Error(`${frameNames.length - selectedFrames.length} frame(s) could not be found on the board.`);
|
|
48
|
+
}
|
|
49
|
+
await window.miro.board.deselect();
|
|
50
|
+
for (const { id } of selectedFrames) {
|
|
51
|
+
await window.miro.board.select({ id });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return await window.cmd.board.api.export.makeVector();
|
|
55
|
+
}, frameNames);
|
|
56
|
+
if (outputFile?.includes("{frameName}")) {
|
|
57
|
+
if (!frameNames) {
|
|
58
|
+
throw Error("Expected frame names to be given when the output file name format expects a frame name.");
|
|
59
|
+
}
|
|
60
|
+
for (const frameName of frameNames) {
|
|
61
|
+
const svg = await getSvgForFrames([frameName]);
|
|
62
|
+
await (0, promises_1.writeFile)(outputFile.replace("{frameName}", frameName), svg);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const svg = await getSvgForFrames(frameNames);
|
|
67
|
+
if (outputFile) {
|
|
68
|
+
await (0, promises_1.writeFile)(outputFile, svg);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
process.stdout.write(svg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await page.close();
|
|
75
|
+
await browser.close();
|
|
76
|
+
})();
|
package/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "miro-export",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "jolle <npm-contact@jolle.io>",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Export Miro boards or frames as SVGs",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"miro",
|
|
9
|
+
"svg",
|
|
10
|
+
"export"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/jolle/miro-export.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/jolle/miro-export/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/jolle/miro-export",
|
|
20
|
+
"main": "./build/index.js",
|
|
21
|
+
"bin": "./build/index.js",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.11.19",
|
|
24
|
+
"eslint": "^8.56.0",
|
|
25
|
+
"eslint-config-prettier": "^9.1.0",
|
|
26
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
27
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
28
|
+
"prettier": "^3.2.5",
|
|
29
|
+
"typescript": "^5.3.3",
|
|
30
|
+
"typescript-eslint": "^7.0.1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@commander-js/extra-typings": "^12.0.0",
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"puppeteer": "^22.1.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"lint": "eslint *.ts && prettier -c *.ts",
|
|
39
|
+
"build": "tsc && echo \"#!/usr/bin/env node\n$(cat ./build/index.js)\" > ./build/index.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["DOM"],
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"outDir": "./build",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
}
|
|
13
|
+
}
|