From d6cd8e4403c65ea28f2c2dbf2e4f561c58e0db9c Mon Sep 17 00:00:00 2001 From: Tobias Lins Date: Tue, 21 Apr 2020 13:54:53 +0200 Subject: [PATCH] Implement pages, tables & users; WIP Co-authored-by: Timo --- .gitattributes | 2 - .gitignore | 4 + README.md | 36 +++++- package.json | 22 +++- src/api/notion.ts | 94 ++++++++++++++++ src/api/types.ts | 86 ++++++++++++++ src/api/utils.ts | 45 ++++++++ src/index.ts | 43 +++++++ src/response.ts | 12 ++ src/routes/page.ts | 10 ++ src/routes/table.ts | 53 +++++++++ src/routes/user.ts | 8 ++ tsconfig.json | 20 ++++ webpack.config.js | 32 ++++++ wrangler.example.toml | 8 ++ yarn.lock | 255 ++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 723 insertions(+), 7 deletions(-) delete mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 src/api/notion.ts create mode 100644 src/api/types.ts create mode 100644 src/api/utils.ts create mode 100644 src/index.ts create mode 100644 src/response.ts create mode 100644 src/routes/page.ts create mode 100644 src/routes/table.ts create mode 100644 src/routes/user.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.js create mode 100644 wrangler.example.toml create mode 100644 yarn.lock diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc574ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +worker +wrangler.toml \ No newline at end of file diff --git a/README.md b/README.md index 930c4ed..43cf17e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ # notion-cloudflare-worker - + +A serverless layer on top of the private Notion API. It leverages [Cloudflare Workers](https://workers.cloudflare.com/), to provide fast and easy access to all your Notion content. + +_This package might become obsolete, once the official Notion API arrives._ + +## Features + +🍭 **Easy to use** – Receive Notion data with a single GET request +✨ **Fast CDN** – Leverage the global Cloudflare CDN +🛫 **CORS Friendly** – Access your data where you need it +🗄 **Table Access** – Get structured data from tables & databases + +## Use Cases + +- Use a table to manage posts for your blog + +## Endpoints + +We provide a hosted version of this project on [https://notion.splitbee.io/](https://notion.splitbee.io/). You can also [host your own](https://workers.cloudflare.com/). Cloudflare offers a generous free plan with up to 100,000 request per day. + +### Get data from a page - `/v1/page/` + +[Example](https://notion.splitbee.io/v1/page/2e22de6b770e4166be301490f6ffd420) + +Returns all block data for a given page. +For example, you can render this data with [`react-notion`](https://github.com/splitbee/react-notion). + +### Get parsed data from table `/v1/table/` + +[Example](https://notion.splitbee.io/v1/page/2e22de6b770e4166be301490f6ffd420) + +## Credits + +- [Timo Lins](https://timo.sh) – Idea, Documentation +- [Tobias Lins](https://tobi.sh) – Code diff --git a/package.json b/package.json index 4b62193..17e87de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,21 @@ { "name": "notion-cloudflare-worker", - "version": "1.0.0", - "main": "index.js", - "author": "Tobias Lins", - "license": "MIT" + "version": "0.1.0", + "main": "dist/index.js", + "license": "MIT", + "scripts": { + "build": "webpack", + "dev": "wrangler preview --watch", + "deploy": "wrangler publish -e production" + }, + "dependencies": { + "tiny-request-router": "^1.2.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^1.0.9", + "@types/node": "^13.13.1", + "prettier": "^2.0.4", + "ts-loader": "^7.0.1", + "typescript": "^3.8.3" + } } diff --git a/src/api/notion.ts b/src/api/notion.ts new file mode 100644 index 0000000..06623f8 --- /dev/null +++ b/src/api/notion.ts @@ -0,0 +1,94 @@ +import { resolve } from "dns"; + +const NOTION_API = "https://www.notion.so/api/v3"; + +type JSONData = + | null + | boolean + | number + | string + | JSONData[] + | { [prop: string]: JSONData }; + +type INotionParams = { + resource: string; + body: JSONData; +}; + +const loadPageChunkBody = { + limit: 999, + cursor: { stack: [] }, + chunkNumber: 0, + verticalColumns: false, +}; + +const fetchNotionData = async ({ resource, body }: INotionParams) => { + const res = await fetch(`${NOTION_API}/${resource}`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(body), + //cf: {} + }); + + return res.json(); +}; + +export const fetchPageById = async (pageId: string) => { + const res = await fetchNotionData({ + resource: "loadPageChunk", + body: { + pageId, + ...loadPageChunkBody, + }, + }); + + return res; +}; + +const queryCollectionBody = { + query: { aggregations: [{ property: "title", aggregator: "count" }] }, + loader: { + type: "table", + limit: 999, + searchQuery: "", + userTimeZone: "Europe/Vienna", + userLocale: "en", + loadContentCover: true, + }, +}; + +export const fetchTableData = async ( + collectionId: string, + collectionViewId: string +) => { + const table = await fetchNotionData({ + resource: "queryCollection", + body: { + collectionId, + collectionViewId, + ...queryCollectionBody, + }, + }); + return table; +}; + +export const fetchNotionUser = async ( + userIds: string[] +): Promise<{ id: string; full_name: string }[]> => { + const users = await fetchNotionData({ + resource: "getRecordValues", + body: { + requests: userIds.map((id) => ({ id, table: "notion_user" })), + }, + }); + + return users.results.map((u: any) => { + const user = { + id: u.value.id, + full_name: u.value.given_name + " " + u.value.family_name, + }; + return user; + }); +}; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..5a0b1c1 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,86 @@ +type BoldFormatType = ["b"]; +type ItalicFormatType = ["i"]; +type StrikeFormatType = ["s"]; +type CodeFormatType = ["c"]; +type LinkFormatType = ["a", string]; +type DateFormatType = [ + "d", + { + type: "date"; + start_date: string; + date_format: string; + } +]; +type UserFormatType = ["u", string]; +type PageFormatType = ["p", string]; +type SubDecorationType = + | BoldFormatType + | ItalicFormatType + | StrikeFormatType + | CodeFormatType + | LinkFormatType + | DateFormatType + | UserFormatType + | PageFormatType; +type BaseDecorationType = [string]; +type AdditionalDecorationType = [string, SubDecorationType[]]; +export type DecorationType = BaseDecorationType | AdditionalDecorationType; + +export type ColumnType = + | "select" + | "text" + | "date" + | "person" + | "checkbox" + | "title" + | "multi_select" + | "number"; + +export type ColumnSchemaType = { + name: string; + type: ColumnType; +}; + +export type RowContentType = + | string + | boolean + | number + | string[] + | { title: string; id: string }; + +export interface BaseValueType { + id: string; + version: number; + created_time: number; + last_edited_time: number; + parent_id: string; + parent_table: string; + alive: boolean; + created_by_table: string; + created_by_id: string; + last_edited_by_table: string; + last_edited_by_id: string; + content?: string[]; +} + +export interface CollectionType { + value: { + id: string; + version: number; + name: string[][]; + schema: { [key: string]: ColumnSchemaType }; + icon: string; + parent_id: string; + parent_table: string; + alive: boolean; + copied_from: string; + }; +} + +export interface RowType { + value: { + id: string; + parent_id: string; + properties: { [key: string]: DecorationType[] }; + }; +} diff --git a/src/api/utils.ts b/src/api/utils.ts new file mode 100644 index 0000000..4fca68a --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,45 @@ +import { DecorationType, ColumnType, RowContentType } from "./types"; + +export const pathToId = (path: string) => + `${path.substr(0, 8)}-${path.substr(8, 4)}-${path.substr( + 12, + 4 + )}-${path.substr(16, 4)}-${path.substr(20)}`; + +export const parsePageId = (id: string) => { + return id.includes("-") ? id : pathToId(id); +}; + +export const getNotionValue = ( + val: DecorationType[], + type: ColumnType +): RowContentType => { + switch (type) { + case "text": + return getTextContent(val); + case "person": + return ( + val.filter((v) => v.length > 1).map((v) => v[1]![0][1] as string) || [] + ); + case "checkbox": + return val[0][0] === "Yes"; + case "date": + if (val[0][1]![0][0] === "d") return val[0]![1]![0]![1]!.start_date; + else return ""; + case "title": + return getTextContent(val); + case "select": + return val[0][0]; + case "multi_select": + return val[0] as string[]; + case "number": + return Number(val[0][0]); + default: + console.log({ val, type }); + return "Not supported"; + } +}; + +const getTextContent = (text: DecorationType[]) => { + return text.reduce((prev, current) => prev + current[0], ""); +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..44a96ed --- /dev/null +++ b/src/index.ts @@ -0,0 +1,43 @@ +import {} from "@cloudflare/workers-types"; +import { Router, Method } from "tiny-request-router"; + +import { pageRoute } from "./routes/page"; +import { tableRoute } from "./routes/table"; +import { userRoute } from "./routes/user"; + +const router = new Router(); + +router.options("*", () => new Response("", { headers: {} })); +router.get("/v1/page/:pageId", pageRoute); +router.get("/v1/table/:pageId", tableRoute); +router.get("/v1/user/:userId", userRoute); + +router.get( + "*", + async (event: FetchEvent) => + new Response( + `Route not found! +Available routes: + - /v1/page/:pageId + - /v1/table/:pageId`, + { status: 404 } + ) +); + +const handleRequest = async (fetchEvent: FetchEvent): Promise => { + const request = fetchEvent.request; + const { pathname } = new URL(request.url); + + const match = router.match(request.method as Method, pathname); + + if (!match) { + return new Response("Endpoint not found.", { status: 404 }); + } + + return match.handler(match.params); +}; + +self.addEventListener("fetch", async (event: Event) => { + const fetchEvent = event as FetchEvent; + fetchEvent.respondWith(handleRequest(fetchEvent)); +}); diff --git a/src/response.ts b/src/response.ts new file mode 100644 index 0000000..7c9987d --- /dev/null +++ b/src/response.ts @@ -0,0 +1,12 @@ +export const createResponse = ( + body: string, + headers?: { [key: string]: string } +) => { + return new Response(body, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + ...headers, + }, + }); +}; diff --git a/src/routes/page.ts b/src/routes/page.ts new file mode 100644 index 0000000..4d3b96d --- /dev/null +++ b/src/routes/page.ts @@ -0,0 +1,10 @@ +import { fetchPageById } from "../api/notion"; +import { parsePageId } from "../api/utils"; +import { createResponse } from "../response"; + +export async function pageRoute(params: { pageId: string }) { + const pageId = parsePageId(params.pageId); + const res = await fetchPageById(pageId); + + return createResponse(JSON.stringify(res.recordMap.block)); +} diff --git a/src/routes/table.ts b/src/routes/table.ts new file mode 100644 index 0000000..0c82334 --- /dev/null +++ b/src/routes/table.ts @@ -0,0 +1,53 @@ +import { fetchPageById, fetchTableData } from "../api/notion"; +import { parsePageId, getNotionValue } from "../api/utils"; +import { RowContentType, CollectionType, RowType } from "../api/types"; +import { createResponse } from "../response"; + +export async function tableRoute(params: { pageId: string }) { + const pageId = parsePageId(params.pageId); + const page = await fetchPageById(pageId); + + console.log({ page }); + + const collection: CollectionType = Object.keys(page.recordMap.collection).map( + (k) => page.recordMap.collection[k] + )[0]; + const collectionView: { + value: { id: CollectionType["value"]["id"] }; + } = Object.keys(page.recordMap.collection_view).map( + (k) => page.recordMap.collection_view[k] + )[0]; + + const table = await fetchTableData( + collection.value.id, + collectionView.value.id + ); + + console.log({ table }); + + const collectionRows = collection.value.schema; + const collectionColKeys = Object.keys(collectionRows); + + const tableArr: RowType[] = table.result.blockIds.map( + (id: string) => table.recordMap.block[id] + ); + + const tableData = tableArr.filter( + (b) => + b.value && b.value.properties && b.value.parent_id === collection.value.id + ); + + const rows = tableData.map((td) => { + let row: { [key: string]: RowContentType } = { id: td.value.id }; + collectionColKeys.forEach((key) => { + const val = td.value.properties[key]; + if (val) { + const schema = collectionRows[key]; + row[schema.name] = getNotionValue(val, schema.type); + } + }); + return row; + }); + + return createResponse(JSON.stringify(rows)); +} diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..6a825e6 --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,8 @@ +import { fetchNotionUser } from "../api/notion"; +import { createResponse } from "../response"; + +export async function userRoute(params: { userId: string }) { + const users = await fetchNotionUser([params.userId]); + + return createResponse(JSON.stringify(users[0])); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..03965f3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "commonjs", + "target": "esnext", + "lib": ["esnext", "DOM", "DOM.Iterable", "WebWorker"], + "alwaysStrict": true, + "strict": true, + "preserveConstEnums": true, + "moduleResolution": "node", + "sourceMap": true, + "esModuleInterop": true + }, + "include": [ + "./src/*.ts", + "./src/**/*.ts", + "./node_modules/@cloudflare/workers-types/index.d.ts" + ], + "exclude": ["node_modules/", "dist/"] +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..4930d2a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require("path"); + +const mode = process.env.NODE_ENV || "production"; + +module.exports = { + output: { + filename: `worker.${mode}.js`, + path: path.join(__dirname, "dist"), + }, + target: "webworker", + devtool: "source-map", + mode, + resolve: { + extensions: [".ts", ".js"], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: "ts-loader", + options: { + transpileOnly: true, + }, + }, + { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, + ], + }, + optimization: { + usedExports: true, + }, +}; diff --git a/wrangler.example.toml b/wrangler.example.toml new file mode 100644 index 0000000..50b4b77 --- /dev/null +++ b/wrangler.example.toml @@ -0,0 +1,8 @@ +name = "notion-cloudflare-worker" +webpack_config = "webpack.config.js" +type = "webpack" + +account_id = "" +zone_id = "" + +route = "notion-api.splitbee.io/*" \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..872ceee --- /dev/null +++ b/yarn.lock @@ -0,0 +1,255 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cloudflare/workers-types@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-1.0.9.tgz#00cfb188e93125f95ccc27d7a4c6aabd6b62e699" + integrity sha512-x6aA5FRK0fR0pC/mkDq0nG6WNkf3aVu6vLRfA7FoMtajbvWKN9JBveI/HhbjpMOIRWfNnGd2rw6WH7NAv4XesA== + +"@types/node@^13.13.1": + version "13.13.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.1.tgz#1ba94c5a177a1692518bfc7b41aec0aa1a14354e" + integrity sha512-uysqysLJ+As9jqI5yqjwP3QJrhOcUwBjHUlUxPxjbplwKoILvXVsmYWEhfmAQlrPfbRZmhJB007o4L9sKqtHqQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chalk@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +enhanced-resolve@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" + integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +errno@^0.1.3: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +loader-utils@^1.0.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +path-to-regexp@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427" + integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw== + +picomatch@^2.0.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +prettier@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef" + integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +readable-stream@^2.0.1: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tiny-request-router@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tiny-request-router/-/tiny-request-router-1.2.2.tgz#1b80694497e4e8dcbb8e93851ec7f03c6ca13e75" + integrity sha512-6ZMFU7AP9so+hkqmMM9fJ11V44EAcYuHCmNdsyM8k94oVnNDPQwUAAPoBHqchHSpKG6yZbCasgVeRxaY5v2BCg== + dependencies: + path-to-regexp "^6.1.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-loader@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.1.tgz#ac9ae9eb8f5ebd0aa7b78b44db20691b6e31251b" + integrity sha512-wdGs9xO8UnwASwbluehzXciBtc9HfGlYA8Aiv856etLmdv8mJfAxCkt3YpS4g7G1IsGxaCVKQ102Qh0zycpeZQ== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=