From 2d51f1e5d2db5b418e364130470d44223987fdf1 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Feb 2022 09:40:59 +0100 Subject: [PATCH 01/29] feat: support synced blocks --- README.md | 1 + src/BlockRenderer.ts | 6 +++++- src/Blocks.ts | 7 ++++++- src/RecursiveBodyRenderer.ts | 18 +++++++++++------- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 85fc224..2346f21 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The following [Notion API block object types](https://developers.notion.com/refe | Divider | ✅ Yes | | | Table Of Contents | ❌ not planned | static site generators have their own ToC implementations | | Breadcrumb | ❌ not planned | static site generators have their own nav implementations | +| Synced Block | ✅ Yes | renders all children blocks | Support for other block types can be considered once they are available on the official Notion API. diff --git a/src/BlockRenderer.ts b/src/BlockRenderer.ts index 018e8be..c273765 100644 --- a/src/BlockRenderer.ts +++ b/src/BlockRenderer.ts @@ -24,7 +24,7 @@ export class BlockRenderer { block: Block, assets: AssetWriter, context: RenderingLoggingContext - ): Promise { + ): Promise { const renderMarkdown = async (text: RichText[]) => { return await this.richText.renderMarkdown(text, context); }; @@ -98,6 +98,10 @@ export class BlockRenderer { const msg = `\n`; const db = await this.deferredRenderer.renderChildDatabase(block.id); return { lines: msg + db.markdown }; + case "synced_block": + // nothing to render, only the contents of the synced block are relevant + // however, these are children nöpcl, and thus retrieved by recursion in RecusivveBodyRenderer + return null; case "toggle": case "child_page": case "embed": diff --git a/src/Blocks.ts b/src/Blocks.ts index d99e014..5ac9b7e 100644 --- a/src/Blocks.ts +++ b/src/Blocks.ts @@ -35,6 +35,10 @@ export interface ChildDatabaseBlock extends BlockBase { type: "child_database"; } +export interface SyncedBlock extends BlockBase { + type: "synced_block"; +} + // these are blocks that the notion API client code does not have proper typings for // for unknown reasons they removed types alltogether in v0.4 of the client // https://github.com/makenotion/notion-sdk-js/pulls?q=is%3Apr+is%3Aclosed#issuecomment-927781781 @@ -44,7 +48,8 @@ export type Block = | QuoteBlock | CalloutBlock | DividerBlock - | ChildDatabaseBlock; + | ChildDatabaseBlock + | SyncedBlock; export { Emoji, diff --git a/src/RecursiveBodyRenderer.ts b/src/RecursiveBodyRenderer.ts index 17ba316..8405b86 100644 --- a/src/RecursiveBodyRenderer.ts +++ b/src/RecursiveBodyRenderer.ts @@ -11,12 +11,12 @@ export class RecursiveBodyRenderer { constructor( readonly publicApi: Client, readonly blockRenderer: BlockRenderer - ) { } + ) {} async renderBody( page: Page, assets: AssetWriter, - context: RenderingLoggingContext, + context: RenderingLoggingContext ): Promise { debug("begin rendering body of page " + page.id, page.properties); @@ -42,24 +42,28 @@ export class RecursiveBodyRenderer { assets: AssetWriter, context: RenderingLoggingContext ): Promise { - const parentBlock = await this.blockRenderer.renderBlock(block, assets, context); - const parentLines = this.indent(parentBlock.lines, indent); + const parentBlock = await this.blockRenderer.renderBlock( + block, + assets, + context + ); + const parentLines = parentBlock && this.indent(parentBlock.lines, indent); // due to the way the Notion API is built, we need to recurisvely retrieve child // blocks, see https://developers.notion.com/reference/retrieve-a-block // "If a block contains the key has_children: true, use the Retrieve block children endpoint to get the list of children" const children = block.has_children ? (await this.publicApi.blocks.children.list({ block_id: block.id })) - .results + .results : []; - const childIndent = indent + " ".repeat(parentBlock.childIndent || 0); + const childIndent = indent + " ".repeat(parentBlock?.childIndent || 0); const renderChilds = children.map( async (x) => await this.renderBlock(x, childIndent, assets, context) ); const childLines = await Promise.all(renderChilds); - return [parentLines, ...childLines].join("\n\n"); + return [parentLines, ...childLines].filter((x) => !!x).join("\n\n"); } private indent(content: string, indent: string) { From 5672587b5a321578e6a2ec6a279af3ed704e793f Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Feb 2022 09:48:49 +0100 Subject: [PATCH 02/29] chore: upgrade dependencies --- package.json | 2 +- yarn.lock | 1380 ++++++++++++++++++++++++-------------------------- 2 files changed, 673 insertions(+), 709 deletions(-) diff --git a/package.json b/package.json index 98696c2..2ea663b 100644 --- a/package.json +++ b/package.json @@ -40,4 +40,4 @@ "mime-types": "^2.1.32", "slugify": "^1.6.0" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 3a68c08..f29b531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,32 +2,32 @@ # yarn lockfile v1 -"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== - dependencies: - "@babel/highlight" "^7.16.0" - -"@babel/compat-data@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.0.tgz#ea269d7f78deb3a7826c39a4048eecda541ebdaa" - integrity sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew== - -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.7.5": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" +"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.16.4": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.8.tgz#31560f9f29fdf1868de8cb55049538a1b9732a60" + integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q== + +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" + integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.10" + "@babel/types" "^7.16.8" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -35,144 +35,127 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.16.0", "@babel/generator@^7.7.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== +"@babel/generator@^7.16.8", "@babel/generator@^7.7.2": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" + integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== dependencies: - "@babel/types" "^7.16.0" + "@babel/types" "^7.16.8" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-compilation-targets@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.0.tgz#01d615762e796c17952c29e3ede9d6de07d235a8" - integrity sha512-S7iaOT1SYlqK0sQaCi21RX4+13hmdmnxIEAnQUB/eh7GeAnRjOUgTYpLkUOiRXzD+yog1JxP0qyAQZ7ZxVxLVg== +"@babel/helper-compilation-targets@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" + integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== dependencies: - "@babel/compat-data" "^7.16.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.16.6" + "@babel/compat-data" "^7.16.4" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" - integrity sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog== - dependencies: - "@babel/helper-get-function-arity" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-get-function-arity@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" - integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-hoist-variables@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" - integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-member-expression-to-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" - integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-imports@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" - integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-transforms@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" - integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-simple-access" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-optimise-call-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" - integrity sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-replace-supers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" - integrity sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-simple-access@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" - integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz#29672f43663e936df370aaeb22beddb3baec7438" - integrity sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helpers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.0.tgz#875519c979c232f41adfbd43a3b0398c2e388183" - integrity sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ== - dependencies: - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" + integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-simple-access@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" + integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc" + integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.7.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.0.tgz#cf147d7ada0a3655e79bf4b08ee846f00a00a295" - integrity sha512-TEHWXf0xxpi9wKVyBCmRcSSDjbJ/cl6LUdlbYUHEaNQUJGhreJbZrXT6sR4+fZLxVUJqNRB4KyOvjuy/D9009A== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.10", "@babel/parser@^7.16.12", "@babel/parser@^7.16.7": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6" + integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -259,42 +242,43 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.0.tgz#2feeb13d9334cc582ea9111d3506f773174179bb" - integrity sha512-Xv6mEXqVdaqCBfJFyeab0fH2DnUoMsDmhamxsSi4j8nLd4Vtw213WMJr55xxqipC/YVWyPY3K0blJncPYji+dQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/template@^7.16.0", "@babel/template@^7.3.3": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" - integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.7.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.0.tgz#965df6c6bfc0a958c1e739284d3c9fa4a6e3c45b" - integrity sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" + integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.7.2": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.10.tgz#448f940defbe95b5a8029975b051f75993e8239f" + integrity sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.16.10" + "@babel/types" "^7.16.8" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== dependencies: - "@babel/helper-validator-identifier" "^7.15.7" + "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -330,93 +314,93 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.3.1.tgz#e8ea3a475d3f8162f23d69efbfaa9cbe486bee93" - integrity sha512-RkFNWmv0iui+qsOr/29q9dyfKTTT5DCuP31kUwg7rmOKPT/ozLeGLKJKVIiOfbiKyleUZKIrHwhmiZWVe8IMdw== +"@jest/console@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107" + integrity sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.3.1" - jest-util "^27.3.1" + jest-message-util "^27.4.6" + jest-util "^27.4.2" slash "^3.0.0" -"@jest/core@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.3.1.tgz#04992ef1b58b17c459afb87ab56d81e63d386925" - integrity sha512-DMNE90RR5QKx0EA+wqe3/TNEwiRpOkhshKNxtLxd4rt3IZpCt+RSL+FoJsGeblRZmqdK4upHA/mKKGPPRAifhg== +"@jest/core@^27.4.7": + version "27.4.7" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.4.7.tgz#84eabdf42a25f1fa138272ed229bcf0a1b5e6913" + integrity sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg== dependencies: - "@jest/console" "^27.3.1" - "@jest/reporters" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.4.6" + "@jest/reporters" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.8.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^27.3.0" - jest-config "^27.3.1" - jest-haste-map "^27.3.1" - jest-message-util "^27.3.1" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-resolve-dependencies "^27.3.1" - jest-runner "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" - jest-watcher "^27.3.1" + jest-changed-files "^27.4.2" + jest-config "^27.4.7" + jest-haste-map "^27.4.6" + jest-message-util "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-resolve-dependencies "^27.4.6" + jest-runner "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + jest-validate "^27.4.6" + jest-watcher "^27.4.6" micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.3.1.tgz#2182defbce8d385fd51c5e7c7050f510bd4c86b1" - integrity sha512-BCKCj4mOVLme6Tanoyc9k0ultp3pnmuyHw73UHRPeeZxirsU/7E3HC4le/VDb/SMzE1JcPnto+XBKFOcoiJzVw== +"@jest/environment@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.4.6.tgz#1e92885d64f48c8454df35ed9779fbcf31c56d8b" + integrity sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg== dependencies: - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^27.3.0" + jest-mock "^27.4.6" -"@jest/fake-timers@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.3.1.tgz#1fad860ee9b13034762cdb94266e95609dfce641" - integrity sha512-M3ZFgwwlqJtWZ+QkBG5NmC23A9w+A6ZxNsO5nJxJsKYt4yguBd3i8TpjQz5NfCX91nEve1KqD9RA2Q+Q1uWqoA== +"@jest/fake-timers@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.4.6.tgz#e026ae1671316dbd04a56945be2fa251204324e8" + integrity sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^27.3.1" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-message-util "^27.4.6" + jest-mock "^27.4.6" + jest-util "^27.4.2" -"@jest/globals@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.3.1.tgz#ce1dfb03d379237a9da6c1b99ecfaca1922a5f9e" - integrity sha512-Q651FWiWQAIFiN+zS51xqhdZ8g9b88nGCobC87argAxA7nMfNQq0Q0i9zTfQYgLa6qFXk2cGANEqfK051CZ8Pg== +"@jest/globals@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.4.6.tgz#3f09bed64b0fd7f5f996920258bd4be8f52f060a" + integrity sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw== dependencies: - "@jest/environment" "^27.3.1" - "@jest/types" "^27.2.5" - expect "^27.3.1" + "@jest/environment" "^27.4.6" + "@jest/types" "^27.4.2" + expect "^27.4.6" -"@jest/reporters@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.3.1.tgz#28b5c1f5789481e23788048fa822ed15486430b9" - integrity sha512-m2YxPmL9Qn1emFVgZGEiMwDntDxRRQ2D58tiDQlwYTg5GvbFOKseYCcHtn0WsI8CG4vzPglo3nqbOiT8ySBT/w== +"@jest/reporters@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.4.6.tgz#b53dec3a93baf9b00826abf95b932de919d6d8dd" + integrity sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" @@ -424,74 +408,74 @@ glob "^7.1.2" graceful-fs "^4.2.4" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" + istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^27.3.1" - jest-resolve "^27.3.1" - jest-util "^27.3.1" - jest-worker "^27.3.1" + istanbul-reports "^3.1.3" + jest-haste-map "^27.4.6" + jest-resolve "^27.4.6" + jest-util "^27.4.2" + jest-worker "^27.4.6" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/source-map@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.6.tgz#be9e9b93565d49b0548b86e232092491fb60551f" - integrity sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g== +"@jest/source-map@^27.4.0": + version "27.4.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.4.0.tgz#2f0385d0d884fb3e2554e8f71f8fa957af9a74b6" + integrity sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.3.1.tgz#89adee8b771877c69b3b8d59f52f29dccc300194" - integrity sha512-mLn6Thm+w2yl0opM8J/QnPTqrfS4FoXsXF2WIWJb2O/GBSyResL71BRuMYbYRsGt7ELwS5JGcEcGb52BNrumgg== +"@jest/test-result@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.4.6.tgz#b3df94c3d899c040f602cea296979844f61bdf69" + integrity sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ== dependencies: - "@jest/console" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.4.6" + "@jest/types" "^27.4.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.3.1.tgz#4b3bde2dbb05ee74afdae608cf0768e3354683b1" - integrity sha512-siySLo07IMEdSjA4fqEnxfIX8lB/lWYsBPwNFtkOvsFQvmBrL3yj3k3uFNZv/JDyApTakRpxbKLJ3CT8UGVCrA== +"@jest/test-sequencer@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz#447339b8a3d7b5436f50934df30854e442a9d904" + integrity sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw== dependencies: - "@jest/test-result" "^27.3.1" + "@jest/test-result" "^27.4.6" graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-runtime "^27.3.1" + jest-haste-map "^27.4.6" + jest-runtime "^27.4.6" -"@jest/transform@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.3.1.tgz#ff80eafbeabe811e9025e4b6f452126718455220" - integrity sha512-3fSvQ02kuvjOI1C1ssqMVBKJpZf6nwoCiSu00zAKh5nrp3SptNtZy/8s5deayHnqxhjD9CWDJ+yqQwuQ0ZafXQ== +"@jest/transform@^27.4.6": + version "27.4.6" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.4.6.tgz#153621940b1ed500305eacdb31105d415dc30231" + integrity sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^27.2.5" - babel-plugin-istanbul "^6.0.0" + "@jest/types" "^27.4.2" + babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-regex-util "^27.0.6" - jest-util "^27.3.1" + jest-haste-map "^27.4.6" + jest-regex-util "^27.4.0" + jest-util "^27.4.2" micromatch "^4.0.4" - pirates "^4.0.1" + pirates "^4.0.4" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" - integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ== +"@jest/types@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" + integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -508,9 +492,9 @@ node-fetch "^2.6.1" "@sindresorhus/is@^4.0.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.2.0.tgz#667bfc6186ae7c9e0b45a08960c551437176e1ca" - integrity sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw== + version "4.4.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.4.0.tgz#e277e5bdbdf7cb1e20d320f02f5e2ed113cd3185" + integrity sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -520,9 +504,9 @@ type-detect "4.0.8" "@sinonjs/fake-timers@^8.0.1": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.0.1.tgz#1c1c9a91419f804e59ae8df316a07dd1c3a76b94" - integrity sha512-AU7kwFxreVd6OAXcAFlKSmZquiRUU0FvYm44k1Y1QbK7Co4m0aqfGMhjykIeQp/H6rcl+nFmj0zfdUcGVs9Dew== + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== dependencies: "@sinonjs/commons" "^1.7.0" @@ -559,9 +543,9 @@ integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" - integrity sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ== + version "7.1.18" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" + integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -570,9 +554,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.3" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" - integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== dependencies: "@babel/types" "^7.0.0" @@ -614,9 +598,9 @@ integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": version "3.0.0" @@ -633,17 +617,17 @@ "@types/istanbul-lib-report" "*" "@types/jest@^27.0.2": - version "27.0.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.2.tgz#ac383c4d4aaddd29bbf2b916d8d105c304a5fcd7" - integrity sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA== + version "27.4.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed" + integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== dependencies: jest-diff "^27.0.0" pretty-format "^27.0.0" "@types/js-yaml@^4.0.3": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.4.tgz#cc38781257612581a1a0eb25f1709d2b06812fce" - integrity sha512-AuHubXUmg0AzkXH0Mx6sIxeY/1C110mm/EkE/gB1sTRz3h2dao2W/63q42SlVST+lICxz5Oki2hzYA6+KnnieQ== + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== "@types/keyv@*": version "3.1.3" @@ -666,14 +650,14 @@ form-data "^3.0.0" "@types/node@*": - version "16.11.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== + version "17.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.14.tgz#33b9b94f789a8fedd30a68efdbca4dbb06b61f20" + integrity sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng== "@types/prettier@^2.1.5": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" - integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== + version "2.4.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf" + integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w== "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" @@ -728,9 +712,9 @@ acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4, acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== agent-base@6: version "6.0.2" @@ -800,21 +784,21 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -babel-jest@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.3.1.tgz#0636a3404c68e07001e434ac4956d82da8a80022" - integrity sha512-SjIF8hh/ir0peae2D6S6ZKRhUy7q/DnpH7k/V6fT4Bgs/LXXUztOpX4G2tCgq8mLo5HA9mN6NmlFMeYtKmIsTQ== +babel-jest@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.4.6.tgz#4d024e69e241cdf4f396e453a07100f44f7ce314" + integrity sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg== dependencies: - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^27.2.0" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.4.0" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" -babel-plugin-istanbul@^6.0.0: +babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== @@ -825,10 +809,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.2.0: - version "27.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.2.0.tgz#79f37d43f7e5c4fdc4b2ca3e10cc6cf545626277" - integrity sha512-TOux9khNKdi64mW+0OIhcmbAn75tTlzKhxmiNXevQaPbrBYK7YKjP1jl6NHTJ6XR5UgUrJbCnWlKVnJn29dfjw== +babel-plugin-jest-hoist@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz#d7831fc0f93573788d80dee7e682482da4c730d6" + integrity sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -853,12 +837,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.2.0: - version "27.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.2.0.tgz#556bbbf340608fed5670ab0ea0c8ef2449fba885" - integrity sha512-z7MgQ3peBwN5L5aCqBKnF6iqdlvZvFUQynEhu0J+X9nHLU72jO3iY331lcYrg+AssJ8q7xsv5/3AICzVmJ/wvg== +babel-preset-jest@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz#70d0e676a282ccb200fbabd7f415db5fdf393bca" + integrity sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg== dependencies: - babel-plugin-jest-hoist "^27.2.0" + babel-plugin-jest-hoist "^27.4.0" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -886,13 +870,13 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.16.6: - version "4.17.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.5.tgz#c827bbe172a4c22b123f5e337533ceebadfdd559" - integrity sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA== +browserslist@^4.17.5: + version "4.19.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" + integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== dependencies: - caniuse-lite "^1.0.30001271" - electron-to-chromium "^1.3.878" + caniuse-lite "^1.0.30001286" + electron-to-chromium "^1.4.17" escalade "^3.1.1" node-releases "^2.0.1" picocolors "^1.0.0" @@ -921,7 +905,7 @@ cacheable-lookup@^5.0.3: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== -cacheable-request@^7.0.1: +cacheable-request@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== @@ -945,14 +929,14 @@ camelcase@^5.3.1: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001271: - version "1.0.30001272" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz#8e9790ff995e9eb6e1f4c45cd07ddaa87cddbb14" - integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw== +caniuse-lite@^1.0.30001286: + version "1.0.30001304" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001304.tgz#38af55ed3fc8220cb13e35e6e7309c8c65a05559" + integrity sha512-bdsfZd6K6ap87AGqSHJP/s1V+U6Z5lyrcbBu3ovbCCf8cSYpwTtGrCBObMpJqwxfTbLW6YTIdbb1jEeTelcpYQ== chalk@^2.0.0: version "2.4.2" @@ -977,9 +961,9 @@ char-regex@^1.0.2: integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== ci-info@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" - integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -1096,9 +1080,9 @@ data-urls@^2.0.0: whatwg-url "^8.0.0" debug@4, debug@^4.1.0, debug@^4.1.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -1144,10 +1128,10 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -diff-sequences@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" - integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== +diff-sequences@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" + integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== diff@^4.0.1: version "4.0.2" @@ -1161,10 +1145,10 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -electron-to-chromium@^1.3.878: - version "1.3.885" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.885.tgz#c8cec32fbc61364127849ae00f2395a1bae7c454" - integrity sha512-JXKFJcVWrdHa09n4CNZYfYaK6EW5aAew7/wr3L1OnsD1L+JHL+RCtd7QgIsxUbFPeTwPlvnpqNNTOLkoefmtXg== +electron-to-chromium@^1.4.17: + version "1.4.59" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.59.tgz#657f2588c048fb95975779f8fea101fad854de89" + integrity sha512-AOJ3cAE0TWxz4fQ9zkND5hWrQg16nsZKVz9INOot1oV//u4wWu5xrj9CQMmPTYskkZRunSRc9sAnr4EkexXokg== emittery@^0.8.1: version "0.8.1" @@ -1245,17 +1229,15 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= -expect@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.3.1.tgz#d0f170b1f5c8a2009bab0beffd4bb94f043e38e7" - integrity sha512-MrNXV2sL9iDRebWPGOGFdPQRl2eDQNu/uhxIMShjjx74T6kC6jFIkmQ6OqXDtevjGUkyB2IT56RzDBqXf/QPCg== +expect@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.4.6.tgz#f335e128b0335b6ceb4fcab67ece7cbd14c942e6" + integrity sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag== dependencies: - "@jest/types" "^27.2.5" - ansi-styles "^5.0.0" - jest-get-type "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-regex-util "^27.0.6" + "@jest/types" "^27.4.2" + jest-get-type "^27.4.0" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -1367,16 +1349,16 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== got@^11.8.2: - version "11.8.2" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" - integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== + version "11.8.3" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" + integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" - cacheable-request "^7.0.1" + cacheable-request "^7.0.2" decompress-response "^6.0.0" http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" @@ -1384,9 +1366,9 @@ got@^11.8.2: responselike "^2.0.0" graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== has-flag@^3.0.0: version "3.0.0" @@ -1460,9 +1442,9 @@ iconv-lite@0.4.24: safer-buffer ">= 2.1.2 < 3" import-local@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.3.tgz#4d51c2c495ca9393da259ec66b62e022920211e0" - integrity sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -1485,10 +1467,10 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-core-module@^2.2.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== dependencies: has "^1.0.3" @@ -1532,17 +1514,7 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - -istanbul-lib-instrument@^5.0.4: +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== @@ -1571,234 +1543,234 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384" - integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ== +istanbul-reports@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.3.tgz#4bcae3103b94518117930d51283690960b50d3c2" + integrity sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^27.3.0: - version "27.3.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.3.0.tgz#22a02cc2b34583fc66e443171dc271c0529d263c" - integrity sha512-9DJs9garMHv4RhylUMZgbdCJ3+jHSkpL9aaVKp13xtXAD80qLTLrqcDZL1PHA9dYA0bCI86Nv2BhkLpLhrBcPg== +jest-changed-files@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.4.2.tgz#da2547ea47c6e6a5f6ed336151bd2075736eb4a5" + integrity sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" execa "^5.0.0" throat "^6.0.1" -jest-circus@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.3.1.tgz#1679e74387cbbf0c6a8b42de963250a6469e0797" - integrity sha512-v1dsM9II6gvXokgqq6Yh2jHCpfg7ZqV4jWY66u7npz24JnhP3NHxI0sKT7+ZMQ7IrOWHYAaeEllOySbDbWsiXw== +jest-circus@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.4.6.tgz#d3af34c0eb742a967b1919fbb351430727bcea6c" + integrity sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ== dependencies: - "@jest/environment" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.3.1" + expect "^27.4.6" is-generator-fn "^2.0.0" - jest-each "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" + jest-each "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + pretty-format "^27.4.6" slash "^3.0.0" stack-utils "^2.0.3" throat "^6.0.1" -jest-cli@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.3.1.tgz#b576f9d146ba6643ce0a162d782b40152b6b1d16" - integrity sha512-WHnCqpfK+6EvT62me6WVs8NhtbjAS4/6vZJnk7/2+oOr50cwAzG4Wxt6RXX0hu6m1169ZGMlhYYUNeKBXCph/Q== +jest-cli@^27.4.7: + version "27.4.7" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.4.7.tgz#d00e759e55d77b3bcfea0715f527c394ca314e5a" + integrity sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw== dependencies: - "@jest/core" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/core" "^27.4.7" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" - jest-config "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-config "^27.4.7" + jest-util "^27.4.2" + jest-validate "^27.4.6" prompts "^2.0.1" yargs "^16.2.0" -jest-config@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.3.1.tgz#cb3b7f6aaa8c0a7daad4f2b9573899ca7e09bbad" - integrity sha512-KY8xOIbIACZ/vdYCKSopL44I0xboxC751IX+DXL2+Wx6DKNycyEfV3rryC3BPm5Uq/BBqDoMrKuqLEUNJmMKKg== +jest-config@^27.4.7: + version "27.4.7" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.4.7.tgz#4f084b2acbd172c8b43aa4cdffe75d89378d3972" + integrity sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.3.1" - "@jest/types" "^27.2.5" - babel-jest "^27.3.1" + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.4.6" + "@jest/types" "^27.4.2" + babel-jest "^27.4.6" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-circus "^27.3.1" - jest-environment-jsdom "^27.3.1" - jest-environment-node "^27.3.1" - jest-get-type "^27.3.1" - jest-jasmine2 "^27.3.1" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-runner "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-circus "^27.4.6" + jest-environment-jsdom "^27.4.6" + jest-environment-node "^27.4.6" + jest-get-type "^27.4.0" + jest-jasmine2 "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-runner "^27.4.6" + jest-util "^27.4.2" + jest-validate "^27.4.6" micromatch "^4.0.4" - pretty-format "^27.3.1" + pretty-format "^27.4.6" + slash "^3.0.0" -jest-diff@^27.0.0, jest-diff@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.3.1.tgz#d2775fea15411f5f5aeda2a5e02c2f36440f6d55" - integrity sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ== +jest-diff@^27.0.0, jest-diff@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d" + integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w== dependencies: chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + diff-sequences "^27.4.0" + jest-get-type "^27.4.0" + pretty-format "^27.4.6" -jest-docblock@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3" - integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA== +jest-docblock@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.4.0.tgz#06c78035ca93cbbb84faf8fce64deae79a59f69f" + integrity sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg== dependencies: detect-newline "^3.0.0" -jest-each@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.3.1.tgz#14c56bb4f18dd18dc6bdd853919b5f16a17761ff" - integrity sha512-E4SwfzKJWYcvOYCjOxhZcxwL+AY0uFMvdCOwvzgutJiaiodFjkxQQDxHm8FQBeTqDnSmKsQWn7ldMRzTn2zJaQ== +jest-each@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.4.6.tgz#e7e8561be61d8cc6dbf04296688747ab186c40ff" + integrity sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" chalk "^4.0.0" - jest-get-type "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" - -jest-environment-jsdom@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.3.1.tgz#63ac36d68f7a9303494df783494856222b57f73e" - integrity sha512-3MOy8qMzIkQlfb3W1TfrD7uZHj+xx8Olix5vMENkj5djPmRqndMaXtpnaZkxmxM+Qc3lo+yVzJjzuXbCcZjAlg== - dependencies: - "@jest/environment" "^27.3.1" - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + jest-get-type "^27.4.0" + jest-util "^27.4.2" + pretty-format "^27.4.6" + +jest-environment-jsdom@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz#c23a394eb445b33621dfae9c09e4c8021dea7b36" + integrity sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-mock "^27.4.6" + jest-util "^27.4.2" jsdom "^16.6.0" -jest-environment-node@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.3.1.tgz#af7d0eed04edafb740311b303f3fe7c8c27014bb" - integrity sha512-T89F/FgkE8waqrTSA7/ydMkcc52uYPgZZ6q8OaZgyiZkJb5QNNCF6oPZjH9IfPFfcc9uBWh1574N0kY0pSvTXw== +jest-environment-node@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.4.6.tgz#ee8cd4ef458a0ef09d087c8cd52ca5856df90242" + integrity sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ== dependencies: - "@jest/environment" "^27.3.1" - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-mock "^27.4.6" + jest-util "^27.4.2" -jest-get-type@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff" - integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg== +jest-get-type@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" + integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== -jest-haste-map@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee" - integrity sha512-lYfNZIzwPccDJZIyk9Iz5iQMM/MH56NIIcGj7AFU1YyA4ewWFBl8z+YPJuSCRML/ee2cCt2y3W4K3VXPT6Nhzg== +jest-haste-map@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.4.6.tgz#c60b5233a34ca0520f325b7e2cc0a0140ad0862a" + integrity sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" - jest-regex-util "^27.0.6" - jest-serializer "^27.0.6" - jest-util "^27.3.1" - jest-worker "^27.3.1" + jest-regex-util "^27.4.0" + jest-serializer "^27.4.0" + jest-util "^27.4.2" + jest-worker "^27.4.6" micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.3.1.tgz#df6d3d07c7dafc344feb43a0072a6f09458d32b0" - integrity sha512-WK11ZUetDQaC09w4/j7o4FZDUIp+4iYWH/Lik34Pv7ukL+DuXFGdnmmi7dT58J2ZYKFB5r13GyE0z3NPeyJmsg== +jest-jasmine2@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz#109e8bc036cb455950ae28a018f983f2abe50127" + integrity sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw== dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.3.1" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.4.6" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^27.3.1" + expect "^27.4.6" is-generator-fn "^2.0.0" - jest-each "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" + jest-each "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + pretty-format "^27.4.6" throat "^6.0.1" -jest-leak-detector@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.3.1.tgz#7fb632c2992ef707a1e73286e1e704f9cc1772b2" - integrity sha512-78QstU9tXbaHzwlRlKmTpjP9k4Pvre5l0r8Spo4SbFFVy/4Abg9I6ZjHwjg2QyKEAMg020XcjP+UgLZIY50yEg== +jest-leak-detector@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz#ed9bc3ce514b4c582637088d9faf58a33bd59bf4" + integrity sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA== dependencies: - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + jest-get-type "^27.4.0" + pretty-format "^27.4.6" -jest-matcher-utils@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.3.1.tgz#257ad61e54a6d4044e080d85dbdc4a08811e9c1c" - integrity sha512-hX8N7zXS4k+8bC1Aj0OWpGb7D3gIXxYvPNK1inP5xvE4ztbz3rc4AkI6jGVaerepBnfWB17FL5lWFJT3s7qo8w== +jest-matcher-utils@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz#53ca7f7b58170638590e946f5363b988775509b8" + integrity sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA== dependencies: chalk "^4.0.0" - jest-diff "^27.3.1" - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + jest-diff "^27.4.6" + jest-get-type "^27.4.0" + pretty-format "^27.4.6" -jest-message-util@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.3.1.tgz#f7c25688ad3410ab10bcb862bcfe3152345c6436" - integrity sha512-bh3JEmxsTZ/9rTm0jQrPElbY2+y48Rw2t47uMfByNyUVR+OfPh4anuyKsGqsNkXk/TI4JbLRZx+7p7Hdt6q1yg== +jest-message-util@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.4.6.tgz#9fdde41a33820ded3127465e1a5896061524da31" + integrity sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.4" - pretty-format "^27.3.1" + pretty-format "^27.4.6" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.3.0: - version "27.3.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.3.0.tgz#ddf0ec3cc3e68c8ccd489bef4d1f525571a1b867" - integrity sha512-ziZiLk0elZOQjD08bLkegBzv5hCABu/c8Ytx45nJKkysQwGaonvmTxwjLqEA4qGdasq9o2I8/HtdGMNnVsMTGw== +jest-mock@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.4.6.tgz#77d1ba87fbd33ccb8ef1f061697e7341b7635195" + integrity sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -1806,188 +1778,182 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" - integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== +jest-regex-util@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca" + integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== -jest-resolve-dependencies@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.3.1.tgz#85b99bdbdfa46e2c81c6228fc4c91076f624f6e2" - integrity sha512-X7iLzY8pCiYOnvYo2YrK3P9oSE8/3N2f4pUZMJ8IUcZnT81vlSonya1KTO9ZfKGuC+svE6FHK/XOb8SsoRUV1A== +jest-resolve-dependencies@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz#fc50ee56a67d2c2183063f6a500cc4042b5e2327" + integrity sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw== dependencies: - "@jest/types" "^27.2.5" - jest-regex-util "^27.0.6" - jest-snapshot "^27.3.1" + "@jest/types" "^27.4.2" + jest-regex-util "^27.4.0" + jest-snapshot "^27.4.6" -jest-resolve@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.3.1.tgz#0e5542172a1aa0270be6f66a65888647bdd74a3e" - integrity sha512-Dfzt25CFSPo3Y3GCbxynRBZzxq9AdyNN+x/v2IqYx6KVT5Z6me2Z/PsSGFSv3cOSUZqJ9pHxilao/I/m9FouLw== +jest-resolve@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.4.6.tgz#2ec3110655e86d5bfcfa992e404e22f96b0b5977" + integrity sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" chalk "^4.0.0" graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" + jest-haste-map "^27.4.6" jest-pnp-resolver "^1.2.2" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-util "^27.4.2" + jest-validate "^27.4.6" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.3.1.tgz#1d594dcbf3bd8600a7e839e790384559eaf96e3e" - integrity sha512-r4W6kBn6sPr3TBwQNmqE94mPlYVn7fLBseeJfo4E2uCTmAyDFm2O5DYAQAFP7Q3YfiA/bMwg8TVsciP7k0xOww== +jest-runner@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.4.6.tgz#1d390d276ec417e9b4d0d081783584cbc3e24773" + integrity sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg== dependencies: - "@jest/console" "^27.3.1" - "@jest/environment" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.4.6" + "@jest/environment" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.8.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-docblock "^27.0.6" - jest-environment-jsdom "^27.3.1" - jest-environment-node "^27.3.1" - jest-haste-map "^27.3.1" - jest-leak-detector "^27.3.1" - jest-message-util "^27.3.1" - jest-resolve "^27.3.1" - jest-runtime "^27.3.1" - jest-util "^27.3.1" - jest-worker "^27.3.1" + jest-docblock "^27.4.0" + jest-environment-jsdom "^27.4.6" + jest-environment-node "^27.4.6" + jest-haste-map "^27.4.6" + jest-leak-detector "^27.4.6" + jest-message-util "^27.4.6" + jest-resolve "^27.4.6" + jest-runtime "^27.4.6" + jest-util "^27.4.2" + jest-worker "^27.4.6" source-map-support "^0.5.6" throat "^6.0.1" -jest-runtime@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.3.1.tgz#80fa32eb85fe5af575865ddf379874777ee993d7" - integrity sha512-qtO6VxPbS8umqhEDpjA4pqTkKQ1Hy4ZSi9mDVeE9Za7LKBo2LdW2jmT+Iod3XFaJqINikZQsn2wEi0j9wPRbLg== - dependencies: - "@jest/console" "^27.3.1" - "@jest/environment" "^27.3.1" - "@jest/globals" "^27.3.1" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" - "@types/yargs" "^16.0.0" +jest-runtime@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.4.6.tgz#83ae923818e3ea04463b22f3597f017bb5a1cffa" + integrity sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/globals" "^27.4.6" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" execa "^5.0.0" - exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-message-util "^27.3.1" - jest-mock "^27.3.0" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-haste-map "^27.4.6" + jest-message-util "^27.4.6" + jest-mock "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^16.2.0" -jest-serializer@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1" - integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== +jest-serializer@^27.4.0: + version "27.4.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.4.0.tgz#34866586e1cae2388b7d12ffa2c7819edef5958a" + integrity sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ== dependencies: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.3.1.tgz#1da5c0712a252d70917d46c037054f5918c49ee4" - integrity sha512-APZyBvSgQgOT0XumwfFu7X3G5elj6TGhCBLbBdn3R1IzYustPGPE38F51dBWMQ8hRXa9je0vAdeVDtqHLvB6lg== +jest-snapshot@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.4.6.tgz#e2a3b4fff8bdce3033f2373b2e525d8b6871f616" + integrity sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ== dependencies: "@babel/core" "^7.7.2" "@babel/generator" "^7.7.2" - "@babel/parser" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.3.1" + expect "^27.4.6" graceful-fs "^4.2.4" - jest-diff "^27.3.1" - jest-get-type "^27.3.1" - jest-haste-map "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-resolve "^27.3.1" - jest-util "^27.3.1" + jest-diff "^27.4.6" + jest-get-type "^27.4.0" + jest-haste-map "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-util "^27.4.2" natural-compare "^1.4.0" - pretty-format "^27.3.1" + pretty-format "^27.4.6" semver "^7.3.2" -jest-util@^27.0.0, jest-util@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.3.1.tgz#a58cdc7b6c8a560caac9ed6bdfc4e4ff23f80429" - integrity sha512-8fg+ifEH3GDryLQf/eKZck1DEs2YuVPBCMOaHQxVVLmQwl/CDhWzrvChTX4efLZxGrw+AA0mSXv78cyytBt/uw== +jest-util@^27.0.0, jest-util@^27.4.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.4.2.tgz#ed95b05b1adfd761e2cda47e0144c6a58e05a621" + integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" graceful-fs "^4.2.4" picomatch "^2.2.3" -jest-validate@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.3.1.tgz#3a395d61a19cd13ae9054af8cdaf299116ef8a24" - integrity sha512-3H0XCHDFLA9uDII67Bwi1Vy7AqwA5HqEEjyy934lgVhtJ3eisw6ShOF1MDmRPspyikef5MyExvIm0/TuLzZ86Q== +jest-validate@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.4.6.tgz#efc000acc4697b6cf4fa68c7f3f324c92d0c4f1f" + integrity sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.4.2" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.3.1" + jest-get-type "^27.4.0" leven "^3.1.0" - pretty-format "^27.3.1" + pretty-format "^27.4.6" -jest-watcher@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.3.1.tgz#ba5e0bc6aa843612b54ddb7f009d1cbff7e05f3e" - integrity sha512-9/xbV6chABsGHWh9yPaAGYVVKurWoP3ZMCv6h+O1v9/+pkOroigs6WzZ0e9gLP/njokUwM7yQhr01LKJVMkaZA== +jest-watcher@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.4.6.tgz#673679ebeffdd3f94338c24f399b85efc932272d" + integrity sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw== dependencies: - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.3.1" + jest-util "^27.4.2" string-length "^4.0.1" -jest-worker@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.3.1.tgz#0def7feae5b8042be38479799aeb7b5facac24b2" - integrity sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g== +jest-worker@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.6.tgz#5d2d93db419566cb680752ca0792780e71b3273e" + integrity sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" jest@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.3.1.tgz#b5bab64e8f56b6f7e275ba1836898b0d9f1e5c8a" - integrity sha512-U2AX0AgQGd5EzMsiZpYt8HyZ+nSVIh5ujQ9CPp9EQZJMjXIiSZpJNweZl0swatKRoqHWgGKM3zaSwm4Zaz87ng== + version "27.4.7" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.7.tgz#87f74b9026a1592f2da05b4d258e57505f28eca4" + integrity sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg== dependencies: - "@jest/core" "^27.3.1" + "@jest/core" "^27.4.7" import-local "^3.0.2" - jest-cli "^27.3.1" + jest-cli "^27.4.7" js-tokens@^4.0.0: version "4.0.0" @@ -2076,9 +2042,9 @@ keyv-file@^0.2.0: tslib "^1.9.3" keyv@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" - integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + version "4.0.5" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.5.tgz#bb12b467aba372fab2a44d4420c00d3c4ebd484c" + integrity sha512-531pkGLqV3BMg0eDqqJFI0R1mkK1Nm5xIP2mM6keP5P8WfFtCkg2IOwplTUmlGoTgIg9yQYZ/kdihhz89XH3vA== dependencies: json-buffer "3.0.1" @@ -2161,17 +2127,17 @@ micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" -mime-db@1.50.0: - version "1.50.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" - integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== mime-types@^2.1.12, mime-types@^2.1.32: - version "2.1.33" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" - integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: - mime-db "1.50.0" + mime-db "1.51.0" mimic-fn@^2.1.0: version "2.1.0" @@ -2211,9 +2177,9 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= node-fetch@^2.6.1: - version "2.6.5" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" - integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" @@ -2222,11 +2188,6 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-modules-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= - node-releases@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" @@ -2324,7 +2285,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -2335,16 +2296,14 @@ picocolors@^1.0.0: integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pirates@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== - dependencies: - node-modules-regexp "^1.0.0" +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== pkg-dir@^4.2.0: version "4.2.0" @@ -2358,12 +2317,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -pretty-format@^27.0.0, pretty-format@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" - integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== +pretty-format@^27.0.0, pretty-format@^27.4.6: + version "27.4.6" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" + integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== dependencies: - "@jest/types" "^27.2.5" ansi-regex "^5.0.1" ansi-styles "^5.0.0" react-is "^17.0.1" @@ -2432,12 +2390,13 @@ resolve.exports@^1.1.0: integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== resolve@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" responselike@^2.0.0: version "2.0.0" @@ -2495,9 +2454,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.5" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" - integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== + version "3.0.6" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" + integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== sisteransi@^1.0.5: version "1.0.5" @@ -2510,14 +2469,14 @@ slash@^3.0.0: integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slugify@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.1.tgz#a5fcaef29f4e57c6e932ce7044b6ffd9cf81b641" - integrity sha512-5ofqMTbetNhxlzjYYLBaZFQd6oiTuSkQlyfPEFIMwgUABlZQ0hbk5xIV9Ydd5jghWeRoO7GkiJliUvTpLOjNRA== + version "1.6.5" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8" + integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ== source-map-support@^0.5.6: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -2612,6 +2571,11 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -2678,9 +2642,9 @@ tr46@~0.0.3: integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= ts-jest@^27.0.7: - version "27.0.7" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.0.7.tgz#fb7c8c8cb5526ab371bc1b23d06e745652cca2d0" - integrity sha512-O41shibMqzdafpuP+CkrOL7ykbmLh+FqQrXEmV9CydQ5JBk0Sj0uAEF5TNNe94fZWKm3yYvWa/IbyV4Yg1zK2Q== + version "27.1.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.3.tgz#1f723e7e74027c4da92c0ffbd73287e8af2b2957" + integrity sha512-6Nlura7s6uM9BVUAoqLH7JHyMXjz8gluryjpPXxr3IxZdAXnU6FhjvVLHFtfd1vsE1p8zD1OJfskkc0jhTSnkA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" @@ -2739,9 +2703,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@^4.4.3: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" @@ -2749,9 +2713,9 @@ universalify@^0.1.0, universalify@^0.1.2: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== v8-to-istanbul@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz#0aeb763894f1a0a1676adf8a8b7612a38902446c" - integrity sha512-/PRhfd8aTNp9Ggr62HPzXg2XasNFGy5PBt0Rp04du7/8GNNSgxFL6WBTkgMKSL9bFjH+8kKEG3f37FmxiTqUUA== + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -2859,9 +2823,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.6: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== xml-name-validator@^3.0.0: version "3.0.0" From dd4c1ea962060d5fae1220ebf447d2bafe025681 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Feb 2022 09:58:04 +0100 Subject: [PATCH 03/29] chore: bump version to 0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ea663b..1f90b18 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.6.0", + "version": "0.7.0", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From 09092f078e34ce7aca23a8f05243968ac686eec4 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Feb 2022 10:12:55 +0100 Subject: [PATCH 04/29] fix: do not render formatting for formatted whitespace --- src/RichTextRenderer.spec.ts | 35 ++++++++++++++++++++++++++++++++++- src/RichTextRenderer.ts | 7 +++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/RichTextRenderer.spec.ts b/src/RichTextRenderer.spec.ts index a6e35cd..423310f 100644 --- a/src/RichTextRenderer.spec.ts +++ b/src/RichTextRenderer.spec.ts @@ -15,7 +15,7 @@ function annotations(x: Partial): Annotations { }; } -const context = new RenderingLoggingContext(''); +const context = new RenderingLoggingContext(""); describe("RichTextRenderer", () => { let sut: RichTextRenderer; @@ -143,5 +143,38 @@ describe("RichTextRenderer", () => { expect(result).toEqual("**Hello** \n\tWorld\n"); }); + + test("handles annotated whitespace", async () => { + const text: RichText[] = [ + { + type: "text", + plain_text: "Hello", + text: { + content: "Hello", + }, + annotations: annotations({}), + }, + { + type: "text", + plain_text: " ", + text: { + content: " ", + }, + annotations: annotations({ bold: true, italic: true }), + }, + { + type: "text", + plain_text: "World", + text: { + content: "World", + }, + annotations: annotations({}), + }, + ]; + + const result = await sut.renderMarkdown(text, context); + + expect(result).toEqual("Hello World"); + }); }); }); diff --git a/src/RichTextRenderer.ts b/src/RichTextRenderer.ts index bb15634..34f6e1e 100644 --- a/src/RichTextRenderer.ts +++ b/src/RichTextRenderer.ts @@ -120,8 +120,15 @@ export class RichTextRenderer { `Content failed parsing the whitespace test: '${content}'` ); } + const [_input, leading, core, trailing] = matchGroups; + if (!core) { + // this can happen e.g. if the formatted content is all whitespace (e.g. a "bold whitespace") + // this is something that can easily happen in a range based RTF editor, but does not make sense in markdown output + return [leading, trailing].join(""); + } + return [leading, modifier, core, reversedMod, trailing].join(""); } From 3cdc13a65159e5e7ed485d653daab58a00cac1c9 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 9 Feb 2022 16:10:55 +0100 Subject: [PATCH 05/29] refactor: introduce NotionApiFacade this allows handling common concerns like error handling for 502, rate limits etc. in a single place. Also notion api stats etc. if we ever want to do that. --- src/ChildDatabaseRenderer.ts | 18 ++++--------- src/MentionedPageRenderer.ts | 7 ++--- src/NotionApiFacade.ts | 51 ++++++++++++++++++++++++++++++++++++ src/RecursiveBodyRenderer.ts | 11 +++----- src/sync.ts | 35 +++++++++++-------------- 5 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 src/NotionApiFacade.ts diff --git a/src/ChildDatabaseRenderer.ts b/src/ChildDatabaseRenderer.ts index 0dccbec..bc91a12 100644 --- a/src/ChildDatabaseRenderer.ts +++ b/src/ChildDatabaseRenderer.ts @@ -1,4 +1,3 @@ -import { Client } from '@notionhq/client/build/src'; import { Page } from '@notionhq/client/build/src/api-types'; import { SyncConfig } from './'; @@ -7,6 +6,7 @@ import { Database } from './Database'; import { DatabaseTableRenderer } from './DatabaseTableRenderer'; import { DatabaseViewRenderer } from './DatabaseViewRenderer'; import { DeferredRenderer } from './DeferredRenderer'; +import { NotionApiFacade } from './NotionApiFacade'; import { RenderDatabasePageTask } from './RenderDatabasePageTask'; import { DatabaseConfig, DatabaseConfigRenderPages, DatabaseConfigRenderTable } from './SyncConfig'; @@ -15,7 +15,7 @@ const debug = require("debug")("child-database"); export class ChildDatabaseRenderer { constructor( private readonly config: SyncConfig, - private readonly publicApi: Client, + private readonly publicApi: NotionApiFacade, private readonly deferredRenderer: DeferredRenderer, private readonly tableRenderer: DatabaseTableRenderer, private readonly viewRenderer: DatabaseViewRenderer @@ -81,21 +81,13 @@ export class ChildDatabaseRenderer { databaseId: string, dbConfig: DatabaseConfig ): Promise { - const db = await this.publicApi.databases.retrieve({ - database_id: databaseId, - }); + const db = await this.publicApi.retrieveDatabase(databaseId); - const allPages = await this.publicApi.databases.query({ + const allPages = await this.publicApi.queryDatabase({ database_id: db.id, sorts: dbConfig.sorts, page_size: 100, - }); // todo: paging - - if (allPages.next_cursor) { - throw new Error( - `Paging not implemented, db ${db.id} has more than 100 entries` - ); - } + }); return allPages.results; } diff --git a/src/MentionedPageRenderer.ts b/src/MentionedPageRenderer.ts index fc0a3e8..1bab586 100644 --- a/src/MentionedPageRenderer.ts +++ b/src/MentionedPageRenderer.ts @@ -1,13 +1,14 @@ -import { APIErrorCode, Client } from '@notionhq/client'; +import { APIErrorCode } from '@notionhq/client'; import { SyncConfig } from './'; import { lookupDatabaseConfig } from './config'; import { DeferredRenderer } from './DeferredRenderer'; +import { NotionApiFacade } from './NotionApiFacade'; import { RenderDatabasePageTask } from './RenderDatabasePageTask'; export class MentionedPageRenderer { constructor( - readonly publicApi: Client, + readonly publicApi: NotionApiFacade, readonly deferredRenderer: DeferredRenderer, readonly config: SyncConfig ) {} @@ -54,7 +55,7 @@ export class MentionedPageRenderer { private async tryFindPage(pageId: string) { try { - return await this.publicApi.pages.retrieve({ page_id: pageId }); + return await this.publicApi.retrievePage( pageId); } catch (error: any) { if (error.code === APIErrorCode.ObjectNotFound) { // this is an expected error, e.g. we do not have access to the page source diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts new file mode 100644 index 0000000..b8e7ec4 --- /dev/null +++ b/src/NotionApiFacade.ts @@ -0,0 +1,51 @@ +import { Client } from '@notionhq/client'; +import { DatabasesQueryParameters } from '@notionhq/client/build/src/api-endpoints'; + +/** + * A common facade for interaction with notion API and handling common concerns (esp. retry on 502/rate limits). + * + * Note: considered splitting this up by consumer, but + */ +export class NotionApiFacade { + private readonly client: Client; + + constructor(notionApiToken: string) { + this.client = new Client({ + auth: notionApiToken, + }); + } + + async retrieveDatabase(databaseId: string) { + return await this.client.databases.retrieve({ + database_id: databaseId, + }); + } + + async queryDatabase(query: DatabasesQueryParameters) { + const result = await this.client.databases.query(query); // todo: paging + + if (result.next_cursor) { + throw new Error( + `Paging not implemented, db ${query.database_id} has more than 100 entries` + ); + } + + return result; + } + + async retrievePage(pageId: string) { + return await this.client.pages.retrieve({ page_id: pageId }); + } + + async listBlockChildren(blockId: string) { + const result = await this.client.blocks.children.list({ block_id: blockId }); // todo: paging here? + + if (result.next_cursor) { + throw new Error( + `Paging not implemented, block ${blockId} has more children than returned in a single request` + ); + } + + return result; + } +} diff --git a/src/RecursiveBodyRenderer.ts b/src/RecursiveBodyRenderer.ts index 8405b86..654a894 100644 --- a/src/RecursiveBodyRenderer.ts +++ b/src/RecursiveBodyRenderer.ts @@ -1,15 +1,15 @@ -import { Client } from '@notionhq/client'; import { Block, Page } from '@notionhq/client/build/src/api-types'; import { AssetWriter } from './AssetWriter'; import { BlockRenderer } from './BlockRenderer'; import { RenderingLoggingContext } from './logger'; +import { NotionApiFacade } from './NotionApiFacade'; const debug = require("debug")("body"); export class RecursiveBodyRenderer { constructor( - readonly publicApi: Client, + readonly publicApi: NotionApiFacade, readonly blockRenderer: BlockRenderer ) {} @@ -20,9 +20,7 @@ export class RecursiveBodyRenderer { ): Promise { debug("begin rendering body of page " + page.id, page.properties); - const childs = await this.publicApi.blocks.children.list({ - block_id: page.id, - }); + const childs = await this.publicApi.listBlockChildren(page.id); // todo: paging const renderChilds = childs.results.map( @@ -53,8 +51,7 @@ export class RecursiveBodyRenderer { // blocks, see https://developers.notion.com/reference/retrieve-a-block // "If a block contains the key has_children: true, use the Retrieve block children endpoint to get the list of children" const children = block.has_children - ? (await this.publicApi.blocks.children.list({ block_id: block.id })) - .results + ? (await this.publicApi.listBlockChildren(block.id)).results : []; const childIndent = indent + " ".repeat(parentBlock?.childIndent || 0); diff --git a/src/sync.ts b/src/sync.ts index a5e5ed5..916f038 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,24 +1,21 @@ -import { Client } from "@notionhq/client"; - -import { BlockRenderer } from "./BlockRenderer"; -import { ChildDatabaseRenderer } from "./ChildDatabaseRenderer"; -import { DatabaseViewRenderer } from "./DatabaseViewRenderer"; -import { DeferredRenderer } from "./DeferredRenderer"; -import { FrontmatterRenderer } from "./FrontmatterRenderer"; -import { LinkRenderer } from "./LinkRenderer"; -import { MentionedPageRenderer } from "./MentionedPageRenderer"; -import { DatabasePageRenderer } from "./DatabasePageRenderer"; -import { PropertiesParser } from "./PropertiesParser"; -import { RecursiveBodyRenderer } from "./RecursiveBodyRenderer"; -import { RichTextRenderer } from "./RichTextRenderer"; -import { SyncConfig } from "./SyncConfig"; -import { DatabaseTableRenderer } from "./DatabaseTableRenderer"; -import { DatabaseEntryRenderer } from "./DatabaseEntryRenderer"; +import { BlockRenderer } from './BlockRenderer'; +import { ChildDatabaseRenderer } from './ChildDatabaseRenderer'; +import { DatabaseEntryRenderer } from './DatabaseEntryRenderer'; +import { DatabasePageRenderer } from './DatabasePageRenderer'; +import { DatabaseTableRenderer } from './DatabaseTableRenderer'; +import { DatabaseViewRenderer } from './DatabaseViewRenderer'; +import { DeferredRenderer } from './DeferredRenderer'; +import { FrontmatterRenderer } from './FrontmatterRenderer'; +import { LinkRenderer } from './LinkRenderer'; +import { MentionedPageRenderer } from './MentionedPageRenderer'; +import { NotionApiFacade } from './NotionApiFacade'; +import { PropertiesParser } from './PropertiesParser'; +import { RecursiveBodyRenderer } from './RecursiveBodyRenderer'; +import { RichTextRenderer } from './RichTextRenderer'; +import { SyncConfig } from './SyncConfig'; export async function sync(notionApiToken: string, config: SyncConfig) { - const publicApi = new Client({ - auth: notionApiToken, - }); + const publicApi = new NotionApiFacade(notionApiToken); const deferredRenderer = new DeferredRenderer(); From 137e6d9dd79a6083e2c2be986d2e415a59f276a9 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 9 Feb 2022 16:46:23 +0100 Subject: [PATCH 06/29] feat: implement a default notion api retry logic this is necessary since notion seems to throw a lot more 502, timeouts and rate limit errors as of late. --- src/NotionApiFacade.ts | 77 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts index b8e7ec4..d6053dc 100644 --- a/src/NotionApiFacade.ts +++ b/src/NotionApiFacade.ts @@ -1,6 +1,10 @@ -import { Client } from '@notionhq/client'; +import { + APIErrorCode, APIResponseError, Client, RequestTimeoutError, UnknownHTTPResponseError +} from '@notionhq/client'; import { DatabasesQueryParameters } from '@notionhq/client/build/src/api-endpoints'; +const debug = require("debug")("notion-api"); + /** * A common facade for interaction with notion API and handling common concerns (esp. retry on 502/rate limits). * @@ -16,13 +20,17 @@ export class NotionApiFacade { } async retrieveDatabase(databaseId: string) { - return await this.client.databases.retrieve({ - database_id: databaseId, - }); + return await withRetry(async () => + this.client.databases.retrieve({ + database_id: databaseId, + }) + ); } async queryDatabase(query: DatabasesQueryParameters) { - const result = await this.client.databases.query(query); // todo: paging + const result = await withRetry( + async () => await this.client.databases.query(query) + ); // todo: paging if (result.next_cursor) { throw new Error( @@ -34,11 +42,18 @@ export class NotionApiFacade { } async retrievePage(pageId: string) { - return await this.client.pages.retrieve({ page_id: pageId }); + return await withRetry( + async () => await this.client.pages.retrieve({ page_id: pageId }) + ); } async listBlockChildren(blockId: string) { - const result = await this.client.blocks.children.list({ block_id: blockId }); // todo: paging here? + const result = await withRetry( + async () => + await this.client.blocks.children.list({ + block_id: blockId, + }) + ); // todo: paging here? if (result.next_cursor) { throw new Error( @@ -49,3 +64,51 @@ export class NotionApiFacade { return result; } } + + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function withRetry( + f: () => Promise, + maxRetries: number = 3, + retriableApiErrorCodes: APIErrorCode[] = [ + APIErrorCode.ServiceUnavailable, + APIErrorCode.RateLimited, + ], + retriableUnknownHTTPStatusCodes: number[] = [502] +) { + let lastError: any; + + for (let i = 1; i <= maxRetries; i++) { + try { + return await f(); + } catch (error: any) { + lastError = error; + const isRetriable = + (APIResponseError.isAPIResponseError(error) && + error.code in retriableApiErrorCodes) || + (UnknownHTTPResponseError.isUnknownHTTPResponseError(error) && + error.status in retriableUnknownHTTPStatusCodes) || + (RequestTimeoutError.isRequestTimeoutError(error)); + + if (!isRetriable) { + // throw any other error immediately + throw error; + } + + debug( + `Notion API request failed with error ${error.code}, ${ + maxRetries - i + } retries left` + ); + + await sleep(1000 * i); // chosen by fair dice roll + } + } + + throw new Error( + `Failed to execute Notion API request, even after ${maxRetries} retries. Original error was ${lastError}` + ); +} \ No newline at end of file From 9b5f03bad36e3896b70fb41d39681c49b191ec35 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 9 Feb 2022 16:59:54 +0100 Subject: [PATCH 07/29] feat: maintain and print notion API stats --- src/NotionApiFacade.ts | 119 +++++++++++++++++++++++++---------------- src/sync.ts | 2 + 2 files changed, 74 insertions(+), 47 deletions(-) diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts index d6053dc..36d2a4d 100644 --- a/src/NotionApiFacade.ts +++ b/src/NotionApiFacade.ts @@ -13,6 +13,12 @@ const debug = require("debug")("notion-api"); export class NotionApiFacade { private readonly client: Client; + private readonly stats = { + totalRequests: 0, + totalRetries: 0, + retriesByErrorCode: new Map(), + }; + constructor(notionApiToken: string) { this.client = new Client({ auth: notionApiToken, @@ -20,7 +26,7 @@ export class NotionApiFacade { } async retrieveDatabase(databaseId: string) { - return await withRetry(async () => + return await this.withRetry(async () => this.client.databases.retrieve({ database_id: databaseId, }) @@ -28,7 +34,7 @@ export class NotionApiFacade { } async queryDatabase(query: DatabasesQueryParameters) { - const result = await withRetry( + const result = await this.withRetry( async () => await this.client.databases.query(query) ); // todo: paging @@ -42,13 +48,13 @@ export class NotionApiFacade { } async retrievePage(pageId: string) { - return await withRetry( + return await this.withRetry( async () => await this.client.pages.retrieve({ page_id: pageId }) ); } async listBlockChildren(blockId: string) { - const result = await withRetry( + const result = await this.withRetry( async () => await this.client.blocks.children.list({ block_id: blockId, @@ -63,52 +69,71 @@ export class NotionApiFacade { return result; } -} + printStats() { + console.log("Notion API request statistics", this.stats); + } -function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function withRetry( - f: () => Promise, - maxRetries: number = 3, - retriableApiErrorCodes: APIErrorCode[] = [ - APIErrorCode.ServiceUnavailable, - APIErrorCode.RateLimited, - ], - retriableUnknownHTTPStatusCodes: number[] = [502] -) { - let lastError: any; - - for (let i = 1; i <= maxRetries; i++) { - try { - return await f(); - } catch (error: any) { - lastError = error; - const isRetriable = - (APIResponseError.isAPIResponseError(error) && - error.code in retriableApiErrorCodes) || - (UnknownHTTPResponseError.isUnknownHTTPResponseError(error) && - error.status in retriableUnknownHTTPStatusCodes) || - (RequestTimeoutError.isRequestTimeoutError(error)); - - if (!isRetriable) { - // throw any other error immediately - throw error; + private async withRetry( + f: () => Promise, + maxRetries: number = 3, + retriableApiErrorCodes: APIErrorCode[] = [ + APIErrorCode.ServiceUnavailable, + APIErrorCode.RateLimited, + ], + retriableUnknownHTTPStatusCodes: number[] = [502] + ) { + let lastError: any; + + for (let i = 1; i <= maxRetries; i++) { + try { + this.stats.totalRequests++; + return await f(); + } catch (error: any) { + lastError = error; + + const apiError = APIResponseError.isAPIResponseError(error) && error; + const unknownError = + UnknownHTTPResponseError.isUnknownHTTPResponseError(error) && error; + const timeoutError = + RequestTimeoutError.isRequestTimeoutError(error) && error; + + const isRetriable = + (apiError && error.code in retriableApiErrorCodes) || + (unknownError && error.status in retriableUnknownHTTPStatusCodes) || + timeoutError; + + if (!isRetriable) { + // throw any other error immediately + throw error; + } + + this.stats.totalRetries++; + const key = + (apiError && apiError.code) || + (unknownError && `${unknownError.code}.${unknownError.status}`) || + (timeoutError && `${timeoutError.code}`) || + "unknown"; + + const count = this.stats.retriesByErrorCode.get(key) || 0; + this.stats.retriesByErrorCode.set(key, count + 1); + + debug( + `Notion API request failed with error ${error.code}, ${ + maxRetries - i + } retries left` + ); + + await sleep(1000 * i); // chosen by fair dice roll } - - debug( - `Notion API request failed with error ${error.code}, ${ - maxRetries - i - } retries left` - ); - - await sleep(1000 * i); // chosen by fair dice roll } + + throw new Error( + `Failed to execute Notion API request, even after ${maxRetries} retries. Original error was ${lastError}` + ); } +} - throw new Error( - `Failed to execute Notion API request, even after ${maxRetries} retries. Original error was ${lastError}` - ); -} \ No newline at end of file +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/sync.ts b/src/sync.ts index 916f038..b551dae 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -58,5 +58,7 @@ export async function sync(notionApiToken: string, config: SyncConfig) { const rendered = deferredRenderer.getRenderedPages(); + publicApi.printStats(); + return rendered; } From 28677fcd81f1e5494528b65a1cf88937b7dadd7e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 9 Feb 2022 17:19:50 +0100 Subject: [PATCH 08/29] fix: correctly test error is retriable --- package.json | 2 +- src/NotionApiFacade.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 1f90b18..73a5daa 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.7.0", + "version": "0.8.0", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts index 36d2a4d..2cbd92b 100644 --- a/src/NotionApiFacade.ts +++ b/src/NotionApiFacade.ts @@ -26,11 +26,11 @@ export class NotionApiFacade { } async retrieveDatabase(databaseId: string) { - return await this.withRetry(async () => - this.client.databases.retrieve({ + return await this.withRetry(async () => { + return this.client.databases.retrieve({ database_id: databaseId, - }) - ); + }); + }); } async queryDatabase(query: DatabasesQueryParameters) { @@ -82,7 +82,7 @@ export class NotionApiFacade { APIErrorCode.RateLimited, ], retriableUnknownHTTPStatusCodes: number[] = [502] - ) { + ): Promise { let lastError: any; for (let i = 1; i <= maxRetries; i++) { @@ -99,8 +99,9 @@ export class NotionApiFacade { RequestTimeoutError.isRequestTimeoutError(error) && error; const isRetriable = - (apiError && error.code in retriableApiErrorCodes) || - (unknownError && error.status in retriableUnknownHTTPStatusCodes) || + (apiError && retriableApiErrorCodes.includes(error.code)) || + (unknownError && + retriableUnknownHTTPStatusCodes.includes(error.status)) || timeoutError; if (!isRetriable) { From 58991a06acffed8aab760d8b9bae1c1f2189c84f Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Feb 2022 18:19:35 +0100 Subject: [PATCH 09/29] fix: skip attempting to render archived pages to prevent 404 errors --- src/DatabasePageRenderer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index edbee3c..00f892b 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -44,7 +44,10 @@ export class DatabasePageRenderer { const context = new RenderingLoggingContext(page.url, file); if (page.archived) { - context.warn(`page is arvhied`); + // have to skip rendering archived pages as attempting to retrieve the block will result in a HTTP 404 + context.warn(`page is archived - skipping`); + + return; } try { From ed8af72627c99dad2f69f6b7dae2b9f4caa1e2c6 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Feb 2022 18:34:02 +0100 Subject: [PATCH 10/29] chore: bump version to 0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73a5daa..005b7c0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.0", + "version": "0.8.1", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From 9300c7faebc0580948d16b744a95aa4237ccc1a3 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 23 Feb 2022 22:55:32 +0100 Subject: [PATCH 11/29] feat: redesign properties handling This is a breaking change. Consumers need more control on how to emit frontmatter properties for advanced scenarios such as renaming/remapping properties to match the static site generator of choice. By taking this out of configuration and instead supplying actual functions we can easily pass that control to consumer while simultaneously removing a lot of complexity from notion-markdown-sync itself (i.e. the notion of property-keys vs. property-names). This simplifies the code overall. We probably will have to come up with a bit more ergonomics for the configuration model (e.g. good defaults) to enable making them more "DRY" --- src/DatabaseEntryRenderer.ts | 11 +--- src/DatabasePageMeta.ts | 3 - src/DatabasePageProperties.ts | 11 +--- src/DatabasePageRenderer.ts | 12 ++-- src/DatabaseTableRenderer.ts | 12 ++-- src/DatabaseViewRenderer.ts | 47 +++++++-------- src/DeferredRenderer.ts | 31 +++++----- src/FrontmatterRenderer.ts | 8 +-- src/LinkRenderer.spec.ts | 8 +-- src/LinkRenderer.ts | 7 +-- src/MentionedPageRenderer.ts | 2 +- src/PropertiesParser.spec.ts | 88 ++++++++------------------- src/PropertiesParser.ts | 105 +++++++-------------------------- src/RenderDatabaseEntryTask.ts | 14 +---- src/RenderedDatabaseEntry.ts | 2 +- src/SyncConfig.ts | 53 ++++------------- src/config.ts | 10 +--- src/index.ts | 1 + src/sync.ts | 2 +- 19 files changed, 124 insertions(+), 303 deletions(-) diff --git a/src/DatabaseEntryRenderer.ts b/src/DatabaseEntryRenderer.ts index 0b0819f..42451f6 100644 --- a/src/DatabaseEntryRenderer.ts +++ b/src/DatabaseEntryRenderer.ts @@ -2,24 +2,19 @@ import { Page } from "@notionhq/client/build/src/api-types"; import { PropertiesParser } from "./PropertiesParser"; import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; -import { DatabaseConfig } from "./SyncConfig"; export class DatabaseEntryRenderer { constructor(private readonly propertiesParser: PropertiesParser) {} async renderEntry( - page: Page, - config: DatabaseConfig + page: Page ): Promise { - const props = await this.propertiesParser.parseProperties(page, config); + const props = await this.propertiesParser.parsePageProperties(page); return { id: page.id, url: page.url, - properties: { - keys: props.keys, - values: props.properties, - }, + properties: props }; } } diff --git a/src/DatabasePageMeta.ts b/src/DatabasePageMeta.ts index 9714e48..12496fb 100644 --- a/src/DatabasePageMeta.ts +++ b/src/DatabasePageMeta.ts @@ -6,9 +6,6 @@ import { DatabaseEntryMeta } from './DatabaseEntryMeta'; */ export interface DatabasePageMeta extends DatabaseEntryMeta { title: string; - category: string; - order?: number; - layout?: string; } diff --git a/src/DatabasePageProperties.ts b/src/DatabasePageProperties.ts index a559c4d..f0f39b3 100644 --- a/src/DatabasePageProperties.ts +++ b/src/DatabasePageProperties.ts @@ -1,4 +1,4 @@ -import { DatabasePageMeta } from './DatabasePageMeta'; +import { DatabasePageMeta } from "./DatabasePageMeta"; export interface DatabasePageProperties { /** @@ -7,12 +7,7 @@ export interface DatabasePageProperties { meta: DatabasePageMeta; /** - * A mapping of property object keys -> property values + * A mapping of Notion property names -> parsed property values */ - values: Record; - - /** - * A mapping of Notion API property names -> property object keys - */ - keys: Map; + properties: Map; } diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index 00f892b..0884cbe 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -8,7 +8,6 @@ import { RenderingLoggingContext } from './logger'; import { PropertiesParser } from './PropertiesParser'; import { RecursiveBodyRenderer } from './RecursiveBodyRenderer'; import { RenderDatabasePageTask as RenderDatabasePageTask } from './RenderDatabasePageTask'; -import { slugify } from './slugify'; import { DatabaseConfigRenderPages } from './SyncConfig'; const fs = fsc.promises; @@ -24,13 +23,10 @@ export class DatabasePageRenderer { page: Page, config: DatabaseConfigRenderPages ): Promise { - const props = await this.propertiesParser.parsePageProperties(page, config); + const props = await this.propertiesParser.parsePageProperties(page); - const categorySlug = slugify(props.meta.category); - const destDir = `${config.outDir}/${categorySlug}`; - - const nameSlug = slugify(props.meta.title); - const file = `${destDir}/${nameSlug}.md`; + const destDir = config.pages.destinationPathBuilder(props); + const file = `${destDir}/${config.pages.filenameBuilder(props)}.md`; // Design: all the rendering performance could be greatly enhanced writing directly to output streams instead // of concatenating all in memory. OTOH naively concatenatic strings is straightforward, easier to debug and rendering @@ -53,7 +49,7 @@ export class DatabasePageRenderer { try { const assetWriter = new AssetWriter(destDir); - const frontmatter = this.frontmatterRenderer.renderFrontmatter(props); + const frontmatter = this.frontmatterRenderer.renderFrontmatter(props, config); const body = await this.bodyRenderer.renderBody( page, assetWriter, diff --git a/src/DatabaseTableRenderer.ts b/src/DatabaseTableRenderer.ts index ff8c212..71a5d7e 100644 --- a/src/DatabaseTableRenderer.ts +++ b/src/DatabaseTableRenderer.ts @@ -5,16 +5,14 @@ export class DatabaseTableRenderer { public renderTable(entries: RenderDatabaseEntryTask[]): string { const table: any[][] = []; - for (const page of entries) { + for (const entry of entries) { if (table.length === 0) { - const headers = Array.from(page.properties.keys.keys()); + const headers = Array.from(entry.properties.properties.keys()); table[0] = headers; } - const cols = Array.from(page.properties.keys.values()).map((c, i) => - DatabaseTableRenderer.escapeTableCell( - page.properties.values[c] - ) + const cols = Array.from(entry.properties.properties.values()).map((c, i) => + DatabaseTableRenderer.escapeTableCell(c) ); table.push(cols); @@ -29,6 +27,6 @@ export class DatabaseTableRenderer { return content.replace(/\n/g, "
"); } - return content.toString(); + return content?.toString() || ""; } } diff --git a/src/DatabaseViewRenderer.ts b/src/DatabaseViewRenderer.ts index 2cf5cf8..9c2a00f 100644 --- a/src/DatabaseViewRenderer.ts +++ b/src/DatabaseViewRenderer.ts @@ -1,29 +1,22 @@ -import { DatabaseTableRenderer } from './DatabaseTableRenderer'; -import { LinkRenderer } from './LinkRenderer'; -import * as markdownTable from './markdown-table'; -import { PropertiesParser } from './PropertiesParser'; -import { RenderDatabasePageTask } from './RenderDatabasePageTask'; -import { DatabaseConfigRenderPages, DatabaseView } from './SyncConfig'; - -const debug = require("debug")("database-views"); +import { DatabaseTableRenderer } from "./DatabaseTableRenderer"; +import { LinkRenderer } from "./LinkRenderer"; +import * as markdownTable from "./markdown-table"; +import { RenderDatabasePageTask } from "./RenderDatabasePageTask"; +import { DatabaseConfigRenderPages, DatabaseView } from "./SyncConfig"; // todo: name afte what it renders, not to where export class DatabaseViewRenderer { constructor(private readonly linkRenderer: LinkRenderer) {} - public renderViews(entries: RenderDatabasePageTask[], config: DatabaseConfigRenderPages): string { + public renderViews( + entries: RenderDatabasePageTask[], + config: DatabaseConfigRenderPages + ): string { const views = config.views?.map((view) => { - const propKeys = entries[0].properties.keys; - const propKey = propKeys.get(view.properties.groupBy); - - if (!propKey) { - const msg = `Could not render view ${view.title}, groupBy property ${view.properties.groupBy} not found`; - debug(msg + "%O", view); - throw new Error(msg); - } - const grouped = new Array( - ...groupBy(entries, (p) => p.properties.values[propKey]) + ...groupBy(entries, (p) => + p.properties.properties.get(view.properties.groupBy) + ) ); return grouped @@ -40,23 +33,23 @@ export class DatabaseViewRenderer { view: DatabaseView ): string { // todo: handle empty page - const props = pages[0].properties; + const pageProps = pages[0].properties; - const keys = PropertiesParser.filterIncludedKeys( - props.keys, - view.properties.include, - ); + const includedProps = + view.properties.include || Array.from(pageProps.properties.keys()); const table: any[][] = []; - const headers = Array.from(keys.keys()); + const headers = includedProps; table[0] = headers; - const cols = Array.from(keys.values()); + const cols = includedProps; pages.forEach((r) => table.push( cols.map((c, i) => { - const content = DatabaseTableRenderer.escapeTableCell(r.properties.values[c]); + const content = DatabaseTableRenderer.escapeTableCell( + r.properties.properties.get(c) + ); return i == 0 ? this.linkRenderer.renderPageLink(content, r) // make the first cell a relative link to the page : content; diff --git a/src/DeferredRenderer.ts b/src/DeferredRenderer.ts index 331e70f..63f634b 100644 --- a/src/DeferredRenderer.ts +++ b/src/DeferredRenderer.ts @@ -1,14 +1,17 @@ -import { Page } from '@notionhq/client/build/src/api-types'; - -import { ChildDatabaseRenderer } from './ChildDatabaseRenderer'; -import { Database } from './Database'; -import { DatabaseEntryRenderer } from './DatabaseEntryRenderer'; -import { DatabasePageRenderer } from './DatabasePageRenderer'; -import { RenderDatabaseEntryTask } from './RenderDatabaseEntryTask'; -import { RenderDatabasePageTask as RenderDatabasePageTask } from './RenderDatabasePageTask'; -import { RenderedDatabaseEntry } from './RenderedDatabaseEntry'; -import { RenderedDatabasePage } from './RenderedDatabasePage'; -import { DatabaseConfigRenderPages, DatabaseConfigRenderTable } from './SyncConfig'; +import { Page } from "@notionhq/client/build/src/api-types"; + +import { ChildDatabaseRenderer } from "./ChildDatabaseRenderer"; +import { Database } from "./Database"; +import { DatabaseEntryRenderer } from "./DatabaseEntryRenderer"; +import { DatabasePageRenderer } from "./DatabasePageRenderer"; +import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; +import { RenderDatabasePageTask as RenderDatabasePageTask } from "./RenderDatabasePageTask"; +import { RenderedDatabaseEntry } from "./RenderedDatabaseEntry"; +import { RenderedDatabasePage } from "./RenderedDatabasePage"; +import { + DatabaseConfigRenderPages, + DatabaseConfigRenderTable, +} from "./SyncConfig"; const debug = require("debug")("rendering"); @@ -59,7 +62,7 @@ export class DeferredRenderer { page: Page, config: DatabaseConfigRenderTable ): Promise { - const task = await this.entryRenderer.renderEntry(page, config); + const task = await this.entryRenderer.renderEntry(page); // entries are complete the moment they are retrieved, there's no more deferred processing necessary on them // also there should be no duplicate entries, so we do not cache/lookup any of them @@ -100,7 +103,7 @@ export class DeferredRenderer { ).map((x) => ({ file: x.file, meta: x.properties.meta, - properties: x.properties.values, + properties: x.properties.properties, })); const entries: RenderedDatabaseEntry[] = this.renderedEntries.map((x) => ({ @@ -108,7 +111,7 @@ export class DeferredRenderer { id: x.id, url: x.url, }, - properties: x.properties.values, + properties: x.properties.properties, })); return pages.concat(entries); diff --git a/src/FrontmatterRenderer.ts b/src/FrontmatterRenderer.ts index 38c7b21..d43444d 100644 --- a/src/FrontmatterRenderer.ts +++ b/src/FrontmatterRenderer.ts @@ -1,15 +1,13 @@ import * as yaml from 'js-yaml'; +import { DatabaseConfigRenderPages } from '.'; import { DatabasePageProperties } from './DatabasePageProperties'; export class FrontmatterRenderer { constructor() {} - public renderFrontmatter(props: DatabasePageProperties) { - const obj = { - ...props.meta, - properties: props.values, - }; + public renderFrontmatter(props: DatabasePageProperties, config: DatabaseConfigRenderPages) { + const obj = config.pages.frontmatterBuilder(props); const frontmatter = `---\n${yaml.dump(obj)}---\n\n`; diff --git a/src/LinkRenderer.spec.ts b/src/LinkRenderer.spec.ts index b6a347b..6968d9b 100644 --- a/src/LinkRenderer.spec.ts +++ b/src/LinkRenderer.spec.ts @@ -2,18 +2,14 @@ import { LinkRenderer } from './LinkRenderer'; import { RenderDatabasePageTask } from './RenderDatabasePageTask'; describe("LinkRenderer", () => { - const config = { - outDir: "out/", - }; - test("renderPageLink strips outDir from link", async () => { - const sut = new LinkRenderer(config as any); + const sut = new LinkRenderer(); const page: Partial = { id: "id", file: "out/test.md", }; const link = sut.renderPageLink("text", page as any); - expect(link).toEqual("[text](test.md)"); + expect(link).toEqual("[text](/out/test.md)"); }); }); diff --git a/src/LinkRenderer.ts b/src/LinkRenderer.ts index e627bf1..42da12c 100644 --- a/src/LinkRenderer.ts +++ b/src/LinkRenderer.ts @@ -1,16 +1,15 @@ -import { SyncConfig } from './'; import { RenderDatabasePageTask } from './RenderDatabasePageTask'; export class LinkRenderer { - constructor(private readonly config: SyncConfig) {} + constructor() {} renderUrlLink(text: string, url: string): string { return `[${text}](${url})`; } renderPageLink(text: string, page: RenderDatabasePageTask): string { - const url = page.file.substring(this.config.outDir.length); - + const url = "/" + page.file; + return this.renderUrlLink(text, url); } } diff --git a/src/MentionedPageRenderer.ts b/src/MentionedPageRenderer.ts index 1bab586..7be6c59 100644 --- a/src/MentionedPageRenderer.ts +++ b/src/MentionedPageRenderer.ts @@ -65,7 +65,7 @@ export class MentionedPageRenderer { } } } - + private formatMentionedPage(pageId: string, mentionPlaintext: string) { const formattedId = pageId.replace(/-/g, ""); return `mentioned page '${mentionPlaintext}' with url https://notion.so/${formattedId}`; diff --git a/src/PropertiesParser.spec.ts b/src/PropertiesParser.spec.ts index 860ed8e..511b27b 100644 --- a/src/PropertiesParser.spec.ts +++ b/src/PropertiesParser.spec.ts @@ -1,8 +1,8 @@ -import { Page } from '@notionhq/client/build/src/api-types'; +import { Page } from "@notionhq/client/build/src/api-types"; +import { DatabasePageProperties } from "./DatabasePageProperties"; -import { PropertiesParser } from './PropertiesParser'; -import { RichTextRenderer } from './RichTextRenderer'; -import { DatabaseConfig } from './SyncConfig'; +import { PropertiesParser } from "./PropertiesParser"; +import { RichTextRenderer } from "./RichTextRenderer"; const page: Partial = { id: "123", @@ -33,72 +33,34 @@ const page: Partial = { }; describe("PropertiesParser", () => { - describe("parse", () => { + describe("parsePageProperties", () => { + test("preserves all properties and adds conventional sort with title coming first", async () => { + const sut = new PropertiesParser( + new RichTextRenderer({} as any, {} as any) + ); - test("preserves all properties and adds conventional with no include filter", async () => { - const sut = new PropertiesParser(new RichTextRenderer({} as any, {} as any)); + const result = await sut.parsePageProperties(page as any); - const config: DatabaseConfig = { - outDir: "db/", - renderAs: 'table', - entries: { - emitToIndex: false - } - }; - - const result = await sut.parseProperties(page as any, config); - - const expected = { - category: null, - order: 30, - title: "Terraform", - keys: new Map([ - ["order", "order"], - ["Category", "category"], - ["Name", "name"], - ]), - properties: { - order: 30, - category: "Tools", - name: "Terraform", + const expected: DatabasePageProperties = { + meta: { + id: page.id!!, + url: page.url!!, + title: "Terraform", }, + properties: new Map([ + ["Name", "Terraform"], + ["order", 30], + ["Category", "Tools"], + ]), }; expect(result).toEqual(expected); // explicitly test key ordering - expect(Array.from(result.keys.keys())).toEqual(["Name", "order", "Category"]); - }); - - test("filters according to include filter", async () => { - const sut = new PropertiesParser(new RichTextRenderer({} as any, {} as any)); - - const config: DatabaseConfig = { - outDir: "db/", - renderAs: "table", - properties: { - include: ["Name", "Category"], - }, - entries: { - emitToIndex: false - } - }; - - const result = await sut.parseProperties(page as any, config); - - const expected = { - category: null, - order: 30, - title: "Terraform", - keys: new Map([ - ["Category", "category"], - ["Name", "name"], - ]), - properties: { - category: "Tools", - name: "Terraform", - }, - }; - expect(result).toEqual(expected); + expect(Array.from(result.properties.keys())).toEqual([ + "Name", + "order", + "Category", + ]); }); }); }); diff --git a/src/PropertiesParser.ts b/src/PropertiesParser.ts index b3a5312..e5aab1b 100644 --- a/src/PropertiesParser.ts +++ b/src/PropertiesParser.ts @@ -1,10 +1,8 @@ -import { Page, PropertyValue } from '@notionhq/client/build/src/api-types'; +import { Page, PropertyValue } from "@notionhq/client/build/src/api-types"; -import { DatabasePageProperties } from './DatabasePageProperties'; -import { RenderingLoggingContext } from './logger'; -import { RichTextRenderer } from './RichTextRenderer'; -import { slugify } from './slugify'; -import { DatabaseConfig, DatabaseConfigRenderPages } from './SyncConfig'; +import { DatabasePageProperties } from "./DatabasePageProperties"; +import { RenderingLoggingContext } from "./logger"; +import { RichTextRenderer } from "./RichTextRenderer"; const debug = require("debug")("properties"); @@ -12,117 +10,69 @@ export class PropertiesParser { constructor(private readonly richText: RichTextRenderer) {} public async parsePageProperties( - page: Page, - config: DatabaseConfigRenderPages + page: Page ): Promise { - const { title, category, order, properties, keys } = - await this.parseProperties(page, config); + const { title, properties } = await this.parseProperties(page); if (!title) { throw this.errorMissingRequiredProperty("of type 'title'", page); } - const theCategory = category || config.pages.frontmatter.category.static; - - if (!theCategory) { - throw this.errorMissingRequiredProperty( - config.pages.frontmatter.category.property || "static category", - page - ); - } - return { meta: { id: page.id, url: page.url, - title: title, // notion API always calls it name - category: theCategory, - order: order, - ...config.pages.frontmatter.extra, + title: title, }, - values: properties, - keys: keys, + properties, }; } - public async parseProperties(page: Page, config: DatabaseConfig) { - /** - * Design: we always lookup the properties on the page object itself. - * This way we only parse properties once and avoid any problems coming from - * e.g. category properties being filtered via include filters. - */ - + private async parseProperties(page: Page) { /** * Terminology: * * property: Notion API property name - * key: slugified Notion API property name, used to later build frontmatter * value: Notion API property value */ /** - * A record of key->value - */ - const properties: Record = {}; - - /** - * A map of proprety -> key + * A record of property->value */ - const keys = new Map(); + const properties: Map = new Map(); let title: string | null = null; let titleProperty: string | null = null; - let category: string | null = null; - let order: number | undefined = undefined; - - const categoryProperty = - config.renderAs === "pages+views" && - config.pages.frontmatter.category.property; const context = new RenderingLoggingContext(page.url); for (const [name, value] of Object.entries(page.properties)) { const parsedValue = await this.parsePropertyValue(value, context); - - if ( - !config.properties?.include || - config.properties.include.indexOf(name) >= 0 - ) { - const slug = slugify(name); - properties[slug] = parsedValue; - keys.set(name, slug); - } + properties.set(name, parsedValue); if (value.type === "title") { title = parsedValue; titleProperty = name; } - - if (categoryProperty && name === categoryProperty) { - category = parsedValue; - } - - if (name === "order") { - order = parsedValue; - } } - if (!titleProperty) { + if (!title || !titleProperty) { throw this.errorMissingRequiredProperty("of type 'title'", page); } // no explicit ordering specified, so we make sure to put the title property first - const includes = config.properties?.include || [ + const keyOrder = [ titleProperty, - ...Array.from(keys.keys()).filter((x) => x != titleProperty), + ...Array.from(properties.keys()).filter((x) => x != titleProperty), ]; + // maps preserve insertion order + const sortedProperties = new Map(); + keyOrder.forEach(x => sortedProperties.set(x, properties.get(x))); + return { title, - category, - order, - properties, - keys: PropertiesParser.filterIncludedKeys(keys, includes), + properties: sortedProperties, }; } @@ -172,21 +122,6 @@ export class PropertiesParser { } } - public static filterIncludedKeys( - keys: Map, - includes: string[] | undefined - ): Map { - if (!includes) { - return keys; - } - - // Maps iterate in insertion order, so preserve the correct ordering of keys according to includes ordering - const filtered = new Map(); - includes.forEach((i) => filtered.set(i, keys.get(i)!!)); // todo: should probably handle undefined here - - return filtered; - } - private errorMissingRequiredProperty(propertyName: string, page: Page) { // todo: should this use context? const msg = `Page ${page.url} is missing required property ${propertyName}`; diff --git a/src/RenderDatabaseEntryTask.ts b/src/RenderDatabaseEntryTask.ts index 34d0538..2c88143 100644 --- a/src/RenderDatabaseEntryTask.ts +++ b/src/RenderDatabaseEntryTask.ts @@ -1,18 +1,8 @@ +import { DatabasePageProperties } from "./DatabasePageProperties"; export interface RenderDatabaseEntryTask { id: string; url: string; - properties: { - /** - * A mapping of property object keys -> property values - */ - values: Record; - - /** - * A mapping of Notion API property names -> property object keys - */ - keys: Map; - }; - + properties: DatabasePageProperties // note: there's nothing to do to render an individual database entry, they are always rendered as part of tables } diff --git a/src/RenderedDatabaseEntry.ts b/src/RenderedDatabaseEntry.ts index 0223692..7b1dcf8 100644 --- a/src/RenderedDatabaseEntry.ts +++ b/src/RenderedDatabaseEntry.ts @@ -2,5 +2,5 @@ import { DatabaseEntryMeta } from './DatabaseEntryMeta'; export interface RenderedDatabaseEntry { meta: DatabaseEntryMeta, - properties: Record; + properties: Map; } diff --git a/src/SyncConfig.ts b/src/SyncConfig.ts index 9f36b06..70ed1bc 100644 --- a/src/SyncConfig.ts +++ b/src/SyncConfig.ts @@ -1,4 +1,5 @@ import { Sort } from "@notionhq/client/build/src/api-types"; +import { DatabasePageProperties } from "./DatabasePageProperties"; export interface SyncConfig { /** @@ -14,11 +15,9 @@ export interface SyncConfig { cmsDatabaseId: string; /** - * The output directory where the sync will place pages. - * - * Example: "docs/" + * Configuration options for the rendered pages */ - outDir: string; + pages: PageConfig; /** * Configuration options for any database encountered while traversing the block graph. @@ -32,29 +31,11 @@ export type DatabaseConfig = | DatabaseConfigRenderTable; export interface DatabaseConfigBase { - /** - * The output directory where the sync will place pages of this database. - * - * Example: docs/mydb" - */ - outDir: string; - /** * Notion API https://developers.notion.com/reference/post-database-query#post-database-query-sort */ sorts?: Sort[]; - /** - * Configuration options for Notion API page properties - */ - properties?: { - /** - * A whitelist of Notion API page property names to include in the markdown page properties. - * Use this to select properties for export and control their ordering in rendered tables. - */ - include?: string[]; - }; - renderAs: "table" | "pages+views"; } @@ -69,29 +50,19 @@ export interface DatabaseConfigRenderTable extends DatabaseConfigBase { }; } +interface PageConfig { + frontmatterBuilder: (props: DatabasePageProperties) => Record; + destinationPathBuilder: (props: DatabasePageProperties) => string; + filenameBuilder: (props: DatabasePageProperties) => string; +} + export interface DatabaseConfigRenderPages extends DatabaseConfigBase { renderAs: "pages+views"; + /** - * Add custom data to the page frontmatter + * Configuration options for the rendered pages */ - pages: { - frontmatter: { - category: { - /** - * The Notion API page property that provides an optional sub-category value to use for the markdown page category. - * - * Example: "Cluster" - */ - property?: string; - /** - * A static category value to assign to every page - */ - static?: string; - }; - - extra?: Record; - }; - }; + pages: PageConfig; /** * Configure "views" to render on the page where the child_database is encountered. diff --git a/src/config.ts b/src/config.ts index 67244b3..1de2dfa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,19 +10,11 @@ export function lookupDatabaseConfig( databaseId: string | null ): DatabaseConfig { const rootCmsDbConfig: DatabaseConfigRenderPages = { - outDir: config.outDir, renderAs: "pages+views", - pages: { - frontmatter: { - category: { - property: "Category", - }, - }, - }, + pages: config.pages, views: [], }; const defaultDbConfig: DatabaseConfigRenderTable = { - outDir: config.outDir + "/" + databaseId, renderAs: "table", entries: { emitToIndex: false, diff --git a/src/index.ts b/src/index.ts index de86d5b..2e3ad18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { RenderedDatabaseEntry } from './RenderedDatabaseEntry'; export { RenderedDatabasePage } from './RenderedDatabasePage'; export { sync } from './sync'; +export { slugify } from './slugify'; export { DatabaseConfig, DatabaseConfigRenderPages, DatabaseConfigRenderTable, SyncConfig } from './SyncConfig'; diff --git a/src/sync.ts b/src/sync.ts index b551dae..bb6c944 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -26,7 +26,7 @@ export async function sync(notionApiToken: string, config: SyncConfig) { deferredRenderer, config ); - const linkRenderer = new LinkRenderer(config); + const linkRenderer = new LinkRenderer(); const viewRenderer = new DatabaseViewRenderer(linkRenderer); const richTextRenderer = new RichTextRenderer( mentionedPageRenderer, From dcd3e042d17d8cf5bb13f66322bcb2afe3644556 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Feb 2022 13:45:36 +0100 Subject: [PATCH 12/29] feat: simplify config model to reduce boilerplate --- src/DatabasePageRenderer.ts | 30 ++++++++++++++++++------------ src/SyncConfig.ts | 25 ++++++++++++++++++++----- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index 0884cbe..b98034f 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -1,14 +1,15 @@ -import * as fsc from 'fs'; +import * as fsc from "fs"; -import { Page } from '@notionhq/client/build/src/api-types'; +import { Page } from "@notionhq/client/build/src/api-types"; -import { AssetWriter } from './AssetWriter'; -import { FrontmatterRenderer } from './FrontmatterRenderer'; -import { RenderingLoggingContext } from './logger'; -import { PropertiesParser } from './PropertiesParser'; -import { RecursiveBodyRenderer } from './RecursiveBodyRenderer'; -import { RenderDatabasePageTask as RenderDatabasePageTask } from './RenderDatabasePageTask'; -import { DatabaseConfigRenderPages } from './SyncConfig'; +import { AssetWriter } from "./AssetWriter"; +import { FrontmatterRenderer } from "./FrontmatterRenderer"; +import { RenderingLoggingContext } from "./logger"; +import { PropertiesParser } from "./PropertiesParser"; +import { RecursiveBodyRenderer } from "./RecursiveBodyRenderer"; +import { RenderDatabasePageTask as RenderDatabasePageTask } from "./RenderDatabasePageTask"; +import { DatabaseConfigRenderPages } from "./SyncConfig"; +import { slugify } from "./slugify"; const fs = fsc.promises; @@ -25,8 +26,10 @@ export class DatabasePageRenderer { ): Promise { const props = await this.propertiesParser.parsePageProperties(page); - const destDir = config.pages.destinationPathBuilder(props); - const file = `${destDir}/${config.pages.filenameBuilder(props)}.md`; + const destDir = config.pages.destinationDirBuilder(props); + const filenameBuilder = + config.pages.filenameBuilder || ((x) => slugify(x.meta.title)); + const file = `${destDir}/${filenameBuilder(props)}.md`; // Design: all the rendering performance could be greatly enhanced writing directly to output streams instead // of concatenating all in memory. OTOH naively concatenatic strings is straightforward, easier to debug and rendering @@ -49,7 +52,10 @@ export class DatabasePageRenderer { try { const assetWriter = new AssetWriter(destDir); - const frontmatter = this.frontmatterRenderer.renderFrontmatter(props, config); + const frontmatter = this.frontmatterRenderer.renderFrontmatter( + props, + config + ); const body = await this.bodyRenderer.renderBody( page, assetWriter, diff --git a/src/SyncConfig.ts b/src/SyncConfig.ts index 70ed1bc..1b1fd30 100644 --- a/src/SyncConfig.ts +++ b/src/SyncConfig.ts @@ -17,7 +17,7 @@ export interface SyncConfig { /** * Configuration options for the rendered pages */ - pages: PageConfig; + pages: PagesConfig; /** * Configuration options for any database encountered while traversing the block graph. @@ -50,10 +50,25 @@ export interface DatabaseConfigRenderTable extends DatabaseConfigBase { }; } -interface PageConfig { +interface PagesConfig { + /** + * Build frontmatter object + */ frontmatterBuilder: (props: DatabasePageProperties) => Record; - destinationPathBuilder: (props: DatabasePageProperties) => string; - filenameBuilder: (props: DatabasePageProperties) => string; + + /** + * Build the destination directory where to store the page. + * Final path will be $destinationDir/$filename.md + */ + destinationDirBuilder: (props: DatabasePageProperties) => string; + + /** + * Build the filename to store the page. + * Final path will be $destinationDir/$filename.md. + * + * default: slug of the meta.title + */ + filenameBuilder?: (props: DatabasePageProperties) => string; } export interface DatabaseConfigRenderPages extends DatabaseConfigBase { @@ -62,7 +77,7 @@ export interface DatabaseConfigRenderPages extends DatabaseConfigBase { /** * Configuration options for the rendered pages */ - pages: PageConfig; + pages: PagesConfig; /** * Configure "views" to render on the page where the child_database is encountered. From f810616ef263b8692b2e5dfd29c9885d056fe143 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Feb 2022 15:04:07 +0100 Subject: [PATCH 13/29] doc: update example --- README.md | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2346f21..041150a 100644 --- a/README.md +++ b/README.md @@ -112,15 +112,20 @@ Consult the [SyncConfig](./src/SyncConfig.ts) reference for documentation of ava > A CLI tool could be made available later. ```typescript -import { SyncConfig, sync } from "notion-markdown-cms"; - +import { slugify, SyncConfig, sync } from "notion-markdown-cms"; const config: SyncConfig = { cmsDatabaseId: "8f1de8c578fb4590ad6fbb0dbe283338", - outDir: "docs/", - indexPath: "docs/.vuepress/index.ts", + pages: { + destinationDirBuilder: (page) => slugify(page.properties.get("Category")), + frontmatterBuilder: (page) => ({ + id: page.meta.id, + url: page.meta.url, + title: page.meta.title, + category: page.properties.get("Category") + }), + }, databases: { "fe9836a9-6557-4f17-8adb-a93d2584f35f": { - parentCategory: "cfmm/", sorts: [ { property: "Scope", @@ -131,10 +136,24 @@ const config: SyncConfig = { direction: "ascending", }, ], - properties: { - category: "scope", - include: ["Name", "Scope", "Cluster", "Journey Stage", "Summary"], + renderAs: "pages+views", + pages: { + destinationDirBuilder: (page) => slugify(page.properties.get("Scope")), + frontmatterBuilder: (page) => ({ + id: page.meta.id, + url: page.meta.url, + title: page.meta.title, + cluster: page.properties.get("Cluster") + }), }, + views: [ + { + title: "By Scope", + properties: { + groupBy: "Scope", + include: ["Name", "Scope", "Cluster", "Summary"], + }, + }, }, }, }; @@ -147,10 +166,20 @@ async function main() { ); } - await sync(notionApiToken, config); + rimraf.sync("docs/!(README.md)**/*"); + + // change into the docs dir, this simplifies handling relative paths + process.chdir("docs/"); + + const rendered = await sync(notionApiToken, config); + + // do something with the rendered index, e.g. writing it to a file or building a nav structure } -main(); +main().catch((e) => { + console.error(e); + process.exit(1); +}); ``` ## Credits, Related Projects and Inspiration From 3c1537ea5d2af9c59ca9e31bf2039a1c4573deef Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Feb 2022 17:17:52 +0100 Subject: [PATCH 14/29] feat: support rendering table views --- README.md | 14 +++--- src/ChildDatabaseRenderer.ts | 20 ++++----- src/DatabaseEntryRenderer.ts | 8 +--- src/DatabasePageRenderer.ts | 1 - src/DatabaseTableRenderer.ts | 32 -------------- src/DatabaseViewRenderer.ts | 78 ++++++++++++++++++++++------------ src/DeferredRenderer.ts | 4 +- src/LinkRenderer.spec.ts | 1 - src/RenderDatabaseEntryTask.ts | 2 - src/RenderDatabasePageTask.ts | 10 ++--- src/SyncConfig.ts | 17 ++++++-- src/sync.ts | 3 -- 12 files changed, 91 insertions(+), 99 deletions(-) delete mode 100644 src/DatabaseTableRenderer.ts diff --git a/README.md b/README.md index 041150a..041eada 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,14 @@ const config: SyncConfig = { cluster: page.properties.get("Cluster") }), }, - views: [ - { - title: "By Scope", - properties: { - groupBy: "Scope", - include: ["Name", "Scope", "Cluster", "Summary"], - }, + views: [ + { + title: "By Scope", + properties: { + groupBy: "Scope", + include: ["Name", "Scope", "Cluster", "Summary"], }, + }, }, }, }; diff --git a/src/ChildDatabaseRenderer.ts b/src/ChildDatabaseRenderer.ts index bc91a12..16ce4c8 100644 --- a/src/ChildDatabaseRenderer.ts +++ b/src/ChildDatabaseRenderer.ts @@ -3,7 +3,6 @@ import { Page } from '@notionhq/client/build/src/api-types'; import { SyncConfig } from './'; import { lookupDatabaseConfig } from './config'; import { Database } from './Database'; -import { DatabaseTableRenderer } from './DatabaseTableRenderer'; import { DatabaseViewRenderer } from './DatabaseViewRenderer'; import { DeferredRenderer } from './DeferredRenderer'; import { NotionApiFacade } from './NotionApiFacade'; @@ -17,7 +16,6 @@ export class ChildDatabaseRenderer { private readonly config: SyncConfig, private readonly publicApi: NotionApiFacade, private readonly deferredRenderer: DeferredRenderer, - private readonly tableRenderer: DatabaseTableRenderer, private readonly viewRenderer: DatabaseViewRenderer ) { } @@ -36,6 +34,16 @@ export class ChildDatabaseRenderer { const entries = await this.queuePageRendering(allPages, pageConfig); const markdown = await this.viewRenderer.renderViews(entries, dbConfig as DatabaseConfigRenderPages); + return { + config: dbConfig, + entries, + markdown, + }; + } else { + // render table + const entries = await this.queueEntryRendering(allPages, dbConfig); + const markdown = this.viewRenderer.renderViews(entries, dbConfig); + return { config: dbConfig, entries, @@ -43,14 +51,6 @@ export class ChildDatabaseRenderer { }; } - const entries = await this.queueEntryRendering(allPages, dbConfig); - const markdown = this.tableRenderer.renderTable(entries); - - return { - config: dbConfig, - entries, - markdown, - }; } private async queueEntryRendering( diff --git a/src/DatabaseEntryRenderer.ts b/src/DatabaseEntryRenderer.ts index 42451f6..d271ad5 100644 --- a/src/DatabaseEntryRenderer.ts +++ b/src/DatabaseEntryRenderer.ts @@ -6,15 +6,11 @@ import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; export class DatabaseEntryRenderer { constructor(private readonly propertiesParser: PropertiesParser) {} - async renderEntry( - page: Page - ): Promise { + async renderEntry(page: Page): Promise { const props = await this.propertiesParser.parsePageProperties(page); return { - id: page.id, - url: page.url, - properties: props + properties: props, }; } } diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index b98034f..94b636e 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -36,7 +36,6 @@ export class DatabasePageRenderer { // performance probably not the bottleneck compared to the IO cost of notion API invocations. return { - id: page.id, file, properties: props, render: async () => { diff --git a/src/DatabaseTableRenderer.ts b/src/DatabaseTableRenderer.ts deleted file mode 100644 index 71a5d7e..0000000 --- a/src/DatabaseTableRenderer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as markdownTable from "./markdown-table"; -import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; - -export class DatabaseTableRenderer { - public renderTable(entries: RenderDatabaseEntryTask[]): string { - const table: any[][] = []; - - for (const entry of entries) { - if (table.length === 0) { - const headers = Array.from(entry.properties.properties.keys()); - table[0] = headers; - } - - const cols = Array.from(entry.properties.properties.values()).map((c, i) => - DatabaseTableRenderer.escapeTableCell(c) - ); - - table.push(cols); - } - - return markdownTable.markdownTable(table); - } - - static escapeTableCell(content: string | number | any): string { - // markdown table cells do not support newlines, however we can insert
elements instead - if (typeof content === "string") { - return content.replace(/\n/g, "
"); - } - - return content?.toString() || ""; - } -} diff --git a/src/DatabaseViewRenderer.ts b/src/DatabaseViewRenderer.ts index 9c2a00f..470cb0b 100644 --- a/src/DatabaseViewRenderer.ts +++ b/src/DatabaseViewRenderer.ts @@ -1,6 +1,7 @@ -import { DatabaseTableRenderer } from "./DatabaseTableRenderer"; +import { DatabaseConfigRenderTable } from "."; import { LinkRenderer } from "./LinkRenderer"; import * as markdownTable from "./markdown-table"; +import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; import { RenderDatabasePageTask } from "./RenderDatabasePageTask"; import { DatabaseConfigRenderPages, DatabaseView } from "./SyncConfig"; @@ -9,34 +10,44 @@ export class DatabaseViewRenderer { constructor(private readonly linkRenderer: LinkRenderer) {} public renderViews( - entries: RenderDatabasePageTask[], - config: DatabaseConfigRenderPages + entries: (RenderDatabasePageTask | RenderDatabaseEntryTask)[], + config: DatabaseConfigRenderPages | DatabaseConfigRenderTable ): string { - const views = config.views?.map((view) => { - const grouped = new Array( - ...groupBy(entries, (p) => - p.properties.properties.get(view.properties.groupBy) - ) - ); - - return grouped - .map(([key, pages]) => this.renderView(pages, key, view)) - .join("\n\n"); + const configuredViews = config.views || [{}]; + + const views = configuredViews?.map((view) => { + const groupByProperty = view?.properties?.groupBy; + + if (!groupByProperty) { + return this.renderView(entries, null, view); + } else { + const grouped = new Array( + ...groupBy(entries, (p) => + p.properties.properties.get(groupByProperty) + ) + ); + + return grouped + .map(([key, pages]) => this.renderView(pages, key, view)) + .join("\n\n"); + } }); return views?.join("\n\n") || ""; } - public renderView( - pages: RenderDatabasePageTask[], - titleAppendix: string, + private renderView( + pages: (RenderDatabasePageTask | RenderDatabaseEntryTask)[], + titleAppendix: string | null, view: DatabaseView ): string { - // todo: handle empty page + if (!pages[0]) { + return ""; + } const pageProps = pages[0].properties; const includedProps = - view.properties.include || Array.from(pageProps.properties.keys()); + view?.properties?.include || Array.from(pageProps.properties.keys()); const table: any[][] = []; @@ -47,20 +58,20 @@ export class DatabaseViewRenderer { pages.forEach((r) => table.push( cols.map((c, i) => { - const content = DatabaseTableRenderer.escapeTableCell( - r.properties.properties.get(c) - ); - return i == 0 + const content = escapeTableCell(r.properties.properties.get(c)); + return i == 0 && isRenderPageTask(r) ? this.linkRenderer.renderPageLink(content, r) // make the first cell a relative link to the page : content; }) ) ); - return ( - `## ${view.title} - ${titleAppendix}\n\n` + - markdownTable.markdownTable(table) - ); + const tableMd = markdownTable.markdownTable(table); + if (view.title) { + return `## ${view.title} - ${titleAppendix}\n\n` + tableMd; + } else { + return tableMd; + } } } @@ -91,3 +102,18 @@ export function groupBy( }); return map; } + +function escapeTableCell(content: string | number | any): string { + // markdown table cells do not support newlines, however we can insert
elements instead + if (typeof content === "string") { + return content.replace(/\n/g, "
"); + } + + return content?.toString() || ""; +} + +function isRenderPageTask( + task: RenderDatabasePageTask | RenderDatabaseEntryTask +): task is RenderDatabasePageTask { + return (task as RenderDatabasePageTask).render !== undefined; +} diff --git a/src/DeferredRenderer.ts b/src/DeferredRenderer.ts index 63f634b..5be901b 100644 --- a/src/DeferredRenderer.ts +++ b/src/DeferredRenderer.ts @@ -108,8 +108,8 @@ export class DeferredRenderer { const entries: RenderedDatabaseEntry[] = this.renderedEntries.map((x) => ({ meta: { - id: x.id, - url: x.url, + id: x.properties.meta.id, + url: x.properties.meta.url, }, properties: x.properties.properties, })); diff --git a/src/LinkRenderer.spec.ts b/src/LinkRenderer.spec.ts index 6968d9b..7bcd2b1 100644 --- a/src/LinkRenderer.spec.ts +++ b/src/LinkRenderer.spec.ts @@ -5,7 +5,6 @@ describe("LinkRenderer", () => { test("renderPageLink strips outDir from link", async () => { const sut = new LinkRenderer(); const page: Partial = { - id: "id", file: "out/test.md", }; diff --git a/src/RenderDatabaseEntryTask.ts b/src/RenderDatabaseEntryTask.ts index 2c88143..3604b34 100644 --- a/src/RenderDatabaseEntryTask.ts +++ b/src/RenderDatabaseEntryTask.ts @@ -1,8 +1,6 @@ import { DatabasePageProperties } from "./DatabasePageProperties"; export interface RenderDatabaseEntryTask { - id: string; - url: string; properties: DatabasePageProperties // note: there's nothing to do to render an individual database entry, they are always rendered as part of tables } diff --git a/src/RenderDatabasePageTask.ts b/src/RenderDatabasePageTask.ts index a39b5b3..72620e7 100644 --- a/src/RenderDatabasePageTask.ts +++ b/src/RenderDatabasePageTask.ts @@ -1,8 +1,6 @@ -import { DatabasePageProperties } from './DatabasePageProperties'; +import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; -export interface RenderDatabasePageTask { - id: string; - file: string; - properties: DatabasePageProperties; - render: () => Promise; +export interface RenderDatabasePageTask extends RenderDatabaseEntryTask { + file: string; + render: () => Promise; } diff --git a/src/SyncConfig.ts b/src/SyncConfig.ts index 1b1fd30..3a7a8d1 100644 --- a/src/SyncConfig.ts +++ b/src/SyncConfig.ts @@ -42,6 +42,16 @@ export interface DatabaseConfigBase { export interface DatabaseConfigRenderTable extends DatabaseConfigBase { renderAs: "table"; + /** + * Customize rendering of the table as one or multiple views. + * + * If not defined, will render a single view of the table with the Notion database property marked "title" + * as the first column. + * + * An empty array will supress rendering of the table (useful if you want to only emit the table to the index). + */ + views?: DatabaseView[]; + entries: { /** * Controls whether to emit database entries to the index of rendered pages/entries @@ -94,10 +104,11 @@ export interface DatabaseConfigRenderPages extends DatabaseConfigBase { */ views: DatabaseView[]; } + export interface DatabaseView { - title: string; - properties: { - groupBy: string; + title?: string; + properties?: { + groupBy?: string; include?: string[]; }; } diff --git a/src/sync.ts b/src/sync.ts index bb6c944..968038d 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -2,7 +2,6 @@ import { BlockRenderer } from './BlockRenderer'; import { ChildDatabaseRenderer } from './ChildDatabaseRenderer'; import { DatabaseEntryRenderer } from './DatabaseEntryRenderer'; import { DatabasePageRenderer } from './DatabasePageRenderer'; -import { DatabaseTableRenderer } from './DatabaseTableRenderer'; import { DatabaseViewRenderer } from './DatabaseViewRenderer'; import { DeferredRenderer } from './DeferredRenderer'; import { FrontmatterRenderer } from './FrontmatterRenderer'; @@ -20,7 +19,6 @@ export async function sync(notionApiToken: string, config: SyncConfig) { const deferredRenderer = new DeferredRenderer(); const frontmatterRenderer = new FrontmatterRenderer(); - const tableRenderer = new DatabaseTableRenderer(); const mentionedPageRenderer = new MentionedPageRenderer( publicApi, deferredRenderer, @@ -46,7 +44,6 @@ export async function sync(notionApiToken: string, config: SyncConfig) { config, publicApi, deferredRenderer, - tableRenderer, viewRenderer ); From a37e8405e397b1502e42c7133268a653ff52089d Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Feb 2022 20:57:16 +0100 Subject: [PATCH 15/29] feat: add tests for rendering table views --- jest.config.js | 6 ++ src/DatabaseViewRenderer.spec.ts | 94 ++++++++++++++++++++++++++++++++ src/DatabaseViewRenderer.ts | 3 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/DatabaseViewRenderer.spec.ts diff --git a/jest.config.js b/jest.config.js index bd27b33..693577e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,12 @@ module.exports = { transform: { "^.+\\.ts?$": "ts-jest", + "^.+\\markdown-table.js?$": "ts-jest", + }, + globals: { + 'ts-jest': { + isolatedModules: true + } }, testEnvironment: "node", testRegex: "./src/.*\\.(test|spec)?\\.(ts|ts)$", diff --git a/src/DatabaseViewRenderer.spec.ts b/src/DatabaseViewRenderer.spec.ts new file mode 100644 index 0000000..7281e1c --- /dev/null +++ b/src/DatabaseViewRenderer.spec.ts @@ -0,0 +1,94 @@ +import { DatabaseViewRenderer } from "./DatabaseViewRenderer"; +import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; + +describe("DatabaseViewRenderer", () => { + test("renders with grouping", async () => { + const linkRenderer = {}; + const sut = new DatabaseViewRenderer(linkRenderer as any); + + const entries: RenderDatabaseEntryTask[] = [ + { + properties: { + meta: { id: "a", title: " A", url: "http://a" }, + properties: new Map([ + ["Name", "A"], + ["Foo", "Bar"], + ]), + }, + }, + { + properties: { + meta: { id: "b", title: " B", url: "http://b" }, + properties: new Map([ + ["Name", "B"], + ["Foo", "Baz"], + ]), + }, + }, + ]; + + const result = sut.renderViews(entries, { + entries: { emitToIndex: false }, + renderAs: "table", + views: [ + { + title: "By Foo", + properties: { + groupBy: "Foo", + }, + }, + ], + }); + + const expected = `## By Foo - Bar + +| Name | Foo | +| ---- | --- | +| A | Bar | + +## By Foo - Baz + +| Name | Foo | +| ---- | --- | +| B | Baz |`; + expect(result).toEqual(expected); + }); + + test("filters columns", async () => { + const linkRenderer = {}; + const sut = new DatabaseViewRenderer(linkRenderer as any); + + const entries: RenderDatabaseEntryTask[] = [ + { + properties: { + meta: { id: "a", title: " A", url: "http://a" }, + properties: new Map([ + ["Name", "A"], + ["Foo", "Bar"], + ["Alice", "Bob"], + ]), + }, + }, + ]; + + const result = sut.renderViews(entries, { + entries: { emitToIndex: false }, + renderAs: "table", + views: [ + { + title: "By Foo", + properties: { + include: ["Name", "Foo"], + }, + }, + ], + }); + + const expected = `## By Foo + +| Name | Foo | +| ---- | --- | +| A | Bar |`; + expect(result).toEqual(expected); + }); +}); diff --git a/src/DatabaseViewRenderer.ts b/src/DatabaseViewRenderer.ts index 470cb0b..5736c87 100644 --- a/src/DatabaseViewRenderer.ts +++ b/src/DatabaseViewRenderer.ts @@ -68,7 +68,8 @@ export class DatabaseViewRenderer { const tableMd = markdownTable.markdownTable(table); if (view.title) { - return `## ${view.title} - ${titleAppendix}\n\n` + tableMd; + const formattedTitle = [view.title, titleAppendix].filter(x => !!x).join(" - "); + return `## ${formattedTitle}\n\n` + tableMd; } else { return tableMd; } From 78575ae0f88a19ae74b0378fa31b6d2afaaaa0d0 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Feb 2022 23:51:05 +0100 Subject: [PATCH 16/29] feat: publish frontmatter in the sync() result this makes it easier for consumers to write index files containing the actual frontmatter data which was built from the frontmatterBuilder functions also align the frontmatter building for table entries so that it works the same as for pages --- src/DatabaseEntryRenderer.ts | 8 +++++++- src/DatabasePageRenderer.ts | 7 +++---- src/DatabaseViewRenderer.spec.ts | 2 -- src/DeferredRenderer.ts | 8 ++++---- src/FrontmatterRenderer.ts | 7 +------ src/RenderDatabaseEntryTask.ts | 1 + src/RenderDatabasePageTask.ts | 1 + src/RenderedDatabaseEntry.ts | 2 +- src/RenderedDatabasePage.ts | 1 + src/SyncConfig.ts | 7 ++++--- src/config.ts | 3 --- src/index.ts | 18 +++++++++++------- 12 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/DatabaseEntryRenderer.ts b/src/DatabaseEntryRenderer.ts index d271ad5..0943080 100644 --- a/src/DatabaseEntryRenderer.ts +++ b/src/DatabaseEntryRenderer.ts @@ -1,4 +1,5 @@ import { Page } from "@notionhq/client/build/src/api-types"; +import { DatabaseConfigRenderTable } from "."; import { PropertiesParser } from "./PropertiesParser"; import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; @@ -6,11 +7,16 @@ import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; export class DatabaseEntryRenderer { constructor(private readonly propertiesParser: PropertiesParser) {} - async renderEntry(page: Page): Promise { + async renderEntry( + page: Page, + config: DatabaseConfigRenderTable + ): Promise { const props = await this.propertiesParser.parsePageProperties(page); + const frontmatterProperties = config.entries?.frontmatterBuilder(props); return { properties: props, + frontmatter: frontmatterProperties, }; } } diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index 94b636e..fe15bc5 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -34,9 +34,11 @@ export class DatabasePageRenderer { // Design: all the rendering performance could be greatly enhanced writing directly to output streams instead // of concatenating all in memory. OTOH naively concatenatic strings is straightforward, easier to debug and rendering // performance probably not the bottleneck compared to the IO cost of notion API invocations. + const frontmatterProperties = config.pages.frontmatterBuilder(props); return { file, + frontmatter: frontmatterProperties, properties: props, render: async () => { const context = new RenderingLoggingContext(page.url, file); @@ -51,10 +53,7 @@ export class DatabasePageRenderer { try { const assetWriter = new AssetWriter(destDir); - const frontmatter = this.frontmatterRenderer.renderFrontmatter( - props, - config - ); + const frontmatter = this.frontmatterRenderer.renderFrontmatter(frontmatterProperties); const body = await this.bodyRenderer.renderBody( page, assetWriter, diff --git a/src/DatabaseViewRenderer.spec.ts b/src/DatabaseViewRenderer.spec.ts index 7281e1c..be7f26b 100644 --- a/src/DatabaseViewRenderer.spec.ts +++ b/src/DatabaseViewRenderer.spec.ts @@ -28,7 +28,6 @@ describe("DatabaseViewRenderer", () => { ]; const result = sut.renderViews(entries, { - entries: { emitToIndex: false }, renderAs: "table", views: [ { @@ -72,7 +71,6 @@ describe("DatabaseViewRenderer", () => { ]; const result = sut.renderViews(entries, { - entries: { emitToIndex: false }, renderAs: "table", views: [ { diff --git a/src/DeferredRenderer.ts b/src/DeferredRenderer.ts index 5be901b..75756b6 100644 --- a/src/DeferredRenderer.ts +++ b/src/DeferredRenderer.ts @@ -62,11 +62,11 @@ export class DeferredRenderer { page: Page, config: DatabaseConfigRenderTable ): Promise { - const task = await this.entryRenderer.renderEntry(page); + const task = await this.entryRenderer.renderEntry(page, config); // entries are complete the moment they are retrieved, there's no more deferred processing necessary on them // also there should be no duplicate entries, so we do not cache/lookup any of them - if (config.entries.emitToIndex) { + if (task.frontmatter) { this.renderedEntries.push(task); } @@ -103,7 +103,7 @@ export class DeferredRenderer { ).map((x) => ({ file: x.file, meta: x.properties.meta, - properties: x.properties.properties, + frontmatter: x.frontmatter, })); const entries: RenderedDatabaseEntry[] = this.renderedEntries.map((x) => ({ @@ -111,7 +111,7 @@ export class DeferredRenderer { id: x.properties.meta.id, url: x.properties.meta.url, }, - properties: x.properties.properties, + frontmatter: x.frontmatter, })); return pages.concat(entries); diff --git a/src/FrontmatterRenderer.ts b/src/FrontmatterRenderer.ts index d43444d..008c4d3 100644 --- a/src/FrontmatterRenderer.ts +++ b/src/FrontmatterRenderer.ts @@ -1,14 +1,9 @@ import * as yaml from 'js-yaml'; -import { DatabaseConfigRenderPages } from '.'; - -import { DatabasePageProperties } from './DatabasePageProperties'; export class FrontmatterRenderer { constructor() {} - public renderFrontmatter(props: DatabasePageProperties, config: DatabaseConfigRenderPages) { - const obj = config.pages.frontmatterBuilder(props); - + public renderFrontmatter(obj: Record) { const frontmatter = `---\n${yaml.dump(obj)}---\n\n`; return frontmatter; diff --git a/src/RenderDatabaseEntryTask.ts b/src/RenderDatabaseEntryTask.ts index 3604b34..23e5b87 100644 --- a/src/RenderDatabaseEntryTask.ts +++ b/src/RenderDatabaseEntryTask.ts @@ -2,5 +2,6 @@ import { DatabasePageProperties } from "./DatabasePageProperties"; export interface RenderDatabaseEntryTask { properties: DatabasePageProperties + frontmatter?: Record // note: there's nothing to do to render an individual database entry, they are always rendered as part of tables } diff --git a/src/RenderDatabasePageTask.ts b/src/RenderDatabasePageTask.ts index 72620e7..cda9757 100644 --- a/src/RenderDatabasePageTask.ts +++ b/src/RenderDatabasePageTask.ts @@ -2,5 +2,6 @@ import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; export interface RenderDatabasePageTask extends RenderDatabaseEntryTask { file: string; + frontmatter: Record render: () => Promise; } diff --git a/src/RenderedDatabaseEntry.ts b/src/RenderedDatabaseEntry.ts index 7b1dcf8..eda71ab 100644 --- a/src/RenderedDatabaseEntry.ts +++ b/src/RenderedDatabaseEntry.ts @@ -2,5 +2,5 @@ import { DatabaseEntryMeta } from './DatabaseEntryMeta'; export interface RenderedDatabaseEntry { meta: DatabaseEntryMeta, - properties: Map; + frontmatter?: Record; } diff --git a/src/RenderedDatabasePage.ts b/src/RenderedDatabasePage.ts index b4acb82..671f5ea 100644 --- a/src/RenderedDatabasePage.ts +++ b/src/RenderedDatabasePage.ts @@ -4,4 +4,5 @@ import { RenderedDatabaseEntry } from './RenderedDatabaseEntry'; export interface RenderedDatabasePage extends RenderedDatabaseEntry { meta: DatabasePageMeta; file: string; + frontmatter: Record; } diff --git a/src/SyncConfig.ts b/src/SyncConfig.ts index 3a7a8d1..5f96d5e 100644 --- a/src/SyncConfig.ts +++ b/src/SyncConfig.ts @@ -52,11 +52,12 @@ export interface DatabaseConfigRenderTable extends DatabaseConfigBase { */ views?: DatabaseView[]; - entries: { + entries?: { /** - * Controls whether to emit database entries to the index of rendered pages/entries + * Optional: Build frontmatter onject for index entries. + * If omitted, no index entries will be rendered for this table */ - emitToIndex: boolean; + frontmatterBuilder: (props: DatabasePageProperties) => Record; }; } diff --git a/src/config.ts b/src/config.ts index 1de2dfa..31f2809 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,9 +16,6 @@ export function lookupDatabaseConfig( }; const defaultDbConfig: DatabaseConfigRenderTable = { renderAs: "table", - entries: { - emitToIndex: false, - }, }; const fallbackDbConfig: DatabaseConfig = diff --git a/src/index.ts b/src/index.ts index 2e3ad18..3469419 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,12 @@ -export { RenderedDatabaseEntry } from './RenderedDatabaseEntry'; -export { RenderedDatabasePage } from './RenderedDatabasePage'; -export { sync } from './sync'; -export { slugify } from './slugify'; +export { RenderedDatabaseEntry } from "./RenderedDatabaseEntry"; +export { RenderedDatabasePage } from "./RenderedDatabasePage"; +export { sync } from "./sync"; +export { slugify } from "./slugify"; export { - DatabaseConfig, DatabaseConfigRenderPages, DatabaseConfigRenderTable, SyncConfig -} from './SyncConfig'; - + DatabaseConfig, + DatabaseConfigRenderPages, + DatabaseConfigRenderTable, + SyncConfig, +} from "./SyncConfig"; +export { DatabasePageProperties } from "./DatabasePageProperties"; +export { DatabasePageMeta } from "./DatabasePageMeta"; From 9e162cb6cde8726fbe917c0768ac6f448e5249ed Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Fri, 25 Feb 2022 09:56:28 +0100 Subject: [PATCH 17/29] chore: bump to v0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 005b7c0..d4d0bc7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.1", + "version": "0.9.0", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From 864c3356b6536288e9aec0c1fa221812b6ed3d6a Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Mar 2022 23:06:54 +0100 Subject: [PATCH 18/29] feat: render links as relative paths We previously rendered all page links as absolute paths e.g. `/page.md`. This created some problems - some page generators that use base path prefixes like `/docs/page.md` and thus need to rewrite these links, which is brittle - text editors like VSCode cannot follow the markdown links properly because they think the links are absolute to the root `/` of the fs - tools like textlint-rule-no-dead-link expect relative links to resolve them correctly Now rendering them as relative paths. As part of these changes it was also necessary to switch properties parsing to render properties only as plaintext, not markdown. This is fine because we consider frontmatter to be data (YML), not markup. Most site generators cannot deal with markdown in frontmatter anyway. This change was necessary because we need to parse page properties before we can determine the destination path of the markdown file where the page will be rendered to, and that would now require resolving relative links. --- README.md | 2 +- src/AssetWriter.ts | 24 ++++++++-------- src/BlockRenderer.ts | 43 +++++++++++++++------------- src/ChildDatabaseRenderer.ts | 53 +++++++++++++++++++++++------------ src/DatabasePageRenderer.ts | 17 +++++------ src/DatabaseViewRenderer.ts | 17 +++++++---- src/DeferredRenderer.ts | 8 ++++-- src/LinkRenderer.ts | 15 ++++++---- src/PageLinkResolver.ts | 19 +++++++++++++ src/PropertiesParser.ts | 10 +++---- src/RecursiveBodyRenderer.ts | 14 ++++----- src/RenderingContext.ts | 27 ++++++++++++++++++ src/RenderingContextLogger.ts | 34 ++++++++++++++++++++++ src/RichTextRenderer.ts | 21 +++++++------- src/logger.ts | 31 +------------------- src/sync.ts | 4 ++- 16 files changed, 210 insertions(+), 129 deletions(-) create mode 100644 src/PageLinkResolver.ts create mode 100644 src/RenderingContext.ts create mode 100644 src/RenderingContextLogger.ts diff --git a/README.md b/README.md index 041eada..c37e822 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ The following [Notion API page property types](https://developers.notion.com/ref | Propety type | Supported | Notes | | ---------------- | --------- | ----------------------------- | -| Rich text | ✅ Yes | rendered as markdown string | +| Rich text | ✅ Yes | rendered as plaintext string | | Number | ✅ Yes | | | Select | ✅ Yes | rendered as name | | Multi Select | ✅ Yes | rendered as array of names | diff --git a/src/AssetWriter.ts b/src/AssetWriter.ts index 6d01ff5..d9dcdf9 100644 --- a/src/AssetWriter.ts +++ b/src/AssetWriter.ts @@ -1,27 +1,25 @@ -import { promises as fs } from 'fs'; -import got from 'got'; -import { KeyvFile } from 'keyv-file'; -import * as mime from 'mime-types'; - -import { RenderingLoggingContext } from './logger'; +import { promises as fs } from "fs"; +import got from "got"; +import { KeyvFile } from "keyv-file"; +import * as mime from "mime-types"; +import { RenderingContextLogger } from "./RenderingContextLogger"; const cache = new KeyvFile({ filename: ".cache/keyv.json", }); export class AssetWriter { - constructor(readonly dir: string) {} + constructor( + private readonly dir: string, + private readonly logger: RenderingContextLogger + ) {} async store(name: string, buffer: Buffer) { await fs.mkdir(this.dir, { recursive: true }); await fs.writeFile(`${this.dir}/${name}`, buffer); } - async download( - url: string, - fileName: string, - context: RenderingLoggingContext - ) { + async download(url: string, fileName: string) { // the got http lib promises to do proper user-agent compliant http caching // see https://github.com/sindresorhus/got/blob/main/documentation/cache.md @@ -35,7 +33,7 @@ export class AssetWriter { const imageFile = fileName + "." + ext; const cacheInfo = response.isFromCache ? " (from cache)" : ""; - context.info(`downloading ${imageFile}` + cacheInfo); + this.logger.info(`downloading ${imageFile}` + cacheInfo); await this.store(imageFile, response.rawBody); return imageFile; diff --git a/src/BlockRenderer.ts b/src/BlockRenderer.ts index c273765..54ffe9b 100644 --- a/src/BlockRenderer.ts +++ b/src/BlockRenderer.ts @@ -1,12 +1,19 @@ -import { RichText } from '@notionhq/client/build/src/api-types'; +import { RichText } from "@notionhq/client/build/src/api-types"; -import { AssetWriter } from './AssetWriter'; +import { AssetWriter } from "./AssetWriter"; import { - Block, Emoji, ExternalFile, ExternalFileWithCaption, File, FileWithCaption, ImageBlock -} from './Blocks'; -import { DeferredRenderer } from './DeferredRenderer'; -import { RenderingLoggingContext } from './logger'; -import { RichTextRenderer } from './RichTextRenderer'; + Block, + Emoji, + ExternalFile, + ExternalFileWithCaption, + File, + FileWithCaption, + ImageBlock, +} from "./Blocks"; +import { DeferredRenderer } from "./DeferredRenderer"; +import { RenderingContextLogger } from "./RenderingContextLogger"; +import { RichTextRenderer } from "./RichTextRenderer"; +import { RenderingContext } from "./RenderingContext"; const debug = require("debug")("blocks"); @@ -22,8 +29,7 @@ export class BlockRenderer { async renderBlock( block: Block, - assets: AssetWriter, - context: RenderingLoggingContext + context: RenderingContext ): Promise { const renderMarkdown = async (text: RichText[]) => { return await this.richText.renderMarkdown(text, context); @@ -63,7 +69,7 @@ export class BlockRenderer { }; case "image": return { - lines: await this.renderImage(block, assets, context), + lines: await this.renderImage(block, context.assetWriter), }; case "quote": { // it's legal for a notion block to be cmoposed of multiple lines @@ -85,7 +91,7 @@ export class BlockRenderer { case "callout": { // render emoji as bold, this enables css to target it as `blockquote > strong:first-child` const content = - `**${this.renderIcon(block.callout.icon, context)}** ` + + `**${this.renderIcon(block.callout.icon, context.logger)}** ` + (await renderMarkdown(block.callout.text)); return { @@ -96,7 +102,7 @@ export class BlockRenderer { return { lines: "---" }; case "child_database": const msg = `\n`; - const db = await this.deferredRenderer.renderChildDatabase(block.id); + const db = await this.deferredRenderer.renderChildDatabase(block.id, context.linkResolver); return { lines: msg + db.markdown }; case "synced_block": // nothing to render, only the contents of the synced block are relevant @@ -116,7 +122,7 @@ export class BlockRenderer { lines: this.renderUnsupported( `unsupported block type: ${block.type}`, block, - context + context.logger ), }; } @@ -124,7 +130,7 @@ export class BlockRenderer { private renderIcon( icon: File | ExternalFile | Emoji, - context: RenderingLoggingContext + logger: RenderingContextLogger ): string { switch (icon.type) { case "emoji": @@ -134,19 +140,18 @@ export class BlockRenderer { return this.renderUnsupported( `unsupported icon type: ${icon.type}`, icon, - context + logger ); } } async renderImage( block: ImageBlock, - assets: AssetWriter, - context: RenderingLoggingContext + assets: AssetWriter ): Promise { const url = this.parseUrl(block.image); - const imageFile = await assets.download(url, block.id, context); + const imageFile = await assets.download(url, block.id); // todo: caption support const markdown = `![image-${block.id}](./${imageFile})`; @@ -172,7 +177,7 @@ export class BlockRenderer { private renderUnsupported( msg: string, obj: any, - context: RenderingLoggingContext + context: RenderingContextLogger ): string { context.warn(msg); debug(msg + "\n%O", obj); diff --git a/src/ChildDatabaseRenderer.ts b/src/ChildDatabaseRenderer.ts index 16ce4c8..5273eca 100644 --- a/src/ChildDatabaseRenderer.ts +++ b/src/ChildDatabaseRenderer.ts @@ -1,13 +1,18 @@ -import { Page } from '@notionhq/client/build/src/api-types'; - -import { SyncConfig } from './'; -import { lookupDatabaseConfig } from './config'; -import { Database } from './Database'; -import { DatabaseViewRenderer } from './DatabaseViewRenderer'; -import { DeferredRenderer } from './DeferredRenderer'; -import { NotionApiFacade } from './NotionApiFacade'; -import { RenderDatabasePageTask } from './RenderDatabasePageTask'; -import { DatabaseConfig, DatabaseConfigRenderPages, DatabaseConfigRenderTable } from './SyncConfig'; +import { Page } from "@notionhq/client/build/src/api-types"; + +import { SyncConfig } from "./"; +import { lookupDatabaseConfig } from "./config"; +import { Database } from "./Database"; +import { DatabaseViewRenderer } from "./DatabaseViewRenderer"; +import { DeferredRenderer } from "./DeferredRenderer"; +import { NotionApiFacade } from "./NotionApiFacade"; +import { PageLinkResolver } from "./PageLinkResolver"; +import { RenderDatabasePageTask } from "./RenderDatabasePageTask"; +import { + DatabaseConfig, + DatabaseConfigRenderPages, + DatabaseConfigRenderTable, +} from "./SyncConfig"; const debug = require("debug")("child-database"); @@ -17,22 +22,31 @@ export class ChildDatabaseRenderer { private readonly publicApi: NotionApiFacade, private readonly deferredRenderer: DeferredRenderer, private readonly viewRenderer: DatabaseViewRenderer - ) { } + ) {} - async renderChildDatabase(databaseId: string): Promise { + async renderChildDatabase( + databaseId: string, + linkResolver: PageLinkResolver + ): Promise { const dbConfig = lookupDatabaseConfig(this.config, databaseId); // no view was defined for this database, render as a plain inline table const allPages = await this.fetchPages(databaseId, dbConfig); - const renderPages = dbConfig.renderAs === "pages+views" + const renderPages = dbConfig.renderAs === "pages+views"; - debug("rendering child database " + databaseId + " as " + dbConfig.renderAs); + debug( + "rendering child database " + databaseId + " as " + dbConfig.renderAs + ); if (renderPages) { const pageConfig = dbConfig as DatabaseConfigRenderPages; const entries = await this.queuePageRendering(allPages, pageConfig); - const markdown = await this.viewRenderer.renderViews(entries, dbConfig as DatabaseConfigRenderPages); + const markdown = await this.viewRenderer.renderViews( + entries, + dbConfig as DatabaseConfigRenderPages, + linkResolver + ); return { config: dbConfig, @@ -42,15 +56,18 @@ export class ChildDatabaseRenderer { } else { // render table const entries = await this.queueEntryRendering(allPages, dbConfig); - const markdown = this.viewRenderer.renderViews(entries, dbConfig); - + const markdown = this.viewRenderer.renderViews( + entries, + dbConfig, + linkResolver + ); + return { config: dbConfig, entries, markdown, }; } - } private async queueEntryRendering( diff --git a/src/DatabasePageRenderer.ts b/src/DatabasePageRenderer.ts index fe15bc5..d3ea572 100644 --- a/src/DatabasePageRenderer.ts +++ b/src/DatabasePageRenderer.ts @@ -2,14 +2,13 @@ import * as fsc from "fs"; import { Page } from "@notionhq/client/build/src/api-types"; -import { AssetWriter } from "./AssetWriter"; import { FrontmatterRenderer } from "./FrontmatterRenderer"; -import { RenderingLoggingContext } from "./logger"; import { PropertiesParser } from "./PropertiesParser"; import { RecursiveBodyRenderer } from "./RecursiveBodyRenderer"; import { RenderDatabasePageTask as RenderDatabasePageTask } from "./RenderDatabasePageTask"; import { DatabaseConfigRenderPages } from "./SyncConfig"; import { slugify } from "./slugify"; +import { RenderingContext } from "./RenderingContext"; const fs = fsc.promises; @@ -41,35 +40,33 @@ export class DatabasePageRenderer { frontmatter: frontmatterProperties, properties: props, render: async () => { - const context = new RenderingLoggingContext(page.url, file); - + + const context = new RenderingContext(page.url, file) + if (page.archived) { // have to skip rendering archived pages as attempting to retrieve the block will result in a HTTP 404 - context.warn(`page is archived - skipping`); + context.logger.warn(`page is archived - skipping`); return; } try { - const assetWriter = new AssetWriter(destDir); - const frontmatter = this.frontmatterRenderer.renderFrontmatter(frontmatterProperties); const body = await this.bodyRenderer.renderBody( page, - assetWriter, context ); await fs.mkdir(destDir, { recursive: true }); await fs.writeFile(file, frontmatter + body); - context.complete(); + context.logger.complete(); } catch (error) { // While catch-log-throw is usually an antipattern, it is the renderes job to orchestrate the rendering // job with concerns like logging and writing to the outside world. Hence this place is appropriate. // We need to throw the error here so that the rendering process can crash with a proper error message, since // an error at this point here is unrecoverable. - context.error(error); + context.logger.error(error); throw error; } }, diff --git a/src/DatabaseViewRenderer.ts b/src/DatabaseViewRenderer.ts index 5736c87..16cc20e 100644 --- a/src/DatabaseViewRenderer.ts +++ b/src/DatabaseViewRenderer.ts @@ -1,6 +1,7 @@ import { DatabaseConfigRenderTable } from "."; import { LinkRenderer } from "./LinkRenderer"; import * as markdownTable from "./markdown-table"; +import { PageLinkResolver } from "./PageLinkResolver"; import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; import { RenderDatabasePageTask } from "./RenderDatabasePageTask"; import { DatabaseConfigRenderPages, DatabaseView } from "./SyncConfig"; @@ -11,7 +12,8 @@ export class DatabaseViewRenderer { public renderViews( entries: (RenderDatabasePageTask | RenderDatabaseEntryTask)[], - config: DatabaseConfigRenderPages | DatabaseConfigRenderTable + config: DatabaseConfigRenderPages | DatabaseConfigRenderTable, + linkResolver: PageLinkResolver ): string { const configuredViews = config.views || [{}]; @@ -19,7 +21,7 @@ export class DatabaseViewRenderer { const groupByProperty = view?.properties?.groupBy; if (!groupByProperty) { - return this.renderView(entries, null, view); + return this.renderView(entries, null, view, linkResolver); } else { const grouped = new Array( ...groupBy(entries, (p) => @@ -28,7 +30,7 @@ export class DatabaseViewRenderer { ); return grouped - .map(([key, pages]) => this.renderView(pages, key, view)) + .map(([key, pages]) => this.renderView(pages, key, view, linkResolver)) .join("\n\n"); } }); @@ -39,7 +41,8 @@ export class DatabaseViewRenderer { private renderView( pages: (RenderDatabasePageTask | RenderDatabaseEntryTask)[], titleAppendix: string | null, - view: DatabaseView + view: DatabaseView, + linkResolver: PageLinkResolver ): string { if (!pages[0]) { return ""; @@ -60,7 +63,7 @@ export class DatabaseViewRenderer { cols.map((c, i) => { const content = escapeTableCell(r.properties.properties.get(c)); return i == 0 && isRenderPageTask(r) - ? this.linkRenderer.renderPageLink(content, r) // make the first cell a relative link to the page + ? this.linkRenderer.renderPageLink(content, r, linkResolver) // make the first cell a relative link to the page : content; }) ) @@ -68,7 +71,9 @@ export class DatabaseViewRenderer { const tableMd = markdownTable.markdownTable(table); if (view.title) { - const formattedTitle = [view.title, titleAppendix].filter(x => !!x).join(" - "); + const formattedTitle = [view.title, titleAppendix] + .filter((x) => !!x) + .join(" - "); return `## ${formattedTitle}\n\n` + tableMd; } else { return tableMd; diff --git a/src/DeferredRenderer.ts b/src/DeferredRenderer.ts index 75756b6..228e9f1 100644 --- a/src/DeferredRenderer.ts +++ b/src/DeferredRenderer.ts @@ -4,6 +4,7 @@ import { ChildDatabaseRenderer } from "./ChildDatabaseRenderer"; import { Database } from "./Database"; import { DatabaseEntryRenderer } from "./DatabaseEntryRenderer"; import { DatabasePageRenderer } from "./DatabasePageRenderer"; +import { PageLinkResolver } from "./PageLinkResolver"; import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask"; import { RenderDatabasePageTask as RenderDatabasePageTask } from "./RenderDatabasePageTask"; import { RenderedDatabaseEntry } from "./RenderedDatabaseEntry"; @@ -35,8 +36,11 @@ export class DeferredRenderer { this.entryRenderer = entryRenderer; } - public async renderChildDatabase(databaseId: string): Promise { - return await this.dbRenderer.renderChildDatabase(databaseId); + public async renderChildDatabase( + databaseId: string, + linkResolver: PageLinkResolver + ): Promise { + return await this.dbRenderer.renderChildDatabase(databaseId, linkResolver); } public async renderPage( diff --git a/src/LinkRenderer.ts b/src/LinkRenderer.ts index 42da12c..978ae38 100644 --- a/src/LinkRenderer.ts +++ b/src/LinkRenderer.ts @@ -1,4 +1,5 @@ -import { RenderDatabasePageTask } from './RenderDatabasePageTask'; +import { PageLinkResolver } from "./PageLinkResolver"; +import { RenderDatabasePageTask } from "./RenderDatabasePageTask"; export class LinkRenderer { constructor() {} @@ -7,9 +8,13 @@ export class LinkRenderer { return `[${text}](${url})`; } - renderPageLink(text: string, page: RenderDatabasePageTask): string { - const url = "/" + page.file; - - return this.renderUrlLink(text, url); + renderPageLink( + text: string, + toPage: RenderDatabasePageTask, + linkResolver: PageLinkResolver + ): string { + const link = linkResolver.resolveRelativeLinkTo(toPage.file); + + return this.renderUrlLink(text, link); } } diff --git a/src/PageLinkResolver.ts b/src/PageLinkResolver.ts new file mode 100644 index 0000000..21406ec --- /dev/null +++ b/src/PageLinkResolver.ts @@ -0,0 +1,19 @@ +import * as path from "path"; + +export class PageLinkResolver { + private readonly absoluteFromDir: string; + + constructor(sourceDir: string) { + this.absoluteFromDir = path.resolve(sourceDir); + } + + resolveRelativeLinkTo(file: string) { + const absoluteToDir = path.resolve(file); + + const relativePath = path.relative(this.absoluteFromDir, absoluteToDir); + + // normalize the rleative path to start with a ./ as that's the markdown convention for relative links + // at least how it's most commonly interpreted (e.g. by textlint) + return relativePath.startsWith(".") ? relativePath : "./" + relativePath; + } +} diff --git a/src/PropertiesParser.ts b/src/PropertiesParser.ts index e5aab1b..0051d13 100644 --- a/src/PropertiesParser.ts +++ b/src/PropertiesParser.ts @@ -1,7 +1,7 @@ import { Page, PropertyValue } from "@notionhq/client/build/src/api-types"; import { DatabasePageProperties } from "./DatabasePageProperties"; -import { RenderingLoggingContext } from "./logger"; +import { RenderingContextLogger } from "./RenderingContextLogger"; import { RichTextRenderer } from "./RichTextRenderer"; const debug = require("debug")("properties"); @@ -44,7 +44,7 @@ export class PropertiesParser { let title: string | null = null; let titleProperty: string | null = null; - const context = new RenderingLoggingContext(page.url); + const context = new RenderingContextLogger(page.url); for (const [name, value] of Object.entries(page.properties)) { const parsedValue = await this.parsePropertyValue(value, context); @@ -78,15 +78,15 @@ export class PropertiesParser { private async parsePropertyValue( value: PropertyValue, - context: RenderingLoggingContext + context: RenderingContextLogger ): Promise { switch (value.type) { case "number": return value.number; case "title": - return await this.richText.renderMarkdown(value.title, context); + return await this.richText.renderPlainText(value.title); case "rich_text": - return await this.richText.renderMarkdown(value.rich_text, context); + return await this.richText.renderPlainText(value.rich_text); case "select": return value.select?.name; case "multi_select": diff --git a/src/RecursiveBodyRenderer.ts b/src/RecursiveBodyRenderer.ts index 654a894..2d7f01a 100644 --- a/src/RecursiveBodyRenderer.ts +++ b/src/RecursiveBodyRenderer.ts @@ -1,9 +1,8 @@ import { Block, Page } from '@notionhq/client/build/src/api-types'; -import { AssetWriter } from './AssetWriter'; import { BlockRenderer } from './BlockRenderer'; -import { RenderingLoggingContext } from './logger'; import { NotionApiFacade } from './NotionApiFacade'; +import { RenderingContext } from './RenderingContext'; const debug = require("debug")("body"); @@ -15,8 +14,7 @@ export class RecursiveBodyRenderer { async renderBody( page: Page, - assets: AssetWriter, - context: RenderingLoggingContext + context: RenderingContext ): Promise { debug("begin rendering body of page " + page.id, page.properties); @@ -24,7 +22,7 @@ export class RecursiveBodyRenderer { // todo: paging const renderChilds = childs.results.map( - async (x) => await this.renderBlock(x, "", assets, context) + async (x) => await this.renderBlock(x, "", context) ); const blocks = await Promise.all(renderChilds); const body = blocks.join("\n\n"); @@ -37,12 +35,10 @@ export class RecursiveBodyRenderer { async renderBlock( block: Block, indent: string, - assets: AssetWriter, - context: RenderingLoggingContext + context: RenderingContext ): Promise { const parentBlock = await this.blockRenderer.renderBlock( block, - assets, context ); const parentLines = parentBlock && this.indent(parentBlock.lines, indent); @@ -56,7 +52,7 @@ export class RecursiveBodyRenderer { const childIndent = indent + " ".repeat(parentBlock?.childIndent || 0); const renderChilds = children.map( - async (x) => await this.renderBlock(x, childIndent, assets, context) + async (x) => await this.renderBlock(x, childIndent, context) ); const childLines = await Promise.all(renderChilds); diff --git a/src/RenderingContext.ts b/src/RenderingContext.ts new file mode 100644 index 0000000..99468e1 --- /dev/null +++ b/src/RenderingContext.ts @@ -0,0 +1,27 @@ +import * as path from "path"; + +import { AssetWriter } from "./AssetWriter"; +import { PageLinkResolver } from "./PageLinkResolver"; +import { RenderingContextLogger as RenderingContextLogger } from "./RenderingContextLogger"; + +/** + * Unit of work for rendering a specific page. + * Note: this is a bit of a service locator, be careful about not breaking SRP + */ +export class RenderingContext { + readonly assetWriter: AssetWriter; + readonly logger: RenderingContextLogger; + readonly linkResolver: PageLinkResolver; + + constructor(notionUrl: string, file: string) { + this.logger = new RenderingContextLogger(notionUrl, file); + + const dir = path.dirname(file); + + // write all assets right next to the page's markdown file + this.assetWriter = new AssetWriter(dir, this.logger); + + // resolve all links relative to the page's markdown file dir + this.linkResolver = new PageLinkResolver(dir); + } +} diff --git a/src/RenderingContextLogger.ts b/src/RenderingContextLogger.ts new file mode 100644 index 0000000..7a085d0 --- /dev/null +++ b/src/RenderingContextLogger.ts @@ -0,0 +1,34 @@ +import * as chalk from "chalk"; +import { performance } from "perf_hooks"; +import { logger } from "./logger"; + + +export class RenderingContextLogger { + private readonly start = performance.now(); + + constructor( + public readonly notionUrl: string, + public readonly file?: string + ) { } + + info(message: string) { + return logger.info(this.garnish(message)); + } + + warn(message: string) { + return logger.warn(this.garnish(message)); + } + + error(err: unknown) { + return logger.error(this.garnish(err as any)); // bah + } + + complete() { + const elapsed = performance.now() - this.start; + this.info("rendered page in " + Math.round(elapsed) + "ms"); + } + + private garnish(message: string) { + return `${message} ${chalk.gray(this.file || this.notionUrl)}`; + } +} diff --git a/src/RichTextRenderer.ts b/src/RichTextRenderer.ts index 34f6e1e..dcc9dce 100644 --- a/src/RichTextRenderer.ts +++ b/src/RichTextRenderer.ts @@ -1,8 +1,9 @@ -import { RichText } from '@notionhq/client/build/src/api-types'; +import { RichText } from "@notionhq/client/build/src/api-types"; -import { LinkRenderer } from './LinkRenderer'; -import { RenderingLoggingContext } from './logger'; -import { MentionedPageRenderer } from './MentionedPageRenderer'; +import { LinkRenderer } from "./LinkRenderer"; +import { RenderingContextLogger } from "./RenderingContextLogger"; +import { MentionedPageRenderer } from "./MentionedPageRenderer"; +import { RenderingContext } from "./RenderingContext"; const debug = require("debug")("richtext"); @@ -20,7 +21,7 @@ export class RichTextRenderer { public async renderMarkdown( text: RichText[], - context: RenderingLoggingContext + context: RenderingContext ): Promise { const result: string[] = []; @@ -35,7 +36,7 @@ export class RichTextRenderer { private async renderMarkdownCode( rt: RichText, - context: RenderingLoggingContext + context: RenderingContext ) { const mod = this.modifier(rt); @@ -44,7 +45,7 @@ export class RichTextRenderer { return this.renderUnsupported( `unsupported rich text type: ${rt.type}`, rt, - context + context.logger ); case "mention": switch (rt.mention.type) { @@ -54,14 +55,14 @@ export class RichTextRenderer { rt.plain_text ); const text = this.wrap(mod, page.properties.meta.title); - return this.linkRenderer.renderPageLink(text, page); + return this.linkRenderer.renderPageLink(text, page, context.linkResolver); case "database": case "date": case "user": return this.renderUnsupported( `unsupported rich text mention type: ${rt.mention.type}`, rt, - context + context.logger ); } case "text": @@ -135,7 +136,7 @@ export class RichTextRenderer { private renderUnsupported( msg: string, obj: any, - context: RenderingLoggingContext + context: RenderingContextLogger ): string { context.warn(msg); debug(msg + "\n%O", obj); diff --git a/src/logger.ts b/src/logger.ts index 182103a..875cdc2 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,5 +1,4 @@ import * as chalk from 'chalk'; -import { performance } from 'perf_hooks'; const info = (...args: any[]): void => { console.log(chalk.cyan("info"), ...args); @@ -13,38 +12,10 @@ const error = (...args: any[]): void => { console.error(chalk.red("error"), ...args); }; -const logger = { +export const logger = { info, warn, error, }; -export class RenderingLoggingContext { - private readonly start = performance.now(); - constructor( - public readonly notionUrl: string, - public readonly file?: string - ) {} - - info(message: string) { - return logger.info(this.garnish(message)); - } - - warn(message: string) { - return logger.warn(this.garnish(message)); - } - - error(err: unknown) { - return logger.error(this.garnish(err as any)); // bah - } - - complete() { - const elapsed = performance.now() - this.start; - this.info("rendered page in " + Math.round(elapsed) + "ms"); - } - - private garnish(message: string) { - return `${message} ${chalk.gray(this.file || this.notionUrl)}`; - } -} diff --git a/src/sync.ts b/src/sync.ts index 968038d..ffd7b14 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -8,6 +8,7 @@ import { FrontmatterRenderer } from './FrontmatterRenderer'; import { LinkRenderer } from './LinkRenderer'; import { MentionedPageRenderer } from './MentionedPageRenderer'; import { NotionApiFacade } from './NotionApiFacade'; +import { PageLinkResolver } from './PageLinkResolver'; import { PropertiesParser } from './PropertiesParser'; import { RecursiveBodyRenderer } from './RecursiveBodyRenderer'; import { RichTextRenderer } from './RichTextRenderer'; @@ -50,7 +51,8 @@ export async function sync(notionApiToken: string, config: SyncConfig) { deferredRenderer.initialize(dbRenderer, pageRenderer, entryRenderer); // seed it with the root database - await deferredRenderer.renderChildDatabase(config.cmsDatabaseId); + const rootLinkResolver = new PageLinkResolver("."); + await deferredRenderer.renderChildDatabase(config.cmsDatabaseId, rootLinkResolver); await deferredRenderer.process(); const rendered = deferredRenderer.getRenderedPages(); From f68ec044977d86fddb403ad94dd326646ad4308c Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Mar 2022 23:07:30 +0100 Subject: [PATCH 19/29] chore: bump to version 0.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4d0bc7..2dbe7e7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.0", + "version": "0.10.0", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From 5d2eb17bb88c95e7786fb875451fa8f16e809789 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 1 Mar 2022 23:18:22 +0100 Subject: [PATCH 20/29] test: fix tests --- src/LinkRenderer.spec.ts | 7 +++++-- src/RichTextRenderer.spec.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/LinkRenderer.spec.ts b/src/LinkRenderer.spec.ts index 7bcd2b1..938ddc3 100644 --- a/src/LinkRenderer.spec.ts +++ b/src/LinkRenderer.spec.ts @@ -1,14 +1,17 @@ import { LinkRenderer } from './LinkRenderer'; +import { PageLinkResolver } from './PageLinkResolver'; import { RenderDatabasePageTask } from './RenderDatabasePageTask'; describe("LinkRenderer", () => { + test("renderPageLink strips outDir from link", async () => { + const resolver = new PageLinkResolver("out"); const sut = new LinkRenderer(); const page: Partial = { file: "out/test.md", }; - const link = sut.renderPageLink("text", page as any); - expect(link).toEqual("[text](/out/test.md)"); + const link = sut.renderPageLink("text", page as any, resolver); + expect(link).toEqual("[text](./test.md)"); }); }); diff --git a/src/RichTextRenderer.spec.ts b/src/RichTextRenderer.spec.ts index 423310f..34e31f6 100644 --- a/src/RichTextRenderer.spec.ts +++ b/src/RichTextRenderer.spec.ts @@ -1,6 +1,6 @@ import { Annotations, RichText } from '@notionhq/client/build/src/api-types'; +import { RenderingContext } from './RenderingContext'; -import { RenderingLoggingContext } from './logger'; import { RichTextRenderer } from './RichTextRenderer'; function annotations(x: Partial): Annotations { @@ -15,7 +15,7 @@ function annotations(x: Partial): Annotations { }; } -const context = new RenderingLoggingContext(""); +const context = new RenderingContext("", ""); describe("RichTextRenderer", () => { let sut: RichTextRenderer; From 9de860ba657650c3d162ab8a000a62213c6718d8 Mon Sep 17 00:00:00 2001 From: Jelle Date: Tue, 15 Mar 2022 11:10:01 +0100 Subject: [PATCH 21/29] fix: Remove whitespace from RichText joining * Closes #4 --- .gitignore | 3 ++- src/RichTextRenderer.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ff3babd..c9e85bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules .temp .cache .env -dist/ \ No newline at end of file +dist/ +.idea/ \ No newline at end of file diff --git a/src/RichTextRenderer.ts b/src/RichTextRenderer.ts index dcc9dce..eeb0531 100644 --- a/src/RichTextRenderer.ts +++ b/src/RichTextRenderer.ts @@ -16,7 +16,7 @@ export class RichTextRenderer { ) {} public async renderPlainText(text: RichText[]): Promise { - return text.map((rt) => rt.plain_text).join(" "); + return text.map((rt) => rt.plain_text).join(""); } public async renderMarkdown( From c4e004b39b43840344283331c643f1026c7c24f4 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Mar 2022 17:38:33 +0100 Subject: [PATCH 22/29] chore: bump version to v0.10.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2dbe7e7..203538f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.0", + "version": "0.10.1", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From aa404d4b0687312d847af0e8f2fce0e13ef9740e Mon Sep 17 00:00:00 2001 From: dewkul Date: Sat, 18 Jun 2022 22:31:05 +0700 Subject: [PATCH 23/29] feat: render bookmark as link --- README.md | 2 +- src/BlockRenderer.ts | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c37e822..3240b6b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The following [Notion API block object types](https://developers.notion.com/refe | Video | ❌ Missing | | | File | ❌ Missing | | | PDF | ❌ Missing | | -| Bookmark | ❌ Missing | | +| Bookmark | ✅ Yes | use a caption as a link name | | | Equation | ❌ Missing | | | Divider | ✅ Yes | | | Table Of Contents | ❌ not planned | static site generators have their own ToC implementations | diff --git a/src/BlockRenderer.ts b/src/BlockRenderer.ts index 54ffe9b..ddc52db 100644 --- a/src/BlockRenderer.ts +++ b/src/BlockRenderer.ts @@ -102,16 +102,28 @@ export class BlockRenderer { return { lines: "---" }; case "child_database": const msg = `\n`; - const db = await this.deferredRenderer.renderChildDatabase(block.id, context.linkResolver); + const db = await this.deferredRenderer.renderChildDatabase( + block.id, + context.linkResolver + ); return { lines: msg + db.markdown }; case "synced_block": // nothing to render, only the contents of the synced block are relevant // however, these are children nöpcl, and thus retrieved by recursion in RecusivveBodyRenderer return null; + case "bookmark": + const caption = block.bookmark.caption; + let title = block.bookmark.url; + if (caption) { + if (caption.length > 0) + title = await this.richText.renderPlainText(caption); + } + return { + lines: `[${title}](${block.bookmark.url})`, + }; case "toggle": case "child_page": case "embed": - case "bookmark": case "video": case "file": case "pdf": @@ -145,10 +157,7 @@ export class BlockRenderer { } } - async renderImage( - block: ImageBlock, - assets: AssetWriter - ): Promise { + async renderImage(block: ImageBlock, assets: AssetWriter): Promise { const url = this.parseUrl(block.image); const imageFile = await assets.download(url, block.id); From 2c11274f25f0950f6ecaa7714752ae54d92f650d Mon Sep 17 00:00:00 2001 From: dewkul Date: Wed, 22 Jun 2022 23:29:56 +0700 Subject: [PATCH 24/29] refactor: render bookmark with LinkRenderer --- src/BlockRenderer.ts | 15 ++++++++------- src/sync.ts | 41 ++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/BlockRenderer.ts b/src/BlockRenderer.ts index ddc52db..6f98ea9 100644 --- a/src/BlockRenderer.ts +++ b/src/BlockRenderer.ts @@ -14,6 +14,7 @@ import { DeferredRenderer } from "./DeferredRenderer"; import { RenderingContextLogger } from "./RenderingContextLogger"; import { RichTextRenderer } from "./RichTextRenderer"; import { RenderingContext } from "./RenderingContext"; +import { LinkRenderer } from "./LinkRenderer"; const debug = require("debug")("blocks"); @@ -24,7 +25,8 @@ export interface BlockRenderResult { export class BlockRenderer { constructor( private readonly richText: RichTextRenderer, - private readonly deferredRenderer: DeferredRenderer + private readonly deferredRenderer: DeferredRenderer, + private readonly link: LinkRenderer ) {} async renderBlock( @@ -112,14 +114,13 @@ export class BlockRenderer { // however, these are children nöpcl, and thus retrieved by recursion in RecusivveBodyRenderer return null; case "bookmark": - const caption = block.bookmark.caption; + // render caption (if provided) as a link name + const caption = block.bookmark.caption || []; let title = block.bookmark.url; - if (caption) { - if (caption.length > 0) - title = await this.richText.renderPlainText(caption); - } + if (caption.length > 0) + title = await this.richText.renderPlainText(caption); return { - lines: `[${title}](${block.bookmark.url})`, + lines: this.link.renderUrlLink(title, block.bookmark.url), }; case "toggle": case "child_page": diff --git a/src/sync.ts b/src/sync.ts index ffd7b14..4bbeda9 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,18 +1,18 @@ -import { BlockRenderer } from './BlockRenderer'; -import { ChildDatabaseRenderer } from './ChildDatabaseRenderer'; -import { DatabaseEntryRenderer } from './DatabaseEntryRenderer'; -import { DatabasePageRenderer } from './DatabasePageRenderer'; -import { DatabaseViewRenderer } from './DatabaseViewRenderer'; -import { DeferredRenderer } from './DeferredRenderer'; -import { FrontmatterRenderer } from './FrontmatterRenderer'; -import { LinkRenderer } from './LinkRenderer'; -import { MentionedPageRenderer } from './MentionedPageRenderer'; -import { NotionApiFacade } from './NotionApiFacade'; -import { PageLinkResolver } from './PageLinkResolver'; -import { PropertiesParser } from './PropertiesParser'; -import { RecursiveBodyRenderer } from './RecursiveBodyRenderer'; -import { RichTextRenderer } from './RichTextRenderer'; -import { SyncConfig } from './SyncConfig'; +import { BlockRenderer } from "./BlockRenderer"; +import { ChildDatabaseRenderer } from "./ChildDatabaseRenderer"; +import { DatabaseEntryRenderer } from "./DatabaseEntryRenderer"; +import { DatabasePageRenderer } from "./DatabasePageRenderer"; +import { DatabaseViewRenderer } from "./DatabaseViewRenderer"; +import { DeferredRenderer } from "./DeferredRenderer"; +import { FrontmatterRenderer } from "./FrontmatterRenderer"; +import { LinkRenderer } from "./LinkRenderer"; +import { MentionedPageRenderer } from "./MentionedPageRenderer"; +import { NotionApiFacade } from "./NotionApiFacade"; +import { PageLinkResolver } from "./PageLinkResolver"; +import { PropertiesParser } from "./PropertiesParser"; +import { RecursiveBodyRenderer } from "./RecursiveBodyRenderer"; +import { RichTextRenderer } from "./RichTextRenderer"; +import { SyncConfig } from "./SyncConfig"; export async function sync(notionApiToken: string, config: SyncConfig) { const publicApi = new NotionApiFacade(notionApiToken); @@ -32,7 +32,11 @@ export async function sync(notionApiToken: string, config: SyncConfig) { linkRenderer ); const propertiesParser = new PropertiesParser(richTextRenderer); - const blockRenderer = new BlockRenderer(richTextRenderer, deferredRenderer); + const blockRenderer = new BlockRenderer( + richTextRenderer, + deferredRenderer, + linkRenderer + ); const bodyRenderer = new RecursiveBodyRenderer(publicApi, blockRenderer); const entryRenderer = new DatabaseEntryRenderer(propertiesParser); const pageRenderer = new DatabasePageRenderer( @@ -52,7 +56,10 @@ export async function sync(notionApiToken: string, config: SyncConfig) { // seed it with the root database const rootLinkResolver = new PageLinkResolver("."); - await deferredRenderer.renderChildDatabase(config.cmsDatabaseId, rootLinkResolver); + await deferredRenderer.renderChildDatabase( + config.cmsDatabaseId, + rootLinkResolver + ); await deferredRenderer.process(); const rendered = deferredRenderer.getRenderedPages(); From 3c95e7cd227b2e906d868a3342c78f3d69c10f67 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 17 Oct 2022 12:05:17 +0200 Subject: [PATCH 25/29] feat: add support for database paging notion-markdown-cms would previously fail when trying to query pages from a database with more than 100 entries because paging was not implemented. --- package.json | 2 +- src/ChildDatabaseRenderer.ts | 2 +- src/NotionApiFacade.ts | 36 ++++++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 203538f..361c857 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.1", + "version": "0.11.0", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" diff --git a/src/ChildDatabaseRenderer.ts b/src/ChildDatabaseRenderer.ts index 5273eca..ae52645 100644 --- a/src/ChildDatabaseRenderer.ts +++ b/src/ChildDatabaseRenderer.ts @@ -106,6 +106,6 @@ export class ChildDatabaseRenderer { page_size: 100, }); - return allPages.results; + return allPages; } } diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts index 2cbd92b..5b4727d 100644 --- a/src/NotionApiFacade.ts +++ b/src/NotionApiFacade.ts @@ -1,7 +1,11 @@ import { - APIErrorCode, APIResponseError, Client, RequestTimeoutError, UnknownHTTPResponseError -} from '@notionhq/client'; -import { DatabasesQueryParameters } from '@notionhq/client/build/src/api-endpoints'; + APIErrorCode, + APIResponseError, + Client, + RequestTimeoutError, + UnknownHTTPResponseError, +} from "@notionhq/client"; +import { DatabasesQueryParameters } from "@notionhq/client/build/src/api-endpoints"; const debug = require("debug")("notion-api"); @@ -34,17 +38,25 @@ export class NotionApiFacade { } async queryDatabase(query: DatabasesQueryParameters) { - const result = await this.withRetry( - async () => await this.client.databases.query(query) - ); // todo: paging + const results = []; - if (result.next_cursor) { - throw new Error( - `Paging not implemented, db ${query.database_id} has more than 100 entries` - ); - } + let next_cursor: string | null = null; - return result; + do { + const response = await this.withRetry( + async () => + await this.client.databases.query({ + ...query, + start_cursor: next_cursor || undefined, + }) + ); + + results.push(...response.results); + + next_cursor = response.next_cursor; + } while (next_cursor); + + return results; } async retrievePage(pageId: string) { From c16618307b2aec56ef42a9fdc70947d26537e6d4 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 17 Oct 2022 12:12:06 +0200 Subject: [PATCH 26/29] feat: implement support for block children paging this hasn't been a problem so far since blocks usually don't have more than 100 children. Fix this potential issue nonetheless while we're at it. --- src/NotionApiFacade.ts | 33 +++++++++++++++++++-------------- src/RecursiveBodyRenderer.ts | 5 ++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/NotionApiFacade.ts b/src/NotionApiFacade.ts index 5b4727d..721c2d2 100644 --- a/src/NotionApiFacade.ts +++ b/src/NotionApiFacade.ts @@ -49,10 +49,10 @@ export class NotionApiFacade { ...query, start_cursor: next_cursor || undefined, }) - ); + ); results.push(...response.results); - + next_cursor = response.next_cursor; } while (next_cursor); @@ -66,20 +66,25 @@ export class NotionApiFacade { } async listBlockChildren(blockId: string) { - const result = await this.withRetry( - async () => - await this.client.blocks.children.list({ - block_id: blockId, - }) - ); // todo: paging here? - - if (result.next_cursor) { - throw new Error( - `Paging not implemented, block ${blockId} has more children than returned in a single request` + const results = []; + + let next_cursor: string | null = null; + + do { + const response = await this.withRetry( + async () => + await this.client.blocks.children.list({ + block_id: blockId, + start_cursor: next_cursor || undefined, + }) ); - } - return result; + results.push(...response.results); + + next_cursor = response.next_cursor; + } while (next_cursor); + + return results; } printStats() { diff --git a/src/RecursiveBodyRenderer.ts b/src/RecursiveBodyRenderer.ts index 2d7f01a..251b9e3 100644 --- a/src/RecursiveBodyRenderer.ts +++ b/src/RecursiveBodyRenderer.ts @@ -20,8 +20,7 @@ export class RecursiveBodyRenderer { const childs = await this.publicApi.listBlockChildren(page.id); - // todo: paging - const renderChilds = childs.results.map( + const renderChilds = childs.map( async (x) => await this.renderBlock(x, "", context) ); const blocks = await Promise.all(renderChilds); @@ -47,7 +46,7 @@ export class RecursiveBodyRenderer { // blocks, see https://developers.notion.com/reference/retrieve-a-block // "If a block contains the key has_children: true, use the Retrieve block children endpoint to get the list of children" const children = block.has_children - ? (await this.publicApi.listBlockChildren(block.id)).results + ? (await this.publicApi.listBlockChildren(block.id)) : []; const childIndent = indent + " ".repeat(parentBlock?.childIndent || 0); From 6da3627993f3823dc43d80b654dd126708f4ef6f Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 17 Oct 2022 12:14:17 +0200 Subject: [PATCH 27/29] chore: bump version to v0.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 361c857..7d64c0f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.11.0", + "version": "0.11.1", "name": "@meshcloud/notion-markdown-cms", "engines": { "node": ">=14" From 5cbf73e2ac6c231d98ce7938e6efd795675a3acf Mon Sep 17 00:00:00 2001 From: Yasin Junet Date: Thu, 4 Jan 2024 21:24:27 +0700 Subject: [PATCH 28/29] feat: add support for people property --- src/PropertiesParser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PropertiesParser.ts b/src/PropertiesParser.ts index 0051d13..333716e 100644 --- a/src/PropertiesParser.ts +++ b/src/PropertiesParser.ts @@ -109,9 +109,10 @@ export class PropertiesParser { return value.last_edited_time; case "last_edited_by": return value.last_edited_by.name; + case "people": + return value.people.map((person) => person.name).join(", "); case "formula": case "rollup": - case "people": case "files": case "checkbox": const notSupported = "unsupported property type: " + value.type; From 37594d5ef517f0060a77cd314e502c54402d146d Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Sat, 20 Apr 2024 08:38:08 +0200 Subject: [PATCH 29/29] doc: document support for People property --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3240b6b..9be1e42 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The following [Notion API page property types](https://developers.notion.com/ref | Relation | ✅ Yes | rendered as array of page ids | | Rollup | ❌ missing | | | Title | ✅ Yes | used as page title | -| People | ❌ missing | | +| People | ✅ Yes | rendered as comma-separated list of names | | Files | ❌ missing | | | Checkbox | ❌ missing | | | Url | ✅ Yes | rendered as string |