├── .github └── workflows │ └── run.yml ├── .gitignore ├── .npmignore ├── .vscode ├── cspell.json ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── prettier.config.js ├── src ├── clients.ts ├── crawler.ts ├── index.ts ├── libs.ts ├── notion.types.ts ├── serializer │ ├── __tests__ │ │ └── utils.test.ts │ ├── block │ │ ├── __tests__ │ │ │ └── defaults.test.ts │ │ ├── defaults.ts │ │ ├── index.ts │ │ ├── strategy.ts │ │ └── types.ts │ ├── index.ts │ ├── property │ │ ├── defaults.ts │ │ ├── index.ts │ │ ├── strategy.ts │ │ └── types.ts │ └── utils.ts ├── types.ts └── utils.ts └── tsconfig.json /.github/workflows/run.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | unit-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | 22 | - uses: pnpm/action-setup@v3 23 | name: Install pnpm 24 | with: 25 | version: 8 26 | run_install: false 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Run tests 32 | run: pnpm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | src/test.ts 133 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.json 3 | src 4 | -------------------------------------------------------------------------------- /.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "notionhq", 6 | "tsup" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TomPenguin(mochi) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notion-md-crawler 2 | 3 | A library to recursively retrieve and serialize Notion pages and databases with customization for machine learning applications. 4 | 5 | [![NPM Version](https://badge.fury.io/js/notion-md-crawler.svg)](https://www.npmjs.com/package/notion-md-crawler) 6 | 7 | ## 🌟 Features 8 | 9 | - **🕷️ Crawling Pages and Databases**: Dig deep into Notion's hierarchical structure with ease. 10 | - **📝 Serialize to Markdown**: Seamlessly convert Notion pages to Markdown for easy use in machine learning and other. 11 | - **🛠️ Custom Serialization**: Adapt the serialization process to fit your specific machine learning needs. 12 | - **⏳ Async Generator**: Yields results on a page-by-page basis, so even huge documents can be made memory efficient. 13 | 14 | ## 🛠️ Installation 15 | 16 | [`@notionhq/client`](https://github.com/makenotion/notion-sdk-js) must also be installed. 17 | 18 | Using npm 📦: 19 | 20 | ```bash 21 | npm install notion-md-crawler @notionhq/client 22 | ``` 23 | 24 | Using yarn 🧶: 25 | 26 | ```bash 27 | yarn add notion-md-crawler @notionhq/client 28 | ``` 29 | 30 | Using pnpm 🚀: 31 | 32 | ```bash 33 | pnpm add notion-md-crawler @notionhq/client 34 | ``` 35 | 36 | ## 🚀 Quick Start 37 | 38 | > ⚠️ Note: Before getting started, create [an integration and find the token](https://www.notion.so/my-integrations). Details on methods can be found in [API section](https://github.com/souvikinator/notion-to-md#api) 39 | 40 | Leveraging the power of JavaScript generators, this library is engineered to handle even the most extensive Notion documents with ease. It's designed to yield results page-by-page, allowing for efficient memory usage and real-time processing. 41 | 42 | ```ts 43 | import { Client } from "@notionhq/client"; 44 | import { crawler, pageToString } from "notion-md-crawler"; 45 | 46 | // Need init notion client with credential. 47 | const client = new Client({ auth: process.env.NOTION_API_KEY }); 48 | 49 | const crawl = crawler({ client }); 50 | 51 | const main = async () => { 52 | const rootPageId = "****"; 53 | for await (const result of crawl(rootPageId)) { 54 | if (result.success) { 55 | const pageText = pageToString(result.page); 56 | console.log(pageText); 57 | } 58 | } 59 | }; 60 | 61 | main(); 62 | ``` 63 | 64 | ## 🌐 API 65 | 66 | ### crawler 67 | 68 | Recursively crawl the Notion Page. [`dbCrawler`](#dbcrawler) should be used if the Root is a Notion Database. 69 | 70 | > Note: It tries to continue crawling as much as possible even if it fails to retrieve a particular Notion Page. 71 | 72 | #### Parameters: 73 | 74 | - `options` ([`CrawlerOptions`](#optionscrawleroptions)): Crawler options. 75 | - `rootPageId` (string): Id of the root page to be crawled. 76 | 77 | #### Returns: 78 | 79 | - `AsyncGenerator`: Crawling results with failed information. 80 | 81 | ### dbCrawler 82 | 83 | Recursively crawl the Notion Database. [`crawler`](#crawler) should be used if the Root is a Notion Page. 84 | 85 | #### Parameters: 86 | 87 | - `options` ([`CrawlerOptions`](#crawleroptions)): Crawler options. 88 | - `rootDatabaseId` (string): Id of the root page to be crawled. 89 | 90 | #### Returns: 91 | 92 | - `AsyncGenerator`: Crawling results with failed information. 93 | 94 | ### CrawlerOptions 95 | 96 | | Option | Description | Type | Default | 97 | | ------------------------ | --------------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------- | 98 | | `client` | Instance of Notion Client. Set up an instance of the Client class imported from `@notionhq/client`. | Notion Client | - | 99 | | `serializers?` | Used for custom serialization of Block and Property objects. | Object | `undefined` | 100 | | `serializers?.block?` | Map of Notion block type and [`BlockSerializer`](#blockserializer). | [`BlockSerializers`](#blockserializers) | `undefined` | 101 | | `serializers?.property?` | Map of Notion Property Type and [`PropertySerializer`](#propertyserializer). | [`PropertySerializers`](#propertyserializers) | `undefined` | 102 | | `metadataBuilder?` | The metadata generation process can be customize. | [`MetadataBuilder`](#metadatabuilder) | `undefined` | 103 | | `urlMask?` | If specified, the url is masked with the string. | string \| false | `false` | 104 | | `skipPageIds?` | List of page Ids to skip crawling (also skips descendant pages) | string[] | `undefined` | 105 | 106 | #### `BlockSerializers` 107 | 108 | Map with Notion block type (like `"heading_1"`, `"to_do"`, `"code"`) as key and [`BlockSerializer`](#blockserializer) as value. 109 | 110 | #### `BlockSerializer` 111 | 112 | BlockSerializer that takes a Notion block object as argument. Returning `false` will skip serialization of that Notion block. 113 | 114 | **[Type]** 115 | 116 | ```ts 117 | type BlockSerializer = ( 118 | block: NotionBlock, 119 | ) => string | false | Promise; 120 | ``` 121 | 122 | #### `PropertySerializers` 123 | 124 | Map with Notion Property Type (like `"heading_1"`, `"to_do"`, `"code"`) as key and [`PropertySerializer`](#propertyserializer) as value. 125 | 126 | #### `PropertySerializer` 127 | 128 | PropertySerializer that takes a Notion property object as argument. Returning `false` will skip serialization of that Notion property. 129 | 130 | **[Type]** 131 | 132 | ```ts 133 | type PropertySerializer = ( 134 | name: string, 135 | block: NotionBlock, 136 | ) => string | false | Promise; 137 | ``` 138 | 139 | #### `MetadataBuilder` 140 | 141 | Retrieving metadata is sometimes very important, but the information you want to retrieve will vary depending on the context. `MetadataBuilder` allows you to customize it according to your use case. 142 | 143 | **[Example]** 144 | 145 | ```ts 146 | import { crawler, MetadataBuilderParams } from "notion-md-crawler"; 147 | 148 | const getUrl = (id: string) => `https://www.notion.so/${id.replace(/-/g, "")}`; 149 | 150 | const metadataBuilder = ({ page }: MetadataBuilderParams) => ({ 151 | url: getUrl(page.metadata.id), 152 | }); 153 | 154 | const crawl = crawler({ client, metadataBuilder }); 155 | 156 | for await (const result of crawl("notion-page-id")) { 157 | if (result.success) { 158 | console.log(result.page.metadata.url); // "https://www.notion.so/********" 159 | } 160 | } 161 | ``` 162 | 163 | ## 📊 Use Metadata 164 | 165 | Since `crawler` returns `Page` objects and `Page` object contain metadata, you can be used it for machine learning. 166 | 167 | ## 🛠️ Custom Serialization 168 | 169 | `notion-md-crawler` gives you the flexibility to customize the serialization logic for various Notion objects to cater to the unique requirements of your machine learning model or any other use case. 170 | 171 | ### Define your custom serializer 172 | 173 | You can define your own custom serializer. You can also use the utility function for convenience. 174 | 175 | ```ts 176 | import { BlockSerializer, crawler, serializer } from "notion-md-crawler"; 177 | 178 | const customEmbedSerializer: BlockSerializer<"embed"> = (block) => { 179 | if (block.embed.url) return ""; 180 | 181 | // You can use serializer utility. 182 | const caption = serializer.utils.fromRichText(block.embed.caption); 183 | 184 | return `
185 | 186 |
${caption}
187 |
`; 188 | }; 189 | 190 | const serializers = { 191 | block: { 192 | embed: customEmbedSerializer, 193 | }, 194 | }; 195 | 196 | const crawl = crawler({ client, serializers }); 197 | ``` 198 | 199 | ### Skip serialize 200 | 201 | Returning `false` in the serializer allows you to skip the serialize of that block. This is useful when you want to omit unnecessary information. 202 | 203 | ```ts 204 | const image: BlockSerializer<"image"> = () => false; 205 | const crawl = crawler({ client, serializers: { block: { image } } }); 206 | ``` 207 | 208 | ### Advanced: Use default serializer in custom serializer 209 | 210 | If you want to customize serialization only in specific cases, you can use the default serializer in a custom serializer. 211 | 212 | ```ts 213 | import { BlockSerializer, crawler, serializer } from "notion-md-crawler"; 214 | 215 | const defaultImageSerializer = serializer.block.defaults.image; 216 | 217 | const customImageSerializer: BlockSerializer<"image"> = (block) => { 218 | // Utility function to retrieve the link 219 | const { title, href } = serializer.utils.fromLink(block.image); 220 | 221 | // If the image is from a specific domain, wrap it in a special div 222 | if (href.includes("special-domain.com")) { 223 | return `
224 | ${defaultImageSerializer(block)} 225 |
`; 226 | } 227 | 228 | // Use the default serializer for all other images 229 | return defaultImageSerializer(block); 230 | }; 231 | 232 | const serializers = { 233 | block: { 234 | image: customImageSerializer, 235 | }, 236 | }; 237 | 238 | const crawl = crawler({ client, serializers }); 239 | ``` 240 | 241 | ## 🔍 Supported Blocks and Database properties 242 | 243 | ### Blocks 244 | 245 | | Block Type | Supported | 246 | | ------------------ | --------- | 247 | | Text | ✅ Yes | 248 | | Bookmark | ✅ Yes | 249 | | Bulleted List | ✅ Yes | 250 | | Numbered List | ✅ Yes | 251 | | Heading 1 | ✅ Yes | 252 | | Heading 2 | ✅ Yes | 253 | | Heading 3 | ✅ Yes | 254 | | Quote | ✅ Yes | 255 | | Callout | ✅ Yes | 256 | | Equation (block) | ✅ Yes | 257 | | Equation (inline) | ✅ Yes | 258 | | Todos (checkboxes) | ✅ Yes | 259 | | Table Of Contents | ✅ Yes | 260 | | Divider | ✅ Yes | 261 | | Column | ✅ Yes | 262 | | Column List | ✅ Yes | 263 | | Toggle | ✅ Yes | 264 | | Image | ✅ Yes | 265 | | Embed | ✅ Yes | 266 | | Video | ✅ Yes | 267 | | Figma | ✅ Yes | 268 | | PDF | ✅ Yes | 269 | | Audio | ✅ Yes | 270 | | File | ✅ Yes | 271 | | Link | ✅ Yes | 272 | | Page Link | ✅ Yes | 273 | | External Page Link | ✅ Yes | 274 | | Code (block) | ✅ Yes | 275 | | Code (inline) | ✅ Yes | 276 | 277 | ### Database Properties 278 | 279 | | Property Type | Supported | 280 | | ---------------- | --------- | 281 | | Checkbox | ✅ Yes | 282 | | Created By | ✅ Yes | 283 | | Created Time | ✅ Yes | 284 | | Date | ✅ Yes | 285 | | Email | ✅ Yes | 286 | | Files | ✅ Yes | 287 | | Formula | ✅ Yes | 288 | | Last Edited By | ✅ Yes | 289 | | Last Edited Time | ✅ Yes | 290 | | Multi Select | ✅ Yes | 291 | | Number | ✅ Yes | 292 | | People | ✅ Yes | 293 | | Phone Number | ✅ Yes | 294 | | Relation | ✅ Yes | 295 | | Rich Text | ✅ Yes | 296 | | Rollup | ✅ Yes | 297 | | Select | ✅ Yes | 298 | | Status | ✅ Yes | 299 | | Title | ✅ Yes | 300 | | Unique Id | ✅ Yes | 301 | | Url | ✅ Yes | 302 | | Verification | □ No | 303 | 304 | ## 💬 Issues and Feedback 305 | 306 | For any issues, feedback, or feature requests, please file an issue on GitHub. 307 | 308 | ## 📜 License 309 | 310 | MIT 311 | 312 | --- 313 | 314 | Made with ❤️ by TomPenguin. 315 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-md-crawler", 3 | "version": "1.0.1", 4 | "description": "A library to recursively retrieve and serialize Notion pages with customization for machine learning applications.", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsup src/index.ts --format cjs,esm --dts --clean --minify --sourcemap", 21 | "prepublishOnly": "pnpm run build", 22 | "test": "vitest" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/TomPenguin/notion-md-crawler.git" 27 | }, 28 | "keywords": [ 29 | "notion", 30 | "crawler", 31 | "crawling", 32 | "serialization", 33 | "machine-learning", 34 | "ai", 35 | "markdown" 36 | ], 37 | "homepage": "https://github.com/TomPenguin/notion-md-crawler#readme", 38 | "author": "TomPenguin (https://github.com/TomPenguin)", 39 | "bugs": { 40 | "url": "https://github.com/TomPenguin/notion-md-crawler/issues" 41 | }, 42 | "license": "MIT", 43 | "devDependencies": { 44 | "@types/node": "^22.10.9", 45 | "prettier": "^3.4.2", 46 | "prettier-plugin-organize-imports": "^4.1.0", 47 | "tsup": "^8.3.5", 48 | "vitest": "^3.0.3" 49 | }, 50 | "dependencies": { 51 | "@notionhq/client": "^2.2.15", 52 | "md-utils-ts": "^2.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@notionhq/client': 9 | specifier: ^2.2.15 10 | version: 2.2.15 11 | md-utils-ts: 12 | specifier: ^2.0.0 13 | version: 2.0.0 14 | 15 | devDependencies: 16 | '@types/node': 17 | specifier: ^22.10.9 18 | version: 22.10.9 19 | prettier: 20 | specifier: ^3.4.2 21 | version: 3.4.2 22 | prettier-plugin-organize-imports: 23 | specifier: ^4.1.0 24 | version: 4.1.0(prettier@3.4.2)(typescript@5.7.3) 25 | tsup: 26 | specifier: ^8.3.5 27 | version: 8.3.5(typescript@5.7.3) 28 | vitest: 29 | specifier: ^3.0.3 30 | version: 3.0.3(@types/node@22.10.9) 31 | 32 | packages: 33 | 34 | /@esbuild/aix-ppc64@0.24.2: 35 | resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 36 | engines: {node: '>=18'} 37 | cpu: [ppc64] 38 | os: [aix] 39 | requiresBuild: true 40 | dev: true 41 | optional: true 42 | 43 | /@esbuild/android-arm64@0.24.2: 44 | resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 45 | engines: {node: '>=18'} 46 | cpu: [arm64] 47 | os: [android] 48 | requiresBuild: true 49 | dev: true 50 | optional: true 51 | 52 | /@esbuild/android-arm@0.24.2: 53 | resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 54 | engines: {node: '>=18'} 55 | cpu: [arm] 56 | os: [android] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@esbuild/android-x64@0.24.2: 62 | resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 63 | engines: {node: '>=18'} 64 | cpu: [x64] 65 | os: [android] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@esbuild/darwin-arm64@0.24.2: 71 | resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 72 | engines: {node: '>=18'} 73 | cpu: [arm64] 74 | os: [darwin] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@esbuild/darwin-x64@0.24.2: 80 | resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 81 | engines: {node: '>=18'} 82 | cpu: [x64] 83 | os: [darwin] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@esbuild/freebsd-arm64@0.24.2: 89 | resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 90 | engines: {node: '>=18'} 91 | cpu: [arm64] 92 | os: [freebsd] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@esbuild/freebsd-x64@0.24.2: 98 | resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 99 | engines: {node: '>=18'} 100 | cpu: [x64] 101 | os: [freebsd] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@esbuild/linux-arm64@0.24.2: 107 | resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 108 | engines: {node: '>=18'} 109 | cpu: [arm64] 110 | os: [linux] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@esbuild/linux-arm@0.24.2: 116 | resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 117 | engines: {node: '>=18'} 118 | cpu: [arm] 119 | os: [linux] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@esbuild/linux-ia32@0.24.2: 125 | resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 126 | engines: {node: '>=18'} 127 | cpu: [ia32] 128 | os: [linux] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@esbuild/linux-loong64@0.24.2: 134 | resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 135 | engines: {node: '>=18'} 136 | cpu: [loong64] 137 | os: [linux] 138 | requiresBuild: true 139 | dev: true 140 | optional: true 141 | 142 | /@esbuild/linux-mips64el@0.24.2: 143 | resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 144 | engines: {node: '>=18'} 145 | cpu: [mips64el] 146 | os: [linux] 147 | requiresBuild: true 148 | dev: true 149 | optional: true 150 | 151 | /@esbuild/linux-ppc64@0.24.2: 152 | resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 153 | engines: {node: '>=18'} 154 | cpu: [ppc64] 155 | os: [linux] 156 | requiresBuild: true 157 | dev: true 158 | optional: true 159 | 160 | /@esbuild/linux-riscv64@0.24.2: 161 | resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 162 | engines: {node: '>=18'} 163 | cpu: [riscv64] 164 | os: [linux] 165 | requiresBuild: true 166 | dev: true 167 | optional: true 168 | 169 | /@esbuild/linux-s390x@0.24.2: 170 | resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 171 | engines: {node: '>=18'} 172 | cpu: [s390x] 173 | os: [linux] 174 | requiresBuild: true 175 | dev: true 176 | optional: true 177 | 178 | /@esbuild/linux-x64@0.24.2: 179 | resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 180 | engines: {node: '>=18'} 181 | cpu: [x64] 182 | os: [linux] 183 | requiresBuild: true 184 | dev: true 185 | optional: true 186 | 187 | /@esbuild/netbsd-arm64@0.24.2: 188 | resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 189 | engines: {node: '>=18'} 190 | cpu: [arm64] 191 | os: [netbsd] 192 | requiresBuild: true 193 | dev: true 194 | optional: true 195 | 196 | /@esbuild/netbsd-x64@0.24.2: 197 | resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 198 | engines: {node: '>=18'} 199 | cpu: [x64] 200 | os: [netbsd] 201 | requiresBuild: true 202 | dev: true 203 | optional: true 204 | 205 | /@esbuild/openbsd-arm64@0.24.2: 206 | resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 207 | engines: {node: '>=18'} 208 | cpu: [arm64] 209 | os: [openbsd] 210 | requiresBuild: true 211 | dev: true 212 | optional: true 213 | 214 | /@esbuild/openbsd-x64@0.24.2: 215 | resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 216 | engines: {node: '>=18'} 217 | cpu: [x64] 218 | os: [openbsd] 219 | requiresBuild: true 220 | dev: true 221 | optional: true 222 | 223 | /@esbuild/sunos-x64@0.24.2: 224 | resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 225 | engines: {node: '>=18'} 226 | cpu: [x64] 227 | os: [sunos] 228 | requiresBuild: true 229 | dev: true 230 | optional: true 231 | 232 | /@esbuild/win32-arm64@0.24.2: 233 | resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 234 | engines: {node: '>=18'} 235 | cpu: [arm64] 236 | os: [win32] 237 | requiresBuild: true 238 | dev: true 239 | optional: true 240 | 241 | /@esbuild/win32-ia32@0.24.2: 242 | resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 243 | engines: {node: '>=18'} 244 | cpu: [ia32] 245 | os: [win32] 246 | requiresBuild: true 247 | dev: true 248 | optional: true 249 | 250 | /@esbuild/win32-x64@0.24.2: 251 | resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 252 | engines: {node: '>=18'} 253 | cpu: [x64] 254 | os: [win32] 255 | requiresBuild: true 256 | dev: true 257 | optional: true 258 | 259 | /@isaacs/cliui@8.0.2: 260 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 261 | engines: {node: '>=12'} 262 | dependencies: 263 | string-width: 5.1.2 264 | string-width-cjs: /string-width@4.2.3 265 | strip-ansi: 7.1.0 266 | strip-ansi-cjs: /strip-ansi@6.0.1 267 | wrap-ansi: 8.1.0 268 | wrap-ansi-cjs: /wrap-ansi@7.0.0 269 | dev: true 270 | 271 | /@jridgewell/gen-mapping@0.3.8: 272 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 273 | engines: {node: '>=6.0.0'} 274 | dependencies: 275 | '@jridgewell/set-array': 1.2.1 276 | '@jridgewell/sourcemap-codec': 1.5.0 277 | '@jridgewell/trace-mapping': 0.3.25 278 | dev: true 279 | 280 | /@jridgewell/resolve-uri@3.1.2: 281 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 282 | engines: {node: '>=6.0.0'} 283 | dev: true 284 | 285 | /@jridgewell/set-array@1.2.1: 286 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 287 | engines: {node: '>=6.0.0'} 288 | dev: true 289 | 290 | /@jridgewell/sourcemap-codec@1.5.0: 291 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 292 | dev: true 293 | 294 | /@jridgewell/trace-mapping@0.3.25: 295 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 296 | dependencies: 297 | '@jridgewell/resolve-uri': 3.1.2 298 | '@jridgewell/sourcemap-codec': 1.5.0 299 | dev: true 300 | 301 | /@notionhq/client@2.2.15: 302 | resolution: {integrity: sha512-XhdSY/4B1D34tSco/GION+23GMjaS9S2zszcqYkMHo8RcWInymF6L1x+Gk7EmHdrSxNFva2WM8orhC4BwQCwgw==} 303 | engines: {node: '>=12'} 304 | dependencies: 305 | '@types/node-fetch': 2.6.12 306 | node-fetch: 2.7.0 307 | transitivePeerDependencies: 308 | - encoding 309 | dev: false 310 | 311 | /@pkgjs/parseargs@0.11.0: 312 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 313 | engines: {node: '>=14'} 314 | requiresBuild: true 315 | dev: true 316 | optional: true 317 | 318 | /@rollup/rollup-android-arm-eabi@4.31.0: 319 | resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==} 320 | cpu: [arm] 321 | os: [android] 322 | requiresBuild: true 323 | dev: true 324 | optional: true 325 | 326 | /@rollup/rollup-android-arm64@4.31.0: 327 | resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==} 328 | cpu: [arm64] 329 | os: [android] 330 | requiresBuild: true 331 | dev: true 332 | optional: true 333 | 334 | /@rollup/rollup-darwin-arm64@4.31.0: 335 | resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==} 336 | cpu: [arm64] 337 | os: [darwin] 338 | requiresBuild: true 339 | dev: true 340 | optional: true 341 | 342 | /@rollup/rollup-darwin-x64@4.31.0: 343 | resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==} 344 | cpu: [x64] 345 | os: [darwin] 346 | requiresBuild: true 347 | dev: true 348 | optional: true 349 | 350 | /@rollup/rollup-freebsd-arm64@4.31.0: 351 | resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==} 352 | cpu: [arm64] 353 | os: [freebsd] 354 | requiresBuild: true 355 | dev: true 356 | optional: true 357 | 358 | /@rollup/rollup-freebsd-x64@4.31.0: 359 | resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==} 360 | cpu: [x64] 361 | os: [freebsd] 362 | requiresBuild: true 363 | dev: true 364 | optional: true 365 | 366 | /@rollup/rollup-linux-arm-gnueabihf@4.31.0: 367 | resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==} 368 | cpu: [arm] 369 | os: [linux] 370 | requiresBuild: true 371 | dev: true 372 | optional: true 373 | 374 | /@rollup/rollup-linux-arm-musleabihf@4.31.0: 375 | resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==} 376 | cpu: [arm] 377 | os: [linux] 378 | requiresBuild: true 379 | dev: true 380 | optional: true 381 | 382 | /@rollup/rollup-linux-arm64-gnu@4.31.0: 383 | resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==} 384 | cpu: [arm64] 385 | os: [linux] 386 | requiresBuild: true 387 | dev: true 388 | optional: true 389 | 390 | /@rollup/rollup-linux-arm64-musl@4.31.0: 391 | resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==} 392 | cpu: [arm64] 393 | os: [linux] 394 | requiresBuild: true 395 | dev: true 396 | optional: true 397 | 398 | /@rollup/rollup-linux-loongarch64-gnu@4.31.0: 399 | resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==} 400 | cpu: [loong64] 401 | os: [linux] 402 | requiresBuild: true 403 | dev: true 404 | optional: true 405 | 406 | /@rollup/rollup-linux-powerpc64le-gnu@4.31.0: 407 | resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==} 408 | cpu: [ppc64] 409 | os: [linux] 410 | requiresBuild: true 411 | dev: true 412 | optional: true 413 | 414 | /@rollup/rollup-linux-riscv64-gnu@4.31.0: 415 | resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==} 416 | cpu: [riscv64] 417 | os: [linux] 418 | requiresBuild: true 419 | dev: true 420 | optional: true 421 | 422 | /@rollup/rollup-linux-s390x-gnu@4.31.0: 423 | resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==} 424 | cpu: [s390x] 425 | os: [linux] 426 | requiresBuild: true 427 | dev: true 428 | optional: true 429 | 430 | /@rollup/rollup-linux-x64-gnu@4.31.0: 431 | resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} 432 | cpu: [x64] 433 | os: [linux] 434 | requiresBuild: true 435 | dev: true 436 | optional: true 437 | 438 | /@rollup/rollup-linux-x64-musl@4.31.0: 439 | resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==} 440 | cpu: [x64] 441 | os: [linux] 442 | requiresBuild: true 443 | dev: true 444 | optional: true 445 | 446 | /@rollup/rollup-win32-arm64-msvc@4.31.0: 447 | resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==} 448 | cpu: [arm64] 449 | os: [win32] 450 | requiresBuild: true 451 | dev: true 452 | optional: true 453 | 454 | /@rollup/rollup-win32-ia32-msvc@4.31.0: 455 | resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==} 456 | cpu: [ia32] 457 | os: [win32] 458 | requiresBuild: true 459 | dev: true 460 | optional: true 461 | 462 | /@rollup/rollup-win32-x64-msvc@4.31.0: 463 | resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==} 464 | cpu: [x64] 465 | os: [win32] 466 | requiresBuild: true 467 | dev: true 468 | optional: true 469 | 470 | /@types/estree@1.0.6: 471 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 472 | dev: true 473 | 474 | /@types/node-fetch@2.6.12: 475 | resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} 476 | dependencies: 477 | '@types/node': 22.10.9 478 | form-data: 4.0.1 479 | dev: false 480 | 481 | /@types/node@22.10.9: 482 | resolution: {integrity: sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==} 483 | dependencies: 484 | undici-types: 6.20.0 485 | 486 | /@vitest/expect@3.0.3: 487 | resolution: {integrity: sha512-SbRCHU4qr91xguu+dH3RUdI5dC86zm8aZWydbp961aIR7G8OYNN6ZiayFuf9WAngRbFOfdrLHCGgXTj3GtoMRQ==} 488 | dependencies: 489 | '@vitest/spy': 3.0.3 490 | '@vitest/utils': 3.0.3 491 | chai: 5.1.2 492 | tinyrainbow: 2.0.0 493 | dev: true 494 | 495 | /@vitest/mocker@3.0.3(vite@6.0.11): 496 | resolution: {integrity: sha512-XT2XBc4AN9UdaxJAeIlcSZ0ILi/GzmG5G8XSly4gaiqIvPV3HMTSIDZWJVX6QRJ0PX1m+W8Cy0K9ByXNb/bPIA==} 497 | peerDependencies: 498 | msw: ^2.4.9 499 | vite: ^5.0.0 || ^6.0.0 500 | peerDependenciesMeta: 501 | msw: 502 | optional: true 503 | vite: 504 | optional: true 505 | dependencies: 506 | '@vitest/spy': 3.0.3 507 | estree-walker: 3.0.3 508 | magic-string: 0.30.17 509 | vite: 6.0.11(@types/node@22.10.9) 510 | dev: true 511 | 512 | /@vitest/pretty-format@3.0.3: 513 | resolution: {integrity: sha512-gCrM9F7STYdsDoNjGgYXKPq4SkSxwwIU5nkaQvdUxiQ0EcNlez+PdKOVIsUJvh9P9IeIFmjn4IIREWblOBpP2Q==} 514 | dependencies: 515 | tinyrainbow: 2.0.0 516 | dev: true 517 | 518 | /@vitest/runner@3.0.3: 519 | resolution: {integrity: sha512-Rgi2kOAk5ZxWZlwPguRJFOBmWs6uvvyAAR9k3MvjRvYrG7xYvKChZcmnnpJCS98311CBDMqsW9MzzRFsj2gX3g==} 520 | dependencies: 521 | '@vitest/utils': 3.0.3 522 | pathe: 2.0.2 523 | dev: true 524 | 525 | /@vitest/snapshot@3.0.3: 526 | resolution: {integrity: sha512-kNRcHlI4txBGztuJfPEJ68VezlPAXLRT1u5UCx219TU3kOG2DplNxhWLwDf2h6emwmTPogzLnGVwP6epDaJN6Q==} 527 | dependencies: 528 | '@vitest/pretty-format': 3.0.3 529 | magic-string: 0.30.17 530 | pathe: 2.0.2 531 | dev: true 532 | 533 | /@vitest/spy@3.0.3: 534 | resolution: {integrity: sha512-7/dgux8ZBbF7lEIKNnEqQlyRaER9nkAL9eTmdKJkDO3hS8p59ATGwKOCUDHcBLKr7h/oi/6hP+7djQk8049T2A==} 535 | dependencies: 536 | tinyspy: 3.0.2 537 | dev: true 538 | 539 | /@vitest/utils@3.0.3: 540 | resolution: {integrity: sha512-f+s8CvyzPtMFY1eZKkIHGhPsQgYo5qCm6O8KZoim9qm1/jT64qBgGpO5tHscNH6BzRHM+edLNOP+3vO8+8pE/A==} 541 | dependencies: 542 | '@vitest/pretty-format': 3.0.3 543 | loupe: 3.1.2 544 | tinyrainbow: 2.0.0 545 | dev: true 546 | 547 | /ansi-regex@5.0.1: 548 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 549 | engines: {node: '>=8'} 550 | dev: true 551 | 552 | /ansi-regex@6.1.0: 553 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 554 | engines: {node: '>=12'} 555 | dev: true 556 | 557 | /ansi-styles@4.3.0: 558 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 559 | engines: {node: '>=8'} 560 | dependencies: 561 | color-convert: 2.0.1 562 | dev: true 563 | 564 | /ansi-styles@6.2.1: 565 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 566 | engines: {node: '>=12'} 567 | dev: true 568 | 569 | /any-promise@1.3.0: 570 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 571 | dev: true 572 | 573 | /assertion-error@2.0.1: 574 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 575 | engines: {node: '>=12'} 576 | dev: true 577 | 578 | /asynckit@0.4.0: 579 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 580 | dev: false 581 | 582 | /balanced-match@1.0.2: 583 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 584 | dev: true 585 | 586 | /brace-expansion@2.0.1: 587 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 588 | dependencies: 589 | balanced-match: 1.0.2 590 | dev: true 591 | 592 | /bundle-require@5.1.0(esbuild@0.24.2): 593 | resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} 594 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 595 | peerDependencies: 596 | esbuild: '>=0.18' 597 | dependencies: 598 | esbuild: 0.24.2 599 | load-tsconfig: 0.2.5 600 | dev: true 601 | 602 | /cac@6.7.14: 603 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 604 | engines: {node: '>=8'} 605 | dev: true 606 | 607 | /chai@5.1.2: 608 | resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} 609 | engines: {node: '>=12'} 610 | dependencies: 611 | assertion-error: 2.0.1 612 | check-error: 2.1.1 613 | deep-eql: 5.0.2 614 | loupe: 3.1.2 615 | pathval: 2.0.0 616 | dev: true 617 | 618 | /check-error@2.1.1: 619 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 620 | engines: {node: '>= 16'} 621 | dev: true 622 | 623 | /chokidar@4.0.3: 624 | resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 625 | engines: {node: '>= 14.16.0'} 626 | dependencies: 627 | readdirp: 4.1.1 628 | dev: true 629 | 630 | /color-convert@2.0.1: 631 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 632 | engines: {node: '>=7.0.0'} 633 | dependencies: 634 | color-name: 1.1.4 635 | dev: true 636 | 637 | /color-name@1.1.4: 638 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 639 | dev: true 640 | 641 | /combined-stream@1.0.8: 642 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 643 | engines: {node: '>= 0.8'} 644 | dependencies: 645 | delayed-stream: 1.0.0 646 | dev: false 647 | 648 | /commander@4.1.1: 649 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 650 | engines: {node: '>= 6'} 651 | dev: true 652 | 653 | /consola@3.4.0: 654 | resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} 655 | engines: {node: ^14.18.0 || >=16.10.0} 656 | dev: true 657 | 658 | /cross-spawn@7.0.6: 659 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 660 | engines: {node: '>= 8'} 661 | dependencies: 662 | path-key: 3.1.1 663 | shebang-command: 2.0.0 664 | which: 2.0.2 665 | dev: true 666 | 667 | /debug@4.4.0: 668 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 669 | engines: {node: '>=6.0'} 670 | peerDependencies: 671 | supports-color: '*' 672 | peerDependenciesMeta: 673 | supports-color: 674 | optional: true 675 | dependencies: 676 | ms: 2.1.3 677 | dev: true 678 | 679 | /deep-eql@5.0.2: 680 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 681 | engines: {node: '>=6'} 682 | dev: true 683 | 684 | /delayed-stream@1.0.0: 685 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 686 | engines: {node: '>=0.4.0'} 687 | dev: false 688 | 689 | /eastasianwidth@0.2.0: 690 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 691 | dev: true 692 | 693 | /emoji-regex@8.0.0: 694 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 695 | dev: true 696 | 697 | /emoji-regex@9.2.2: 698 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 699 | dev: true 700 | 701 | /es-module-lexer@1.6.0: 702 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 703 | dev: true 704 | 705 | /esbuild@0.24.2: 706 | resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 707 | engines: {node: '>=18'} 708 | hasBin: true 709 | requiresBuild: true 710 | optionalDependencies: 711 | '@esbuild/aix-ppc64': 0.24.2 712 | '@esbuild/android-arm': 0.24.2 713 | '@esbuild/android-arm64': 0.24.2 714 | '@esbuild/android-x64': 0.24.2 715 | '@esbuild/darwin-arm64': 0.24.2 716 | '@esbuild/darwin-x64': 0.24.2 717 | '@esbuild/freebsd-arm64': 0.24.2 718 | '@esbuild/freebsd-x64': 0.24.2 719 | '@esbuild/linux-arm': 0.24.2 720 | '@esbuild/linux-arm64': 0.24.2 721 | '@esbuild/linux-ia32': 0.24.2 722 | '@esbuild/linux-loong64': 0.24.2 723 | '@esbuild/linux-mips64el': 0.24.2 724 | '@esbuild/linux-ppc64': 0.24.2 725 | '@esbuild/linux-riscv64': 0.24.2 726 | '@esbuild/linux-s390x': 0.24.2 727 | '@esbuild/linux-x64': 0.24.2 728 | '@esbuild/netbsd-arm64': 0.24.2 729 | '@esbuild/netbsd-x64': 0.24.2 730 | '@esbuild/openbsd-arm64': 0.24.2 731 | '@esbuild/openbsd-x64': 0.24.2 732 | '@esbuild/sunos-x64': 0.24.2 733 | '@esbuild/win32-arm64': 0.24.2 734 | '@esbuild/win32-ia32': 0.24.2 735 | '@esbuild/win32-x64': 0.24.2 736 | dev: true 737 | 738 | /estree-walker@3.0.3: 739 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 740 | dependencies: 741 | '@types/estree': 1.0.6 742 | dev: true 743 | 744 | /expect-type@1.1.0: 745 | resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} 746 | engines: {node: '>=12.0.0'} 747 | dev: true 748 | 749 | /fdir@6.4.3(picomatch@4.0.2): 750 | resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} 751 | peerDependencies: 752 | picomatch: ^3 || ^4 753 | peerDependenciesMeta: 754 | picomatch: 755 | optional: true 756 | dependencies: 757 | picomatch: 4.0.2 758 | dev: true 759 | 760 | /foreground-child@3.3.0: 761 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 762 | engines: {node: '>=14'} 763 | dependencies: 764 | cross-spawn: 7.0.6 765 | signal-exit: 4.1.0 766 | dev: true 767 | 768 | /form-data@4.0.1: 769 | resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} 770 | engines: {node: '>= 6'} 771 | dependencies: 772 | asynckit: 0.4.0 773 | combined-stream: 1.0.8 774 | mime-types: 2.1.35 775 | dev: false 776 | 777 | /fsevents@2.3.3: 778 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 779 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 780 | os: [darwin] 781 | requiresBuild: true 782 | dev: true 783 | optional: true 784 | 785 | /glob@10.4.5: 786 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 787 | hasBin: true 788 | dependencies: 789 | foreground-child: 3.3.0 790 | jackspeak: 3.4.3 791 | minimatch: 9.0.5 792 | minipass: 7.1.2 793 | package-json-from-dist: 1.0.1 794 | path-scurry: 1.11.1 795 | dev: true 796 | 797 | /is-fullwidth-code-point@3.0.0: 798 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 799 | engines: {node: '>=8'} 800 | dev: true 801 | 802 | /isexe@2.0.0: 803 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 804 | dev: true 805 | 806 | /jackspeak@3.4.3: 807 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 808 | dependencies: 809 | '@isaacs/cliui': 8.0.2 810 | optionalDependencies: 811 | '@pkgjs/parseargs': 0.11.0 812 | dev: true 813 | 814 | /joycon@3.1.1: 815 | resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 816 | engines: {node: '>=10'} 817 | dev: true 818 | 819 | /lilconfig@3.1.3: 820 | resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 821 | engines: {node: '>=14'} 822 | dev: true 823 | 824 | /lines-and-columns@1.2.4: 825 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 826 | dev: true 827 | 828 | /load-tsconfig@0.2.5: 829 | resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 830 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 831 | dev: true 832 | 833 | /lodash.sortby@4.7.0: 834 | resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} 835 | dev: true 836 | 837 | /loupe@3.1.2: 838 | resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} 839 | dev: true 840 | 841 | /lru-cache@10.4.3: 842 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 843 | dev: true 844 | 845 | /magic-string@0.30.17: 846 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 847 | dependencies: 848 | '@jridgewell/sourcemap-codec': 1.5.0 849 | dev: true 850 | 851 | /md-utils-ts@2.0.0: 852 | resolution: {integrity: sha512-sMG6JtX0ebcRMHxYTcmgsh0/m6o8hGdQHFE2OgjvflRZlQM51CGGj/uuk056D+12BlCiW0aTpt/AdlDNtgQiew==} 853 | dev: false 854 | 855 | /mime-db@1.52.0: 856 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 857 | engines: {node: '>= 0.6'} 858 | dev: false 859 | 860 | /mime-types@2.1.35: 861 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 862 | engines: {node: '>= 0.6'} 863 | dependencies: 864 | mime-db: 1.52.0 865 | dev: false 866 | 867 | /minimatch@9.0.5: 868 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 869 | engines: {node: '>=16 || 14 >=14.17'} 870 | dependencies: 871 | brace-expansion: 2.0.1 872 | dev: true 873 | 874 | /minipass@7.1.2: 875 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 876 | engines: {node: '>=16 || 14 >=14.17'} 877 | dev: true 878 | 879 | /ms@2.1.3: 880 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 881 | dev: true 882 | 883 | /mz@2.7.0: 884 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 885 | dependencies: 886 | any-promise: 1.3.0 887 | object-assign: 4.1.1 888 | thenify-all: 1.6.0 889 | dev: true 890 | 891 | /nanoid@3.3.8: 892 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 893 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 894 | hasBin: true 895 | dev: true 896 | 897 | /node-fetch@2.7.0: 898 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 899 | engines: {node: 4.x || >=6.0.0} 900 | peerDependencies: 901 | encoding: ^0.1.0 902 | peerDependenciesMeta: 903 | encoding: 904 | optional: true 905 | dependencies: 906 | whatwg-url: 5.0.0 907 | dev: false 908 | 909 | /object-assign@4.1.1: 910 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 911 | engines: {node: '>=0.10.0'} 912 | dev: true 913 | 914 | /package-json-from-dist@1.0.1: 915 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 916 | dev: true 917 | 918 | /path-key@3.1.1: 919 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 920 | engines: {node: '>=8'} 921 | dev: true 922 | 923 | /path-scurry@1.11.1: 924 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 925 | engines: {node: '>=16 || 14 >=14.18'} 926 | dependencies: 927 | lru-cache: 10.4.3 928 | minipass: 7.1.2 929 | dev: true 930 | 931 | /pathe@2.0.2: 932 | resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} 933 | dev: true 934 | 935 | /pathval@2.0.0: 936 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 937 | engines: {node: '>= 14.16'} 938 | dev: true 939 | 940 | /picocolors@1.1.1: 941 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 942 | dev: true 943 | 944 | /picomatch@4.0.2: 945 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 946 | engines: {node: '>=12'} 947 | dev: true 948 | 949 | /pirates@4.0.6: 950 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 951 | engines: {node: '>= 6'} 952 | dev: true 953 | 954 | /postcss-load-config@6.0.1: 955 | resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 956 | engines: {node: '>= 18'} 957 | peerDependencies: 958 | jiti: '>=1.21.0' 959 | postcss: '>=8.0.9' 960 | tsx: ^4.8.1 961 | yaml: ^2.4.2 962 | peerDependenciesMeta: 963 | jiti: 964 | optional: true 965 | postcss: 966 | optional: true 967 | tsx: 968 | optional: true 969 | yaml: 970 | optional: true 971 | dependencies: 972 | lilconfig: 3.1.3 973 | dev: true 974 | 975 | /postcss@8.5.1: 976 | resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 977 | engines: {node: ^10 || ^12 || >=14} 978 | dependencies: 979 | nanoid: 3.3.8 980 | picocolors: 1.1.1 981 | source-map-js: 1.2.1 982 | dev: true 983 | 984 | /prettier-plugin-organize-imports@4.1.0(prettier@3.4.2)(typescript@5.7.3): 985 | resolution: {integrity: sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==} 986 | peerDependencies: 987 | prettier: '>=2.0' 988 | typescript: '>=2.9' 989 | vue-tsc: ^2.1.0 990 | peerDependenciesMeta: 991 | vue-tsc: 992 | optional: true 993 | dependencies: 994 | prettier: 3.4.2 995 | typescript: 5.7.3 996 | dev: true 997 | 998 | /prettier@3.4.2: 999 | resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} 1000 | engines: {node: '>=14'} 1001 | hasBin: true 1002 | dev: true 1003 | 1004 | /punycode@2.3.1: 1005 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1006 | engines: {node: '>=6'} 1007 | dev: true 1008 | 1009 | /readdirp@4.1.1: 1010 | resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} 1011 | engines: {node: '>= 14.18.0'} 1012 | dev: true 1013 | 1014 | /resolve-from@5.0.0: 1015 | resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 1016 | engines: {node: '>=8'} 1017 | dev: true 1018 | 1019 | /rollup@4.31.0: 1020 | resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} 1021 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1022 | hasBin: true 1023 | dependencies: 1024 | '@types/estree': 1.0.6 1025 | optionalDependencies: 1026 | '@rollup/rollup-android-arm-eabi': 4.31.0 1027 | '@rollup/rollup-android-arm64': 4.31.0 1028 | '@rollup/rollup-darwin-arm64': 4.31.0 1029 | '@rollup/rollup-darwin-x64': 4.31.0 1030 | '@rollup/rollup-freebsd-arm64': 4.31.0 1031 | '@rollup/rollup-freebsd-x64': 4.31.0 1032 | '@rollup/rollup-linux-arm-gnueabihf': 4.31.0 1033 | '@rollup/rollup-linux-arm-musleabihf': 4.31.0 1034 | '@rollup/rollup-linux-arm64-gnu': 4.31.0 1035 | '@rollup/rollup-linux-arm64-musl': 4.31.0 1036 | '@rollup/rollup-linux-loongarch64-gnu': 4.31.0 1037 | '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0 1038 | '@rollup/rollup-linux-riscv64-gnu': 4.31.0 1039 | '@rollup/rollup-linux-s390x-gnu': 4.31.0 1040 | '@rollup/rollup-linux-x64-gnu': 4.31.0 1041 | '@rollup/rollup-linux-x64-musl': 4.31.0 1042 | '@rollup/rollup-win32-arm64-msvc': 4.31.0 1043 | '@rollup/rollup-win32-ia32-msvc': 4.31.0 1044 | '@rollup/rollup-win32-x64-msvc': 4.31.0 1045 | fsevents: 2.3.3 1046 | dev: true 1047 | 1048 | /shebang-command@2.0.0: 1049 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1050 | engines: {node: '>=8'} 1051 | dependencies: 1052 | shebang-regex: 3.0.0 1053 | dev: true 1054 | 1055 | /shebang-regex@3.0.0: 1056 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1057 | engines: {node: '>=8'} 1058 | dev: true 1059 | 1060 | /siginfo@2.0.0: 1061 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1062 | dev: true 1063 | 1064 | /signal-exit@4.1.0: 1065 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1066 | engines: {node: '>=14'} 1067 | dev: true 1068 | 1069 | /source-map-js@1.2.1: 1070 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1071 | engines: {node: '>=0.10.0'} 1072 | dev: true 1073 | 1074 | /source-map@0.8.0-beta.0: 1075 | resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} 1076 | engines: {node: '>= 8'} 1077 | dependencies: 1078 | whatwg-url: 7.1.0 1079 | dev: true 1080 | 1081 | /stackback@0.0.2: 1082 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1083 | dev: true 1084 | 1085 | /std-env@3.8.0: 1086 | resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} 1087 | dev: true 1088 | 1089 | /string-width@4.2.3: 1090 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1091 | engines: {node: '>=8'} 1092 | dependencies: 1093 | emoji-regex: 8.0.0 1094 | is-fullwidth-code-point: 3.0.0 1095 | strip-ansi: 6.0.1 1096 | dev: true 1097 | 1098 | /string-width@5.1.2: 1099 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1100 | engines: {node: '>=12'} 1101 | dependencies: 1102 | eastasianwidth: 0.2.0 1103 | emoji-regex: 9.2.2 1104 | strip-ansi: 7.1.0 1105 | dev: true 1106 | 1107 | /strip-ansi@6.0.1: 1108 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1109 | engines: {node: '>=8'} 1110 | dependencies: 1111 | ansi-regex: 5.0.1 1112 | dev: true 1113 | 1114 | /strip-ansi@7.1.0: 1115 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1116 | engines: {node: '>=12'} 1117 | dependencies: 1118 | ansi-regex: 6.1.0 1119 | dev: true 1120 | 1121 | /sucrase@3.35.0: 1122 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 1123 | engines: {node: '>=16 || 14 >=14.17'} 1124 | hasBin: true 1125 | dependencies: 1126 | '@jridgewell/gen-mapping': 0.3.8 1127 | commander: 4.1.1 1128 | glob: 10.4.5 1129 | lines-and-columns: 1.2.4 1130 | mz: 2.7.0 1131 | pirates: 4.0.6 1132 | ts-interface-checker: 0.1.13 1133 | dev: true 1134 | 1135 | /thenify-all@1.6.0: 1136 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 1137 | engines: {node: '>=0.8'} 1138 | dependencies: 1139 | thenify: 3.3.1 1140 | dev: true 1141 | 1142 | /thenify@3.3.1: 1143 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1144 | dependencies: 1145 | any-promise: 1.3.0 1146 | dev: true 1147 | 1148 | /tinybench@2.9.0: 1149 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1150 | dev: true 1151 | 1152 | /tinyexec@0.3.2: 1153 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1154 | dev: true 1155 | 1156 | /tinyglobby@0.2.10: 1157 | resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} 1158 | engines: {node: '>=12.0.0'} 1159 | dependencies: 1160 | fdir: 6.4.3(picomatch@4.0.2) 1161 | picomatch: 4.0.2 1162 | dev: true 1163 | 1164 | /tinypool@1.0.2: 1165 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 1166 | engines: {node: ^18.0.0 || >=20.0.0} 1167 | dev: true 1168 | 1169 | /tinyrainbow@2.0.0: 1170 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 1171 | engines: {node: '>=14.0.0'} 1172 | dev: true 1173 | 1174 | /tinyspy@3.0.2: 1175 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 1176 | engines: {node: '>=14.0.0'} 1177 | dev: true 1178 | 1179 | /tr46@0.0.3: 1180 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 1181 | dev: false 1182 | 1183 | /tr46@1.0.1: 1184 | resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 1185 | dependencies: 1186 | punycode: 2.3.1 1187 | dev: true 1188 | 1189 | /tree-kill@1.2.2: 1190 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1191 | hasBin: true 1192 | dev: true 1193 | 1194 | /ts-interface-checker@0.1.13: 1195 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1196 | dev: true 1197 | 1198 | /tsup@8.3.5(typescript@5.7.3): 1199 | resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} 1200 | engines: {node: '>=18'} 1201 | hasBin: true 1202 | peerDependencies: 1203 | '@microsoft/api-extractor': ^7.36.0 1204 | '@swc/core': ^1 1205 | postcss: ^8.4.12 1206 | typescript: '>=4.5.0' 1207 | peerDependenciesMeta: 1208 | '@microsoft/api-extractor': 1209 | optional: true 1210 | '@swc/core': 1211 | optional: true 1212 | postcss: 1213 | optional: true 1214 | typescript: 1215 | optional: true 1216 | dependencies: 1217 | bundle-require: 5.1.0(esbuild@0.24.2) 1218 | cac: 6.7.14 1219 | chokidar: 4.0.3 1220 | consola: 3.4.0 1221 | debug: 4.4.0 1222 | esbuild: 0.24.2 1223 | joycon: 3.1.1 1224 | picocolors: 1.1.1 1225 | postcss-load-config: 6.0.1 1226 | resolve-from: 5.0.0 1227 | rollup: 4.31.0 1228 | source-map: 0.8.0-beta.0 1229 | sucrase: 3.35.0 1230 | tinyexec: 0.3.2 1231 | tinyglobby: 0.2.10 1232 | tree-kill: 1.2.2 1233 | typescript: 5.7.3 1234 | transitivePeerDependencies: 1235 | - jiti 1236 | - supports-color 1237 | - tsx 1238 | - yaml 1239 | dev: true 1240 | 1241 | /typescript@5.7.3: 1242 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 1243 | engines: {node: '>=14.17'} 1244 | hasBin: true 1245 | dev: true 1246 | 1247 | /undici-types@6.20.0: 1248 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1249 | 1250 | /vite-node@3.0.3(@types/node@22.10.9): 1251 | resolution: {integrity: sha512-0sQcwhwAEw/UJGojbhOrnq3HtiZ3tC7BzpAa0lx3QaTX0S3YX70iGcik25UBdB96pmdwjyY2uyKNYruxCDmiEg==} 1252 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1253 | hasBin: true 1254 | dependencies: 1255 | cac: 6.7.14 1256 | debug: 4.4.0 1257 | es-module-lexer: 1.6.0 1258 | pathe: 2.0.2 1259 | vite: 6.0.11(@types/node@22.10.9) 1260 | transitivePeerDependencies: 1261 | - '@types/node' 1262 | - jiti 1263 | - less 1264 | - lightningcss 1265 | - sass 1266 | - sass-embedded 1267 | - stylus 1268 | - sugarss 1269 | - supports-color 1270 | - terser 1271 | - tsx 1272 | - yaml 1273 | dev: true 1274 | 1275 | /vite@6.0.11(@types/node@22.10.9): 1276 | resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} 1277 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1278 | hasBin: true 1279 | peerDependencies: 1280 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1281 | jiti: '>=1.21.0' 1282 | less: '*' 1283 | lightningcss: ^1.21.0 1284 | sass: '*' 1285 | sass-embedded: '*' 1286 | stylus: '*' 1287 | sugarss: '*' 1288 | terser: ^5.16.0 1289 | tsx: ^4.8.1 1290 | yaml: ^2.4.2 1291 | peerDependenciesMeta: 1292 | '@types/node': 1293 | optional: true 1294 | jiti: 1295 | optional: true 1296 | less: 1297 | optional: true 1298 | lightningcss: 1299 | optional: true 1300 | sass: 1301 | optional: true 1302 | sass-embedded: 1303 | optional: true 1304 | stylus: 1305 | optional: true 1306 | sugarss: 1307 | optional: true 1308 | terser: 1309 | optional: true 1310 | tsx: 1311 | optional: true 1312 | yaml: 1313 | optional: true 1314 | dependencies: 1315 | '@types/node': 22.10.9 1316 | esbuild: 0.24.2 1317 | postcss: 8.5.1 1318 | rollup: 4.31.0 1319 | optionalDependencies: 1320 | fsevents: 2.3.3 1321 | dev: true 1322 | 1323 | /vitest@3.0.3(@types/node@22.10.9): 1324 | resolution: {integrity: sha512-dWdwTFUW9rcnL0LyF2F+IfvNQWB0w9DERySCk8VMG75F8k25C7LsZoh6XfCjPvcR8Nb+Lqi9JKr6vnzH7HSrpQ==} 1325 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1326 | hasBin: true 1327 | peerDependencies: 1328 | '@edge-runtime/vm': '*' 1329 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1330 | '@vitest/browser': 3.0.3 1331 | '@vitest/ui': 3.0.3 1332 | happy-dom: '*' 1333 | jsdom: '*' 1334 | peerDependenciesMeta: 1335 | '@edge-runtime/vm': 1336 | optional: true 1337 | '@types/node': 1338 | optional: true 1339 | '@vitest/browser': 1340 | optional: true 1341 | '@vitest/ui': 1342 | optional: true 1343 | happy-dom: 1344 | optional: true 1345 | jsdom: 1346 | optional: true 1347 | dependencies: 1348 | '@types/node': 22.10.9 1349 | '@vitest/expect': 3.0.3 1350 | '@vitest/mocker': 3.0.3(vite@6.0.11) 1351 | '@vitest/pretty-format': 3.0.3 1352 | '@vitest/runner': 3.0.3 1353 | '@vitest/snapshot': 3.0.3 1354 | '@vitest/spy': 3.0.3 1355 | '@vitest/utils': 3.0.3 1356 | chai: 5.1.2 1357 | debug: 4.4.0 1358 | expect-type: 1.1.0 1359 | magic-string: 0.30.17 1360 | pathe: 2.0.2 1361 | std-env: 3.8.0 1362 | tinybench: 2.9.0 1363 | tinyexec: 0.3.2 1364 | tinypool: 1.0.2 1365 | tinyrainbow: 2.0.0 1366 | vite: 6.0.11(@types/node@22.10.9) 1367 | vite-node: 3.0.3(@types/node@22.10.9) 1368 | why-is-node-running: 2.3.0 1369 | transitivePeerDependencies: 1370 | - jiti 1371 | - less 1372 | - lightningcss 1373 | - msw 1374 | - sass 1375 | - sass-embedded 1376 | - stylus 1377 | - sugarss 1378 | - supports-color 1379 | - terser 1380 | - tsx 1381 | - yaml 1382 | dev: true 1383 | 1384 | /webidl-conversions@3.0.1: 1385 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 1386 | dev: false 1387 | 1388 | /webidl-conversions@4.0.2: 1389 | resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} 1390 | dev: true 1391 | 1392 | /whatwg-url@5.0.0: 1393 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 1394 | dependencies: 1395 | tr46: 0.0.3 1396 | webidl-conversions: 3.0.1 1397 | dev: false 1398 | 1399 | /whatwg-url@7.1.0: 1400 | resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} 1401 | dependencies: 1402 | lodash.sortby: 4.7.0 1403 | tr46: 1.0.1 1404 | webidl-conversions: 4.0.2 1405 | dev: true 1406 | 1407 | /which@2.0.2: 1408 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1409 | engines: {node: '>= 8'} 1410 | hasBin: true 1411 | dependencies: 1412 | isexe: 2.0.0 1413 | dev: true 1414 | 1415 | /why-is-node-running@2.3.0: 1416 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1417 | engines: {node: '>=8'} 1418 | hasBin: true 1419 | dependencies: 1420 | siginfo: 2.0.0 1421 | stackback: 0.0.2 1422 | dev: true 1423 | 1424 | /wrap-ansi@7.0.0: 1425 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1426 | engines: {node: '>=10'} 1427 | dependencies: 1428 | ansi-styles: 4.3.0 1429 | string-width: 4.2.3 1430 | strip-ansi: 6.0.1 1431 | dev: true 1432 | 1433 | /wrap-ansi@8.1.0: 1434 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1435 | engines: {node: '>=12'} 1436 | dependencies: 1437 | ansi-styles: 6.2.1 1438 | string-width: 5.1.2 1439 | strip-ansi: 7.1.0 1440 | dev: true 1441 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: ["prettier-plugin-organize-imports"], 3 | }; 4 | -------------------------------------------------------------------------------- /src/clients.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIErrorCode, 3 | Client, 4 | collectPaginatedAPI, 5 | isNotionClientError, 6 | } from "@notionhq/client"; 7 | import { wait } from "./libs.js"; 8 | 9 | const isRateLimitError = (error: unknown) => 10 | isNotionClientError(error) && error.code === APIErrorCode.RateLimited; 11 | 12 | /** 13 | * Executes a function with exponential backoff on rate limit error. 14 | * @param fn - The function to execute. 15 | * @param retries - The number of retries before giving up. Default is 5. 16 | * @param delay - The delay in milliseconds before each retry. Default is 1000ms. 17 | * @returns A promise that resolves to the result of the function. 18 | * @throws If the function throws an error other than rate limit error. 19 | */ 20 | const backoffOnRateLimit = async ( 21 | fn: () => Promise, 22 | retries: number = 5, 23 | delay: number = 1000, 24 | ): Promise => { 25 | try { 26 | return await fn(); 27 | } catch (error) { 28 | if (isRateLimitError(error)) { 29 | if (retries === 0) throw error; 30 | console.log( 31 | `Rate limited. Retries left: ${retries}. Waiting ${delay}ms before retrying...`, 32 | ); 33 | await wait(delay); 34 | return backoffOnRateLimit(fn, retries - 1, delay * 2); 35 | } else { 36 | throw error; 37 | } 38 | } 39 | }; 40 | 41 | /** 42 | * Fetches Notion blocks for a given block ID. 43 | * 44 | * @param client - The Notion client. 45 | * @returns A function that takes a block ID and returns a promise that resolves to an array of Notion blocks. 46 | */ 47 | export const fetchNotionBlocks = (client: Client) => async (blockId: string) => 48 | backoffOnRateLimit(() => 49 | collectPaginatedAPI(client.blocks.children.list, { 50 | block_id: blockId, 51 | }), 52 | ).catch(() => []); 53 | 54 | /** 55 | * Fetches a Notion page using the provided client and page ID. 56 | * @param client The Notion client. 57 | * @returns A function that takes a page ID and returns a Promise that resolves to the retrieved page. 58 | */ 59 | export const fetchNotionPage = (client: Client) => (pageId: string) => 60 | backoffOnRateLimit(() => client.pages.retrieve({ page_id: pageId })); 61 | 62 | /** 63 | * Fetches the Notion database with the specified database ID. 64 | * 65 | * @param client - The Notion client. 66 | * @returns A function that takes a database ID and returns a promise that resolves to an array of database results. 67 | */ 68 | export const fetchNotionDatabase = (client: Client) => (databaseId: string) => 69 | backoffOnRateLimit(() => 70 | client.databases 71 | .query({ database_id: databaseId }) 72 | .then(({ results }) => results), 73 | ).catch(() => []); 74 | -------------------------------------------------------------------------------- /src/crawler.ts: -------------------------------------------------------------------------------- 1 | import { indent as _indent } from "md-utils-ts"; 2 | import { 3 | fetchNotionBlocks, 4 | fetchNotionDatabase, 5 | fetchNotionPage, 6 | } from "./clients.js"; 7 | import { has } from "./libs.js"; 8 | import { serializer } from "./serializer/index.js"; 9 | import { propertiesSerializer } from "./serializer/property/index.js"; 10 | import { 11 | CrawlerOptions, 12 | CrawlingResult, 13 | Dictionary, 14 | Metadata, 15 | MetadataBuilder, 16 | NotionBlock, 17 | NotionBlockObjectResponse, 18 | NotionChildPage, 19 | NotionPage, 20 | NotionProperties, 21 | Page, 22 | } from "./types.js"; 23 | 24 | const blockIs = ( 25 | block: NotionBlock, 26 | types: T, 27 | ): block is Extract => 28 | types.includes(block.type); 29 | 30 | const shouldSkipPage = (currentPageId: string, skipPageIds?: string[]) => 31 | skipPageIds && skipPageIds.includes(currentPageId); 32 | 33 | const pageInit = 34 | (metadataBuilder?: MetadataBuilder) => 35 | async ( 36 | page: NotionPage | NotionBlock, 37 | title: string, 38 | parent?: Page, 39 | properties?: string[], 40 | ): Promise> => { 41 | const metadata: Metadata = { 42 | id: page.id, 43 | title, 44 | createdTime: page.created_time, 45 | lastEditedTime: page.last_edited_time, 46 | parentId: parent?.metadata.id, 47 | }; 48 | 49 | const userMetadata = metadataBuilder 50 | ? await metadataBuilder({ page, title, properties, parent }) 51 | : ({} as T); 52 | 53 | return { 54 | metadata: { ...metadata, ...userMetadata }, 55 | properties: properties || [], 56 | lines: [], 57 | }; 58 | }; 59 | 60 | /** 61 | * List of block types that do not need to be nested. 62 | * Avoid nesting when serializing due to the Notion Block structure. 63 | */ 64 | const IGNORE_NEST_LIST = ["table", "table_row", "column_list", "column"]; 65 | 66 | const indent = _indent(); 67 | 68 | const getBlockSerializer = ( 69 | type: NotionBlock["type"], 70 | { urlMask = false, serializers }: CrawlerOptions, 71 | ) => 72 | ({ 73 | ...serializer.block.strategy({ urlMask }), 74 | ...serializers?.block, 75 | })[type]; 76 | 77 | const isPage = (block: NotionBlock): block is NotionChildPage => 78 | blockIs(block, ["child_page", "child_database"]); 79 | 80 | const getSuccessResult = ( 81 | page: Page, 82 | ): CrawlingResult => ({ 83 | id: page.metadata.id, 84 | success: true, 85 | page, 86 | }); 87 | 88 | const getFailedResult = ( 89 | page: Page, 90 | err: unknown, 91 | ): CrawlingResult => ({ 92 | id: page.metadata.id, 93 | success: false, 94 | failure: { 95 | parentId: page.metadata.parentId, 96 | reason: 97 | err instanceof Error 98 | ? `${err.name}: ${err.message}\n${err.stack}` 99 | : `${err}`, 100 | }, 101 | }); 102 | 103 | const readLines = 104 | (options: CrawlerOptions) => 105 | async (blocks: NotionBlockObjectResponse[], depth = 0) => { 106 | let lines: string[] = []; 107 | let pages: NotionChildPage[] = []; 108 | 109 | for (const block of blocks) { 110 | if (!has(block, "type")) continue; 111 | const { type } = block; 112 | const serialize = getBlockSerializer(type, options); 113 | const text = await serialize(block as any); 114 | 115 | if (text !== false) { 116 | const line = indent(text, depth); 117 | lines.push(line); 118 | } 119 | 120 | if (isPage(block)) { 121 | pages.push(block); 122 | 123 | continue; 124 | } 125 | 126 | if (blockIs(block, ["synced_block"])) { 127 | // Specify the sync destination block id 128 | const blockId = block.synced_block.synced_from?.block_id || block.id; 129 | const _blocks = await fetchNotionBlocks(options.client)(blockId); 130 | const result = await readLines(options)(_blocks, depth); 131 | 132 | lines = [...lines, ...result.lines]; 133 | pages = [...pages, ...result.pages]; 134 | 135 | continue; 136 | } 137 | 138 | if (block.has_children) { 139 | const _blocks = await fetchNotionBlocks(options.client)(block.id); 140 | const _depth = IGNORE_NEST_LIST.includes(type) ? depth : depth + 1; 141 | const result = await readLines(options)(_blocks, _depth); 142 | 143 | lines = [...lines, ...result.lines]; 144 | pages = [...pages, ...result.pages]; 145 | } 146 | } 147 | 148 | return { lines, pages }; 149 | }; 150 | 151 | const walking = (options: CrawlerOptions) => 152 | async function* ( 153 | parent: Page, 154 | blocks: NotionBlockObjectResponse[], 155 | depth = 0, 156 | ): AsyncGenerator> { 157 | try { 158 | const { client, metadataBuilder } = options; 159 | const initPage = pageInit(metadataBuilder); 160 | 161 | const { lines, pages } = await readLines(options)(blocks, depth); 162 | yield getSuccessResult({ ...parent, lines }); 163 | 164 | for (const page of pages) { 165 | if (shouldSkipPage(page.id, options.skipPageIds)) continue; 166 | 167 | if (blockIs(page, ["child_page"])) { 168 | const { title } = page.child_page; 169 | const _parent = await initPage(page, title, parent); 170 | const _blocks = await fetchNotionBlocks(client)(page.id); 171 | 172 | yield* walking(options)(_parent, _blocks, 0); 173 | 174 | continue; 175 | } 176 | 177 | if (blockIs(page, ["child_database"])) { 178 | const { title } = page.child_database; 179 | const _parent = await initPage(page, title, parent); 180 | const _options = { ...options, parent: _parent }; 181 | 182 | yield* dbCrawler(_options)(page.id); 183 | } 184 | } 185 | } catch (err) { 186 | yield getFailedResult(parent, err); 187 | } 188 | }; 189 | 190 | const serializeProperties = ( 191 | properties: NotionProperties, 192 | options: CrawlerOptions, 193 | ) => { 194 | const { urlMask = false, serializers } = options; 195 | const _serializers = { 196 | ...serializer.property.strategy({ urlMask }), 197 | ...serializers?.property, 198 | }; 199 | 200 | return propertiesSerializer(_serializers)(properties); 201 | }; 202 | 203 | const extractPageTitle = (page: NotionPage) => { 204 | if (!has(page, "properties")) return ""; 205 | 206 | let title = ""; 207 | 208 | for (const prop of Object.values(page.properties)) { 209 | if (prop.type !== "title") continue; 210 | 211 | const text = serializer.property.defaults.title("", prop) as string; 212 | title = text.replace("[] ", ""); 213 | } 214 | 215 | return title; 216 | }; 217 | 218 | /** 219 | * `crawler` is a higher-order function that returns a function designed to crawl through Notion pages. 220 | * It utilizes given client, optional serializers, and an optional parentId to customize its operation. 221 | * 222 | * @param {CrawlerOptions} options - The crawler options which contains: 223 | * - client: An instance of the Notion client. 224 | * - serializers?: An optional object that can be used to define custom serializers for blocks and properties. 225 | * - urlMask?: If specified, the url is masked with the string. 226 | * 227 | * @returns {Function} A generator function that takes a `rootPageId` (the ID of the starting Notion page) and yields a Promise that resolves to the crawled pages or an error object. 228 | * 229 | * @example 230 | * // Initialize the crawler with options. 231 | * const crawl = crawler({ client: myClient }); 232 | * 233 | * // Use the initialized crawler 234 | * for await (const result of crawl("someRootPageId")) { 235 | * if (result.success) { 236 | * console.log("Crawled page:", result.page); 237 | * } else { 238 | * console.error("Crawling failed:", result.failure); 239 | * } 240 | * } 241 | */ 242 | export const crawler = (options: CrawlerOptions) => 243 | async function* (rootPageId: string): AsyncGenerator> { 244 | const { client, parent, metadataBuilder, skipPageIds } = options; 245 | if (shouldSkipPage(rootPageId, skipPageIds)) return; 246 | 247 | try { 248 | const notionPage = await fetchNotionPage(client)(rootPageId); 249 | 250 | if (!has(notionPage, "parent")) { 251 | const reason = "Unintended Notion Page object."; 252 | 253 | return yield { 254 | id: rootPageId, 255 | success: false, 256 | failure: { parentId: parent?.metadata.id, reason }, 257 | }; 258 | } 259 | 260 | // Preparation Before Exploring 261 | const props = await serializeProperties(notionPage.properties, options); 262 | const blocks = await fetchNotionBlocks(client)(notionPage.id); 263 | const title = extractPageTitle(notionPage); 264 | const initPage = pageInit(metadataBuilder); 265 | const rootPage = await initPage(notionPage, title, parent, props); 266 | 267 | yield* walking(options)(rootPage, blocks); 268 | } catch { 269 | // Try as DB Page may have been passed. 270 | yield* dbCrawler(options)(rootPageId); 271 | } 272 | }; 273 | 274 | /** 275 | * `dbCrawler` is specifically designed to crawl Notion databases. This function retrieves all records in a database and then 276 | * utilizes the `crawler` function for each individual record. 277 | * 278 | * Note: When working with a root page that is a database, use `dbCrawler` instead of the regular `crawler`. 279 | * 280 | * @param {CrawlerOptions} options - The options necessary for the crawl operation, which includes: 281 | * - client: The Notion client used for making requests. 282 | * - serializers: Optional serializers for block and property. 283 | * - urlMask?: If specified, the url is masked with the string. 284 | * 285 | * @returns {Function} A function that takes a `databaseId` and returns a promise that resolves to a `Pages` object, which is a collection of 286 | * all the pages found within the specified Notion database. 287 | */ 288 | export const dbCrawler = (options: CrawlerOptions) => 289 | async function* (rootDatabaseId: string): AsyncGenerator> { 290 | const { skipPageIds } = options; 291 | if (shouldSkipPage(rootDatabaseId, skipPageIds)) return; 292 | 293 | const crawl = crawler(options); 294 | const records = await fetchNotionDatabase(options.client)(rootDatabaseId); 295 | 296 | const { parent } = options; 297 | if (parent) { 298 | yield getSuccessResult(parent); 299 | } 300 | 301 | for (const record of records) { 302 | yield* crawl(record.id); 303 | } 304 | }; 305 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./crawler.js"; 2 | export * from "./serializer/index.js"; 3 | export * from "./types.js"; 4 | export * from "./utils.js"; 5 | -------------------------------------------------------------------------------- /src/libs.ts: -------------------------------------------------------------------------------- 1 | export const has = ( 2 | obj: T, 3 | key: K, 4 | ): obj is Extract => key in obj; 5 | 6 | export const wait = (ms: number) => 7 | new Promise((resolve) => setTimeout(resolve, ms)); 8 | -------------------------------------------------------------------------------- /src/notion.types.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@notionhq/client"; 2 | 3 | export type PromiseResult any> = Awaited< 4 | ReturnType 5 | >; 6 | 7 | export type NotionClient = InstanceType; 8 | 9 | type NotionBlockListMethod = NotionClient["blocks"]["children"]["list"]; 10 | type NotionBlockListResponse = PromiseResult; 11 | export type NotionBlockObjectResponse = 12 | NotionBlockListResponse["results"][number]; 13 | 14 | type ExtractBlockObjectResponse = T extends { type: string } ? T : never; 15 | export type NotionBlock = ExtractBlockObjectResponse; 16 | export type ExtractBlock = Extract< 17 | NotionBlock, 18 | { type: T } 19 | >; 20 | 21 | type NotionPageRetrieveMethod = NotionClient["pages"]["retrieve"]; 22 | type NotionPageResponse = PromiseResult; 23 | export type NotionPage = Extract; 24 | export type NotionProperties = NotionPage["properties"]; 25 | export type NotionProperty = NotionProperties[string]; 26 | export type ExtractProperty = Extract< 27 | NotionProperty, 28 | { type: T } 29 | >; 30 | 31 | export type NotionChildPage = 32 | | ExtractBlock<"child_page"> 33 | | ExtractBlock<"child_database">; 34 | -------------------------------------------------------------------------------- /src/serializer/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | annotate, 4 | fromDate, 5 | fromLink, 6 | fromRichText, 7 | fromUser, 8 | } from "../utils.js"; 9 | 10 | describe("annotate", () => { 11 | it.each` 12 | text | annotations | expected 13 | ${"base"} | ${[false, false, false, false, false]} | ${"base"} 14 | ${"base"} | ${[true, false, false, false, false]} | ${"`base`"} 15 | ${"base"} | ${[false, true, false, false, false]} | ${"**base**"} 16 | ${"base"} | ${[false, false, true, false, false]} | ${"_base_"} 17 | ${"base"} | ${[false, false, false, true, false]} | ${"~~base~~"} 18 | ${"base"} | ${[false, false, false, false, true]} | ${"base"} 19 | ${"base"} | ${[true, true, false, false, false]} | ${"**`base`**"} 20 | ${"base"} | ${[true, false, true, false, false]} | ${"_`base`_"} 21 | ${"base"} | ${[true, false, false, true, false]} | ${"~~`base`~~"} 22 | ${"base"} | ${[true, false, false, false, true]} | ${"`base`"} 23 | ${"base"} | ${[false, true, true, false, false]} | ${"_**base**_"} 24 | ${"base"} | ${[false, true, false, true, false]} | ${"~~**base**~~"} 25 | ${"base"} | ${[false, true, false, false, true]} | ${"**base**"} 26 | ${"base"} | ${[false, false, true, true, false]} | ${"~~_base_~~"} 27 | ${"base"} | ${[false, false, true, false, true]} | ${"_base_"} 28 | ${"base"} | ${[false, false, false, true, true]} | ${"~~base~~"} 29 | ${"base"} | ${[true, true, true, false, false]} | ${"_**`base`**_"} 30 | ${"base"} | ${[true, true, false, true, false]} | ${"~~**`base`**~~"} 31 | ${"base"} | ${[true, true, false, false, true]} | ${"**`base`**"} 32 | ${"base"} | ${[true, false, true, true, false]} | ${"~~_`base`_~~"} 33 | ${"base"} | ${[true, false, true, false, true]} | ${"_`base`_"} 34 | ${"base"} | ${[true, false, false, true, true]} | ${"~~`base`~~"} 35 | ${"base"} | ${[false, true, true, true, false]} | ${"~~_**base**_~~"} 36 | ${"base"} | ${[false, true, true, false, true]} | ${"_**base**_"} 37 | ${"base"} | ${[false, true, false, true, true]} | ${"~~**base**~~"} 38 | ${"base"} | ${[false, false, true, true, true]} | ${"~~_base_~~"} 39 | ${"base"} | ${[true, true, true, true, false]} | ${"~~_**`base`**_~~"} 40 | ${"base"} | ${[true, true, true, false, true]} | ${"_**`base`**_"} 41 | ${"base"} | ${[true, true, false, true, true]} | ${"~~**`base`**~~"} 42 | ${"base"} | ${[true, false, true, true, true]} | ${"~~_`base`_~~"} 43 | ${"base"} | ${[false, true, true, true, true]} | ${"~~_**base**_~~"} 44 | ${"base"} | ${[true, true, true, true, true]} | ${"~~_**`base`**_~~"} 45 | `("returns $expected", ({ text, annotations, expected }) => { 46 | const [code, bold, italic, strikethrough, underline] = annotations; 47 | const _annotations = { code, bold, italic, strikethrough, underline }; 48 | expect(annotate(text, _annotations as any)).toBe(expected); 49 | }); 50 | }); 51 | 52 | describe("fromRichText", () => { 53 | it("should return an empty string for an empty richTextObject", () => { 54 | expect(fromRichText([])).toBe(""); 55 | }); 56 | 57 | it("should return the whitespace for plain_text consisting only of whitespace", () => { 58 | const input = [{ plain_text: " ", annotations: {}, href: undefined }]; 59 | expect(fromRichText(input as any)).toBe(" "); 60 | }); 61 | 62 | it("should return the plain_text unmodified when no annotations are provided", () => { 63 | const input = [ 64 | { plain_text: "Hello, World!", annotations: {}, href: undefined }, 65 | ]; 66 | expect(fromRichText(input as any)).toBe("Hello, World!"); 67 | }); 68 | 69 | it("should apply bold annotation correctly", () => { 70 | const input = [ 71 | { 72 | plain_text: "Hello, World!", 73 | annotations: { bold: true }, 74 | href: undefined, 75 | }, 76 | ]; 77 | expect(fromRichText(input as any)).toBe("**Hello, World!**"); 78 | }); 79 | 80 | it("should preserve leading and trailing whitespace", () => { 81 | const input = [ 82 | { plain_text: " Hello, World! ", annotations: {}, href: undefined }, 83 | ]; 84 | expect(fromRichText(input as any)).toBe(" Hello, World! "); 85 | }); 86 | 87 | it("should return only the whitespace if plain_text is whitespace even with annotations/href", () => { 88 | const input = [ 89 | { 90 | plain_text: " ", 91 | annotations: { bold: true }, 92 | href: "https://example.com", 93 | }, 94 | ]; 95 | expect(fromRichText(input as any)).toBe(" "); 96 | }); 97 | 98 | it("should embed links correctly when href is provided", () => { 99 | const input = [ 100 | { 101 | plain_text: "Hello, World!", 102 | annotations: {}, 103 | href: "https://example.com", 104 | }, 105 | ]; 106 | expect(fromRichText(input as any)).toBe( 107 | "[Hello, World!](https://example.com)", 108 | ); 109 | }); 110 | 111 | it("should process and concatenate multiple richTextObject entries correctly", () => { 112 | const input = [ 113 | { plain_text: "Hello,", annotations: { bold: true }, href: undefined }, 114 | { 115 | plain_text: " World!", 116 | annotations: { italic: true }, 117 | href: "https://example.com", 118 | }, 119 | ]; 120 | expect(fromRichText(input as any)).toBe( 121 | "**Hello,** [_World!_](https://example.com)", 122 | ); 123 | }); 124 | }); 125 | 126 | describe("fromLink", () => { 127 | it("should return an object with title and href when a valid NotionLinkObject is provided", () => { 128 | const linkObject = { 129 | type: "external", 130 | caption: [{ type: "text", annotations: {}, plain_text: "Notion" }], 131 | external: { url: "https://www.notion.so" }, 132 | }; 133 | const result = fromLink(linkObject as any); 134 | expect(result).toEqual({ title: "Notion", href: "https://www.notion.so" }); 135 | }); 136 | 137 | it("should get href from external.url when type is external", () => { 138 | const linkObject = { 139 | type: "external", 140 | caption: [], 141 | external: { url: "https://www.external.so" }, 142 | }; 143 | const result = fromLink(linkObject as any); 144 | expect(result.href).toBe("https://www.external.so"); 145 | }); 146 | 147 | it("should get href from file.url when type is file", () => { 148 | const linkObject = { 149 | type: "file", 150 | caption: [], 151 | file: { url: "https://www.file.so/file.pdf" }, 152 | }; 153 | const result = fromLink(linkObject as any); 154 | expect(result.href).toBe("https://www.file.so/file.pdf"); 155 | }); 156 | 157 | it("should set title from caption when caption is present", () => { 158 | const linkObject = { 159 | type: "external", 160 | caption: [{ type: "text", annotations: {}, plain_text: "Caption" }], 161 | external: { url: "https://www.caption.so" }, 162 | }; 163 | const result = fromLink(linkObject as any); 164 | expect(result.title).toBe("Caption"); 165 | }); 166 | 167 | it("should set title from fileName when caption is absent and fileName is available", () => { 168 | const linkObject = { 169 | type: "file", 170 | caption: [], 171 | file: { url: "https://www.file.so/file.pdf" }, 172 | }; 173 | const result = fromLink(linkObject as any); 174 | expect(result.title).toBe("file.pdf"); 175 | }); 176 | 177 | it('should set title to default "link" when both caption and fileName are absent', () => { 178 | const linkObject = { 179 | type: "external", 180 | caption: [], 181 | external: { url: "https://www.nocaptionnofile.so" }, 182 | }; 183 | const result = fromLink(linkObject as any); 184 | expect(result.title).toBe("link"); 185 | }); 186 | }); 187 | 188 | describe("fromUser", () => { 189 | it("should return '' when the function is given no user", () => { 190 | expect(fromUser({} as any)).toBe(""); 191 | }); 192 | 193 | it("should return the name as is when given a person user", () => { 194 | const user = { name: "John", type: "person" }; 195 | expect(fromUser(user as any)).toBe("John"); 196 | }); 197 | 198 | it("should return the name with '[bot]' when given a bot user", () => { 199 | const user = { name: "BotName", type: "bot" }; 200 | expect(fromUser(user as any)).toBe("BotName[bot]"); 201 | }); 202 | 203 | it("should return '' when the name property does not exist", () => { 204 | const user = { type: "person" }; 205 | expect(fromUser(user as any)).toBe(""); 206 | }); 207 | 208 | it("should return '' when the type property does not exist", () => { 209 | const user = { name: "John" }; 210 | expect(fromUser(user as any)).toBe(""); 211 | }); 212 | }); 213 | 214 | describe("fromDate", () => { 215 | it('should return "" when date is null', () => { 216 | const date = null; 217 | expect(fromDate(date as any)).toBe(""); 218 | }); 219 | 220 | it("should return only the start date when the date object has no end date", () => { 221 | const date = { start: "2023-09-01" }; 222 | expect(fromDate(date as any)).toBe("2023-09-01"); 223 | }); 224 | 225 | it("should return both start and end dates when the date object has both", () => { 226 | const date = { start: "2023-09-01", end: "2023-09-05" }; 227 | expect(fromDate(date as any)).toBe(`(start)2023-09-01, (end): 2023-09-05`); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /src/serializer/block/__tests__/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { fromLink, fromRichText } from "../../utils.js"; 3 | import { 4 | audio as _audio, 5 | bookmark as _bookmark, 6 | bulletedListItem as _bulletedListItem, 7 | } from "../defaults.js"; 8 | 9 | describe("audio", () => { 10 | const audio = _audio({ urlMask: false }); 11 | 12 | it("should return a markdown anchor tag when a valid block object is provided", () => { 13 | const block = { 14 | audio: { 15 | caption: [{ type: "text", annotations: {}, plain_text: "Audio Title" }], 16 | type: "external", 17 | external: { url: "https://www.audio.so" }, 18 | }, 19 | }; 20 | const result = audio(block as any); 21 | const expected = `[${fromLink(block.audio as any).title}](${ 22 | fromLink(block.audio as any).href 23 | })`; 24 | expect(result).toBe(expected); 25 | }); 26 | 27 | it("should set title and href correctly in the markdown anchor tag", () => { 28 | const block = { 29 | audio: { 30 | caption: [], 31 | type: "file", 32 | file: { url: "https://www.audio.so/audio.mp3" }, 33 | }, 34 | }; 35 | const result = audio(block as any); 36 | const { title, href } = fromLink(block.audio as any); 37 | const expected = `[${title}](${href})`; 38 | expect(result).toBe(expected); 39 | }); 40 | }); 41 | 42 | describe("bookmark", () => { 43 | const bookmark = _bookmark({ urlMask: false }); 44 | 45 | it("should correctly serialize a block with both bookmark caption and url", () => { 46 | const block = { 47 | bookmark: { 48 | caption: [{ plain_text: "Google", annotations: {}, href: undefined }], 49 | url: "https://www.google.com", 50 | }, 51 | }; 52 | expect(bookmark(block as any)).toBe( 53 | `[${fromRichText( 54 | block.bookmark.caption as any, 55 | )}](https://www.google.com)`, 56 | ); 57 | }); 58 | 59 | it("should correctly serialize a block with no caption but with url", () => { 60 | const block = { 61 | bookmark: { 62 | caption: [], 63 | url: "https://www.google.com", 64 | }, 65 | }; 66 | expect(bookmark(block as any)).toBe(`[](https://www.google.com)`); 67 | }); 68 | 69 | it("should correctly serialize a block with a caption but no url", () => { 70 | const block = { 71 | bookmark: { 72 | caption: [{ plain_text: "Google", annotations: {}, href: undefined }], 73 | url: "", 74 | }, 75 | }; 76 | expect(bookmark(block as any)).toBe(`[Google]()`); 77 | }); 78 | 79 | it("should correctly serialize a block with neither caption nor url", () => { 80 | const block = { 81 | bookmark: { 82 | caption: [], 83 | url: "", 84 | }, 85 | }; 86 | expect(bookmark(block as any)).toBe(`[]()`); 87 | }); 88 | 89 | it("should correctly serialize a block with annotated caption and url", () => { 90 | const block = { 91 | bookmark: { 92 | caption: [ 93 | { 94 | plain_text: "Google", 95 | annotations: { bold: true }, 96 | href: undefined, 97 | }, 98 | ], 99 | url: "https://www.google.com", 100 | }, 101 | }; 102 | expect(bookmark(block as any)).toBe(`[**Google**](https://www.google.com)`); 103 | }); 104 | }); 105 | 106 | describe("bulletedListItem", () => { 107 | const bulletedListItem = _bulletedListItem({ urlMask: false }); 108 | 109 | it("should prefix the rich text with a bullet symbol", () => { 110 | const block = { 111 | bulleted_list_item: { 112 | rich_text: [ 113 | { plain_text: "Hello, World!", annotations: {}, href: undefined }, 114 | ], 115 | }, 116 | }; 117 | expect(bulletedListItem(block as any)).toBe("- Hello, World!"); // Assuming that md.bullet uses '-' for bullets 118 | }); 119 | 120 | it("should handle empty list items", () => { 121 | const block = { bulleted_list_item: { rich_text: [] } }; 122 | expect(bulletedListItem(block as any)).toBe("- "); // Assuming that an empty bullet should still be output 123 | }); 124 | 125 | it("should preserve formatting and annotations of the text", () => { 126 | const block = { 127 | bulleted_list_item: { 128 | rich_text: [ 129 | { 130 | plain_text: "Hello, World!", 131 | annotations: { bold: true }, 132 | href: "https://example.com", 133 | }, 134 | ], 135 | }, 136 | }; 137 | expect(bulletedListItem(block as any)).toBe( 138 | "- [**Hello, World!**](https://example.com)", 139 | ); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/serializer/block/defaults.ts: -------------------------------------------------------------------------------- 1 | import * as md from "md-utils-ts"; 2 | import { fromLink, fromRichText } from "../utils.js"; 3 | import { FactoryOptions, SerializerFactory } from "./types.js"; 4 | 5 | type Audio = SerializerFactory<"audio">; 6 | export const audio: Audio = 7 | ({ urlMask }) => 8 | (block) => { 9 | const { title, href } = fromLink(block.audio); 10 | return md.anchor(title, urlMask || href); 11 | }; 12 | 13 | type Bookmark = SerializerFactory<"bookmark">; 14 | export const bookmark: Bookmark = 15 | ({ urlMask }) => 16 | (block) => 17 | md.anchor( 18 | fromRichText(block.bookmark.caption), 19 | urlMask || block.bookmark.url, 20 | ); 21 | 22 | type Breadcrumb = SerializerFactory<"breadcrumb">; 23 | export const breadcrumb: Breadcrumb = () => () => false; 24 | 25 | type BulletedListItem = SerializerFactory<"bulleted_list_item">; 26 | export const bulletedListItem: BulletedListItem = 27 | ({ urlMask }) => 28 | (block) => 29 | md.bullet(fromRichText(block.bulleted_list_item.rich_text, urlMask)); 30 | 31 | type Callout = SerializerFactory<"callout">; 32 | export const callout: Callout = 33 | ({ urlMask }) => 34 | (block) => 35 | md.quote(fromRichText(block.callout.rich_text, urlMask)); 36 | 37 | type ChildPage = SerializerFactory<"child_page">; 38 | export const childPage: ChildPage = () => (block) => 39 | `[${block.child_page.title}]`; 40 | 41 | type ChildDatabase = SerializerFactory<"child_database">; 42 | export const childDatabase: ChildDatabase = () => (block) => 43 | `[${block.child_database.title}]`; 44 | 45 | type Code = SerializerFactory<"code">; 46 | export const code: Code = 47 | ({ urlMask }) => 48 | (block) => 49 | md.codeBlock(block.code.language)( 50 | fromRichText(block.code.rich_text, urlMask), 51 | ); 52 | 53 | type Column = SerializerFactory<"column">; 54 | export const column: Column = () => () => false; 55 | 56 | type ColumnList = SerializerFactory<"column_list">; 57 | export const columnList: ColumnList = () => () => false; 58 | 59 | type Divider = SerializerFactory<"divider">; 60 | export const divider: Divider = () => () => md.hr(); 61 | 62 | type Embed = SerializerFactory<"embed">; 63 | export const embed: Embed = 64 | ({ urlMask }) => 65 | (block) => { 66 | const caption = fromRichText(block.embed.caption, urlMask); 67 | return md.anchor(caption, urlMask || block.embed.url); 68 | }; 69 | 70 | type Equation = SerializerFactory<"equation">; 71 | export const equation: Equation = () => (block) => 72 | md.equationBlock(block.equation.expression); 73 | 74 | type File = SerializerFactory<"file">; 75 | export const file: File = 76 | ({ urlMask }) => 77 | (block) => { 78 | const { title, href } = fromLink(block.file); 79 | return md.anchor(title, urlMask || href); 80 | }; 81 | 82 | type Heading1 = SerializerFactory<"heading_1">; 83 | export const heading1: Heading1 = 84 | ({ urlMask }) => 85 | (block) => 86 | md.h1(fromRichText(block.heading_1.rich_text, urlMask)); 87 | 88 | type Heading2 = SerializerFactory<"heading_2">; 89 | export const heading2: Heading2 = 90 | ({ urlMask }) => 91 | (block) => 92 | md.h2(fromRichText(block.heading_2.rich_text, urlMask)); 93 | 94 | type Heading3 = SerializerFactory<"heading_3">; 95 | export const heading3: Heading3 = 96 | ({ urlMask }) => 97 | (block) => 98 | md.h3(fromRichText(block.heading_3.rich_text, urlMask)); 99 | 100 | type Image = SerializerFactory<"image">; 101 | export const image: Image = 102 | ({ urlMask }) => 103 | (block) => { 104 | const { title, href } = fromLink(block.image); 105 | return md.image(title, urlMask || href); 106 | }; 107 | 108 | type LinkPreview = SerializerFactory<"link_preview">; 109 | export const linkPreview: LinkPreview = 110 | ({ urlMask }) => 111 | (block) => 112 | md.anchor(block.type, urlMask || block.link_preview.url); 113 | 114 | type LinkToPage = SerializerFactory<"link_to_page">; 115 | export const linkToPage: LinkToPage = 116 | ({ urlMask }) => 117 | (block) => { 118 | const href = 119 | block.link_to_page.type === "page_id" ? block.link_to_page.page_id : ""; 120 | return md.anchor(block.type, urlMask || href); 121 | }; 122 | 123 | type NumberedListItem = SerializerFactory<"numbered_list_item">; 124 | export const numberedListItem: NumberedListItem = 125 | ({ urlMask }) => 126 | (block) => 127 | md.bullet(fromRichText(block.numbered_list_item.rich_text, urlMask), 1); 128 | 129 | type Paragraph = SerializerFactory<"paragraph">; 130 | export const paragraph: Paragraph = 131 | ({ urlMask }) => 132 | (block) => 133 | fromRichText(block.paragraph.rich_text, urlMask); 134 | 135 | type PDF = SerializerFactory<"pdf">; 136 | export const pdf: PDF = 137 | ({ urlMask }) => 138 | (block) => { 139 | const { title, href } = fromLink(block.pdf); 140 | return md.anchor(title, urlMask || href); 141 | }; 142 | 143 | type Quote = SerializerFactory<"quote">; 144 | export const quote: Quote = 145 | ({ urlMask }) => 146 | (block) => 147 | md.quote(fromRichText(block.quote.rich_text, urlMask)); 148 | 149 | type SyncedBlock = SerializerFactory<"synced_block">; 150 | export const syncedBlock: SyncedBlock = () => () => false; 151 | 152 | type Table = SerializerFactory<"table">; 153 | export const table: Table = () => () => false; 154 | 155 | type TableOfContents = SerializerFactory<"table_of_contents">; 156 | export const tableOfContents: TableOfContents = () => () => false; 157 | 158 | type TableRow = SerializerFactory<"table_row">; 159 | export const tableRow: TableRow = 160 | ({ urlMask }) => 161 | (block) => 162 | `| ${block.table_row.cells 163 | .flatMap((row) => row.map((column) => fromRichText([column], urlMask))) 164 | .join(" | ")} |`; 165 | 166 | type Template = SerializerFactory<"template">; 167 | export const template: Template = 168 | ({ urlMask }) => 169 | (block) => 170 | fromRichText(block.template.rich_text, urlMask); 171 | 172 | type ToDo = SerializerFactory<"to_do">; 173 | export const toDo: ToDo = 174 | ({ urlMask }) => 175 | (block) => 176 | md.todo(fromRichText(block.to_do.rich_text, urlMask), block.to_do.checked); 177 | 178 | type Toggle = SerializerFactory<"toggle">; 179 | export const toggle: Toggle = 180 | ({ urlMask }) => 181 | (block) => 182 | fromRichText(block.toggle.rich_text, urlMask); 183 | 184 | type Unsupported = SerializerFactory<"unsupported">; 185 | export const unsupported: Unsupported = () => () => false; 186 | 187 | type Video = SerializerFactory<"video">; 188 | export const video: Video = 189 | ({ urlMask }) => 190 | (block) => { 191 | const { title, href } = fromLink(block.video); 192 | return md.anchor(title, urlMask || href); 193 | }; 194 | 195 | export const factory = (options: FactoryOptions) => ({ 196 | audio: audio(options), 197 | bookmark: bookmark(options), 198 | breadcrumb: breadcrumb(options), 199 | bulleted_list_item: bulletedListItem(options), 200 | callout: callout(options), 201 | child_database: childDatabase(options), 202 | child_page: childPage(options), 203 | code: code(options), 204 | column: column(options), 205 | column_list: columnList(options), 206 | divider: divider(options), 207 | embed: embed(options), 208 | equation: equation(options), 209 | file: file(options), 210 | heading_1: heading1(options), 211 | heading_2: heading2(options), 212 | heading_3: heading3(options), 213 | image: image(options), 214 | link_preview: linkPreview(options), 215 | link_to_page: linkToPage(options), 216 | numbered_list_item: numberedListItem(options), 217 | paragraph: paragraph(options), 218 | pdf: pdf(options), 219 | quote: quote(options), 220 | synced_block: syncedBlock(options), 221 | table: table(options), 222 | table_of_contents: tableOfContents(options), 223 | table_row: tableRow(options), 224 | template: template(options), 225 | to_do: toDo(options), 226 | toggle: toggle(options), 227 | unsupported: unsupported(options), 228 | video: video(options), 229 | }); 230 | 231 | export const defaults = factory({ urlMask: false }); 232 | -------------------------------------------------------------------------------- /src/serializer/block/index.ts: -------------------------------------------------------------------------------- 1 | import { defaults } from "./defaults.js"; 2 | import { strategy } from "./strategy.js"; 3 | 4 | export default { defaults, strategy }; 5 | -------------------------------------------------------------------------------- /src/serializer/block/strategy.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "./defaults.js"; 2 | 3 | export const strategy = factory; 4 | -------------------------------------------------------------------------------- /src/serializer/block/types.ts: -------------------------------------------------------------------------------- 1 | import { ExtractBlock, NotionBlock } from "../../types.js"; 2 | 3 | export type FactoryOptions = { 4 | urlMask: string | false; 5 | }; 6 | 7 | export type SerializerFactory = ( 8 | options: FactoryOptions, 9 | ) => (block: ExtractBlock) => string | false | Promise; 10 | 11 | export type Serializer = ReturnType< 12 | SerializerFactory 13 | >; 14 | 15 | export type Serializers = { 16 | [K in NotionBlock["type"]]: Serializer; 17 | }; 18 | -------------------------------------------------------------------------------- /src/serializer/index.ts: -------------------------------------------------------------------------------- 1 | import block from "./block/index.js"; 2 | import { Serializers as BlockSerializers } from "./block/types.js"; 3 | import property from "./property/index.js"; 4 | import { Serializers as PropertySerializers } from "./property/types.js"; 5 | import * as utils from "./utils.js"; 6 | 7 | export { 8 | Serializer as BlockSerializer, 9 | Serializers as BlockSerializers, 10 | } from "./block/types.js"; 11 | 12 | export { 13 | Serializer as PropertySerializer, 14 | Serializers as PropertySerializers, 15 | } from "./property/types.js"; 16 | 17 | export type Serializers = { 18 | block: BlockSerializers; 19 | property: PropertySerializers; 20 | }; 21 | 22 | export const serializer = { block, property, utils }; 23 | -------------------------------------------------------------------------------- /src/serializer/property/defaults.ts: -------------------------------------------------------------------------------- 1 | import { anchor } from "md-utils-ts"; 2 | import { has } from "../../libs.js"; 3 | import { NotionProperty } from "../../types.js"; 4 | import { fromDate, fromRichText, fromUser } from "../utils.js"; 5 | import { FactoryOptions, Serializer, SerializerFactory } from "./types.js"; 6 | 7 | const DELIMITER = ", "; 8 | const EMPTY_STR = ""; 9 | 10 | type Checkbox = SerializerFactory<"checkbox">; 11 | export const checkbox: Checkbox = 12 | ({ urlMask }) => 13 | (name, prop) => 14 | `[${name}] ${prop.checkbox}`; 15 | 16 | type CreatedBy = SerializerFactory<"created_by">; 17 | export const createdBy: CreatedBy = 18 | ({ urlMask }) => 19 | (name, prop) => 20 | `[${name}] ${fromUser(prop.created_by)}`; 21 | 22 | type CreatedTime = SerializerFactory<"created_time">; 23 | export const createdTime: CreatedTime = 24 | ({ urlMask }) => 25 | (name, prop) => 26 | `[${name}] ${prop.created_time}`; 27 | 28 | type _Date = SerializerFactory<"date">; 29 | export const date: _Date = 30 | ({ urlMask }) => 31 | (name, prop) => 32 | `[${name}] ${fromDate(prop.date)}`; 33 | 34 | type Email = SerializerFactory<"email">; 35 | export const email: Email = 36 | ({ urlMask }) => 37 | (name, prop) => 38 | `[${name}] ${prop.email ?? EMPTY_STR}`; 39 | 40 | type Files = SerializerFactory<"files">; 41 | export const files: Files = 42 | ({ urlMask }) => 43 | (name, prop) => 44 | `[${name}] ` + 45 | prop.files 46 | .map((file) => { 47 | const href = has(file, "external") ? file.external.url : file.file.url; 48 | return anchor(file.name, href); 49 | }) 50 | .join(DELIMITER); 51 | 52 | type Formula = SerializerFactory<"formula">; 53 | export const formula: Formula = 54 | ({ urlMask }) => 55 | (name, prop) => { 56 | switch (prop.formula.type) { 57 | case "string": 58 | return `[${name}] ${prop.formula.string ?? EMPTY_STR}`; 59 | case "boolean": 60 | return `[${name}] ${prop.formula.boolean ?? EMPTY_STR}`; 61 | case "date": 62 | return `[${name}] ${fromDate(prop.formula.date)}`; 63 | case "number": 64 | return `[${name}] ${prop.formula.number}`; 65 | } 66 | }; 67 | 68 | type LastEditedBy = SerializerFactory<"last_edited_by">; 69 | export const lastEditedBy: LastEditedBy = 70 | ({ urlMask }) => 71 | (name, prop) => 72 | `[${name}] ${fromUser(prop.last_edited_by)}`; 73 | 74 | type LastEditedTime = SerializerFactory<"last_edited_time">; 75 | export const lastEditedTime: LastEditedTime = 76 | ({ urlMask }) => 77 | (name, prop) => 78 | `[${name}] ${prop.last_edited_time}`; 79 | 80 | type MultiSelect = SerializerFactory<"multi_select">; 81 | export const multiSelect: MultiSelect = 82 | ({ urlMask }) => 83 | (name, prop) => 84 | `[${name}] ` + 85 | prop.multi_select.map((select) => select.name).join(DELIMITER); 86 | 87 | type _Number = SerializerFactory<"number">; 88 | export const number: _Number = 89 | ({ urlMask }) => 90 | (name, prop) => 91 | `[${name}] ${prop.number ?? EMPTY_STR}`; 92 | 93 | type People = SerializerFactory<"people">; 94 | export const people: People = 95 | ({ urlMask }) => 96 | (name, prop) => 97 | `[${name}] ` + 98 | prop.people.map((person) => fromUser(person)).join(DELIMITER); 99 | 100 | type PhoneNumber = SerializerFactory<"phone_number">; 101 | export const phoneNumber: PhoneNumber = 102 | ({ urlMask }) => 103 | (name, prop) => 104 | `[${name}] ${prop.phone_number ?? EMPTY_STR}`; 105 | 106 | type Relation = SerializerFactory<"relation">; 107 | export const relation: Relation = 108 | ({ urlMask }) => 109 | (name, prop) => 110 | `[${name}] ` + prop.relation.map((item) => `${item.id}`).join(DELIMITER); 111 | 112 | type RichText = SerializerFactory<"rich_text">; 113 | export const richText: RichText = 114 | ({ urlMask }) => 115 | (name, prop) => 116 | `[${name}] ${fromRichText(prop.rich_text)}`; 117 | 118 | type Select = SerializerFactory<"select">; 119 | export const select: Select = 120 | ({ urlMask }) => 121 | (name, prop) => 122 | `[${name}] ${prop.select?.name ?? EMPTY_STR}`; 123 | 124 | type Status = SerializerFactory<"status">; 125 | export const status: Status = 126 | ({ urlMask }) => 127 | (name, prop) => 128 | `[${name}] ${prop.status?.name ?? EMPTY_STR}`; 129 | 130 | type Title = SerializerFactory<"title">; 131 | export const title: Title = 132 | ({ urlMask }) => 133 | (name, prop) => 134 | `[${name}] ${fromRichText(prop.title)}`; 135 | 136 | type UniqueId = SerializerFactory<"unique_id">; 137 | export const uniqueId: UniqueId = 138 | ({ urlMask }) => 139 | (name, prop) => { 140 | const prefix = prop.unique_id.prefix ?? ""; 141 | const _number = prop.unique_id.number ?? ""; 142 | const id = prefix + _number; 143 | return `[${name}] ${id || EMPTY_STR}`; 144 | }; 145 | 146 | type Url = SerializerFactory<"url">; 147 | export const url: Url = 148 | ({ urlMask }) => 149 | (name, prop) => 150 | `[${name}] ${prop.url ?? EMPTY_STR}`; 151 | 152 | type Verification = SerializerFactory<"verification">; 153 | export const verification: Verification = 154 | ({ urlMask }) => 155 | () => 156 | false; 157 | 158 | type OmitFromUnion = T extends U ? never : T; 159 | type RollupFactory = (options: FactoryOptions) => { 160 | [K in OmitFromUnion]: Serializer; 161 | }; 162 | const rollupFactory: RollupFactory = (options) => ({ 163 | checkbox: checkbox(options), 164 | created_by: createdBy(options), 165 | created_time: createdTime(options), 166 | date: date(options), 167 | email: email(options), 168 | files: files(options), 169 | formula: formula(options), 170 | last_edited_by: lastEditedBy(options), 171 | last_edited_time: lastEditedTime(options), 172 | multi_select: multiSelect(options), 173 | number: number(options), 174 | people: people(options), 175 | phone_number: phoneNumber(options), 176 | relation: relation(options), 177 | rich_text: richText(options), 178 | select: select(options), 179 | status: status(options), 180 | title: title(options), 181 | unique_id: uniqueId(options), 182 | url: url(options), 183 | verification: verification(options), 184 | }); 185 | 186 | type Rollup = SerializerFactory<"rollup">; 187 | export const rollup: Rollup = (options) => (name, prop) => { 188 | switch (prop.rollup.type) { 189 | case "number": 190 | return number(options)(name, prop.rollup); 191 | case "date": 192 | return date(options)(name, prop.rollup); 193 | case "array": 194 | const strategy = rollupFactory(options); 195 | return Promise.all( 196 | prop.rollup.array.map((item) => strategy[item.type](name, item as any)), 197 | ).then( 198 | (items) => 199 | `[${name}] ` + 200 | items 201 | .map((item) => item as string) 202 | .map((text) => text.replace(`[${name}] `, "")) 203 | .join(DELIMITER), 204 | ); 205 | } 206 | }; 207 | 208 | export const factory = (options: FactoryOptions) => ({ 209 | ...rollupFactory(options), 210 | rollup: rollup(options), 211 | }); 212 | 213 | export const defaults = factory({ urlMask: false }); 214 | -------------------------------------------------------------------------------- /src/serializer/property/index.ts: -------------------------------------------------------------------------------- 1 | import { NotionProperties } from "../../notion.types.js"; 2 | import { defaults } from "./defaults.js"; 3 | import { strategy } from "./strategy.js"; 4 | import { Serializers } from "./types.js"; 5 | 6 | export default { defaults, strategy }; 7 | 8 | export const propertiesSerializer = 9 | (serializers: Serializers) => (props: NotionProperties) => 10 | Promise.all( 11 | Object.entries(props).map(([key, prop]) => 12 | serializers[prop.type](key, prop as any), 13 | ), 14 | ).then((texts) => texts.filter((text): text is string => text !== false)); 15 | -------------------------------------------------------------------------------- /src/serializer/property/strategy.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "./defaults.js"; 2 | 3 | export const strategy = factory; 4 | -------------------------------------------------------------------------------- /src/serializer/property/types.ts: -------------------------------------------------------------------------------- 1 | import { NotionProperty } from "../../notion.types.js"; 2 | 3 | type MakeOptional = Omit & Partial>; 4 | 5 | export type FactoryOptions = { 6 | urlMask: string | false; 7 | }; 8 | 9 | export type SerializerFactory = ( 10 | options: FactoryOptions, 11 | ) => ( 12 | name: string, 13 | property: MakeOptional, "id">, 14 | ) => string | false | Promise; 15 | 16 | export type Serializer = ReturnType< 17 | SerializerFactory 18 | >; 19 | 20 | export type Serializers = { 21 | [K in NotionProperty["type"]]: Serializer; 22 | }; 23 | -------------------------------------------------------------------------------- /src/serializer/utils.ts: -------------------------------------------------------------------------------- 1 | import * as md from "md-utils-ts"; 2 | import { has } from "../libs.js"; 3 | import { ExtractBlock, ExtractProperty } from "../types.js"; 4 | 5 | type NotionParagraphBlock = ExtractBlock<"paragraph">; 6 | type NotionRichText = NotionParagraphBlock["paragraph"]["rich_text"]; 7 | type NotionAnnotations = NotionRichText[number]["annotations"]; 8 | type NotionImageBlock = ExtractBlock<"image">; 9 | type NotionLinkObject = NotionImageBlock["image"]; 10 | 11 | export type Annotate = (text: string, annotations: NotionAnnotations) => string; 12 | 13 | /** 14 | * `annotate` is a function designed to apply various annotations to a given text. It transforms the text based on the `NotionAnnotations` provided. 15 | * 16 | * Annotations include: code, bold, italic, strikethrough, and underline. 17 | * Multiple annotations can be applied to the text at once. 18 | * 19 | * @param {string} text - The original text to which annotations should be applied. 20 | * @param {NotionAnnotations} annotations - An object that specifies which annotations to apply to the text. 21 | * The object can have properties such as `code`, `bold`, `italic`, `strikethrough`, and `underline` set to `true` to apply the corresponding annotation. 22 | * 23 | * @returns {string} The annotated text. 24 | */ 25 | export const annotate: Annotate = (text, annotations) => { 26 | if (annotations.code) text = md.inlineCode(text); 27 | if (annotations.bold) text = md.bold(text); 28 | if (annotations.italic) text = md.italic(text); 29 | if (annotations.strikethrough) text = md.del(text); 30 | if (annotations.underline) text = md.underline(text); 31 | 32 | return text; 33 | }; 34 | 35 | export type FromRichText = ( 36 | richText: NotionRichText, 37 | urlMask?: string | false, 38 | ) => string; 39 | 40 | /** 41 | * `fromRichText` transforms a Notion-rich text object into a plain string representation, preserving annotations such as bold, italic, etc., and links (hrefs). 42 | * 43 | * The function first determines if the provided text is whitespace only. If true, it just returns the whitespace. 44 | * Otherwise, it preserves the leading and trailing spaces, trims the main content, applies annotations, and embeds links if present. 45 | * 46 | * @param {NotionRichText} richTextObject - An array of Notion rich text objects. Each object has a `plain_text` field with the raw text, 47 | * `annotations` detailing style attributes, and an optional `href` for links. 48 | * 49 | * @returns {string} A transformed string representation of the provided Notion-rich text object. 50 | */ 51 | export const fromRichText: FromRichText = (richTextObject, urlMask = false) => 52 | richTextObject 53 | .map(({ plain_text, annotations, href }) => { 54 | if (plain_text.match(/^\s*$/)) return plain_text; 55 | 56 | const leadingSpaceMatch = plain_text.match(/^(\s*)/); 57 | const trailingSpaceMatch = plain_text.match(/(\s*)$/); 58 | 59 | const leading_space = leadingSpaceMatch ? leadingSpaceMatch[0] : ""; 60 | const trailing_space = trailingSpaceMatch ? trailingSpaceMatch[0] : ""; 61 | 62 | const text = plain_text.trim(); 63 | 64 | if (text === "") return leading_space + trailing_space; 65 | 66 | const annotatedText = annotate(text, annotations); 67 | const linkedText = href 68 | ? md.anchor(annotatedText, urlMask || href) 69 | : annotatedText; 70 | 71 | return leading_space + linkedText + trailing_space; 72 | }) 73 | .join(""); 74 | 75 | export type fromLink = (linkObject: NotionLinkObject) => { 76 | title: string; 77 | href: string; 78 | }; 79 | 80 | /** 81 | * `fromLink` transforms a Notion link object into a simpler representation with a title and href. 82 | * 83 | * @param {NotionLinkObject} linkObject - The Notion link object to be transformed. 84 | * 85 | * @returns {Object} An object with a `title` which is either the caption of the link, the file name, or a default "link" string, 86 | * and `href` which is the URL of the link. 87 | */ 88 | export const fromLink: fromLink = (linkObject) => { 89 | const caption = fromRichText(linkObject.caption); 90 | const href = 91 | linkObject.type === "external" 92 | ? linkObject.external.url 93 | : linkObject.file.url; 94 | const fileName = href.match(/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/); 95 | const title = caption.trim() ? caption : fileName ? fileName[0] : "link"; 96 | return { title, href }; 97 | }; 98 | 99 | type NotionUserObject = ExtractProperty<"created_by">["created_by"]; 100 | type FromUser = (_user: NotionUserObject) => string; 101 | 102 | /** 103 | * `fromUser` transforms a Notion user object into a string representation of the user's name. 104 | * If the user is a bot, "[bot]" is appended to the name. 105 | * 106 | * @param {NotionUserObject} _user - The Notion user object to be transformed. 107 | * 108 | * @returns {string} A string representation of the user's name. 109 | */ 110 | export const fromUser: FromUser = (_user) => { 111 | if (!has(_user, "type")) return ""; 112 | 113 | const name = _user.name ?? ""; 114 | return _user.type === "person" ? `${name}` : `${name}[bot]`; 115 | }; 116 | 117 | type NotionDateObject = ExtractProperty<"date">["date"]; 118 | type FromDate = (date: NotionDateObject) => string; 119 | 120 | /** 121 | * `fromDate` transforms a Notion date object into a string representation. 122 | * If the date object contains both a start and end date, both dates are returned. Otherwise, only the start date is returned. 123 | * 124 | * @param {NotionDateObject} date - The Notion date object to be transformed. 125 | * 126 | * @returns {string} A string representation of the date or dates. 127 | */ 128 | export const fromDate: FromDate = (date) => { 129 | if (!date) return ""; 130 | 131 | return date.end ? `(start)${date.start}, (end): ${date.end}` : date.start; 132 | }; 133 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@notionhq/client"; 2 | import { NotionBlock, NotionPage } from "./notion.types.js"; 3 | import { BlockSerializers, PropertySerializers } from "./serializer/index.js"; 4 | 5 | export * from "./notion.types.js"; 6 | 7 | export type Dictionary = Record; 8 | 9 | export type Metadata = { 10 | id: string; 11 | title: string; 12 | createdTime: string; 13 | lastEditedTime: string; 14 | parentId?: string; 15 | } & T; 16 | 17 | export type Page = { 18 | metadata: Metadata; 19 | properties: string[]; 20 | lines: string[]; 21 | }; 22 | 23 | export type CrawlingFailure = { 24 | parentId?: string; 25 | reason: string; 26 | }; 27 | 28 | export type CrawlingResult = 29 | | { 30 | id: string; 31 | success: true; 32 | page: Page; 33 | } 34 | | { 35 | id: string; 36 | success: false; 37 | failure: CrawlingFailure; 38 | }; 39 | 40 | export type OptionalSerializers = { 41 | block?: Partial; 42 | property?: Partial; 43 | }; 44 | 45 | export type MetadataBuilderParams = { 46 | page: NotionPage | NotionBlock; 47 | title: string; 48 | properties?: string[]; 49 | parent?: Page; 50 | }; 51 | 52 | export type MetadataBuilder = ( 53 | params: MetadataBuilderParams, 54 | ) => T | Promise; 55 | 56 | export type CrawlerOptions = { 57 | client: Client; 58 | serializers?: OptionalSerializers; 59 | urlMask?: string | false; 60 | metadataBuilder?: MetadataBuilder; 61 | skipPageIds?: string[]; 62 | parent?: Page; 63 | }; 64 | 65 | export type Crawler = ( 66 | options: CrawlerOptions, 67 | ) => (rootPageId: string) => AsyncGenerator>; 68 | 69 | export type DBCrawler = ( 70 | options: CrawlerOptions, 71 | ) => (rootDatabaseId: string) => AsyncGenerator>; 72 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { h1 } from "md-utils-ts"; 2 | import { 3 | Crawler, 4 | CrawlingResult, 5 | DBCrawler, 6 | Dictionary, 7 | Page, 8 | } from "./types.js"; 9 | 10 | const nestHeading = (text: string) => (text.match(/^#+\s/) ? "#" + text : text); 11 | 12 | /** 13 | * `pageToString` transforms a `Page` object into a string representation. It formats the metadata, properties, and lines 14 | * into a unified string, with the metadata as an H1 heading and the properties nested between triple-dashes. 15 | * 16 | * @param {Page} params - An object containing: 17 | * - metadata: The metadata of the page which includes the title. 18 | * - properties: An array of property strings. 19 | * - lines: An array of line strings. 20 | * 21 | * @returns {string} A string representation of the provided page. 22 | */ 23 | export const pageToString = ({ 24 | metadata, 25 | properties, 26 | lines, 27 | }: Page): string => { 28 | const title = h1(metadata.title); 29 | const data = ["---", properties.join("\n"), "---"].join("\n"); 30 | const body = lines.map(nestHeading); 31 | return [title, data, ...body].join("\n"); 32 | }; 33 | 34 | type Crawling = 35 | | ReturnType>> 36 | | ReturnType>>; 37 | 38 | /** 39 | * Asynchronously waits for all results from a given crawling operation and collects them into an array. 40 | * This function is compatible with both `Crawler` and `DBCrawler` types. 41 | * 42 | * @param {Crawling} crawling - A generator function that yields crawling results. It can be an instance of `Crawler` or `DBCrawler`. 43 | * 44 | * @returns {Promise} A Promise that resolves to an array of `CrawlingResult` objects, which contain the results of the crawling operation. 45 | * 46 | * @example 47 | * // Initialize a Crawler or DBCrawler instance 48 | * const crawl = crawler({ client: myClient }); 49 | * // OR 50 | * const dbCrawl = dbCrawler({ client: myDbClient }); 51 | * 52 | * // Wait for all results and collect them 53 | * waitAllResults(crawl("someRootPageId")) 54 | * .then((allResults) => { 55 | * console.log("All crawled results:", allResults); 56 | * }) 57 | * .catch((error) => { 58 | * console.error("Error during crawling:", error); 59 | * }); 60 | */ 61 | export const waitAllResults = async ( 62 | crawling: Crawling, 63 | ) => { 64 | const results: CrawlingResult[] = []; 65 | 66 | for await (const result of crawling) { 67 | results.push(result); 68 | } 69 | 70 | return results; 71 | }; 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "lib": ["ES2019"], 5 | "module": "Node16", 6 | "resolveJsonModule": false, 7 | "moduleResolution": "Node16", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | }, 13 | "exclude": ["node_modules", "*.spec.ts"], 14 | "include": ["./src/**/*.ts"] 15 | } 16 | --------------------------------------------------------------------------------