├── .editorconfig
├── .eslintrc.json
├── .github
└── workflows
│ └── nx.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── babel.config.json
├── esbuild-runner.config.js
├── jest.config.js
├── jest.preset.js
├── nx.json
├── package.json
├── packages
├── .gitkeep
├── example-library
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── project.json
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── example-library.spec.ts
│ │ │ └── example-library.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.spec.json
│ └── yarn.lock
├── notion-api
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── README.typedoc.md
│ ├── doc
│ │ ├── .nojekyll
│ │ ├── README.md
│ │ ├── classes
│ │ │ ├── Backlinks.md
│ │ │ ├── CMS.md
│ │ │ ├── CMSPropertyResolver.md
│ │ │ └── NotionObjectIndex.md
│ │ ├── interfaces
│ │ │ ├── AssetRequestNextJSQuery.md
│ │ │ ├── Backlink.md
│ │ │ ├── BacklinkFrom.md
│ │ │ ├── BlockFilterFunction.md
│ │ │ ├── CMSConfig.md
│ │ │ ├── CMSCustomPropertyDerived.md
│ │ │ ├── CMSDefaultFrontmatter.md
│ │ │ ├── CMSPage.md
│ │ │ ├── CMSQueryParametersOptions.md
│ │ │ ├── CMSRetrieveOptions.md
│ │ │ ├── CMSScope.md
│ │ │ ├── CMSScopeOptions.md
│ │ │ ├── PaginatedArgs.md
│ │ │ ├── PaginatedList.md
│ │ │ ├── ParsedAssetRequest.md
│ │ │ ├── PropertyPointer.md
│ │ │ ├── SortBuilder.md
│ │ │ └── TimestampSortBuilder.md
│ │ └── modules.md
│ ├── jest.config.js
│ ├── package.json
│ ├── project.json
│ ├── src
│ │ ├── example
│ │ │ ├── blockData.ts
│ │ │ ├── databaseSchema.ts
│ │ │ ├── exampleHelpers.ts
│ │ │ ├── iteration.ts
│ │ │ └── recipeCMS.ts
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── assets.ts
│ │ │ ├── backlinks.ts
│ │ │ ├── cache.ts
│ │ │ ├── content-management-system.ts
│ │ │ ├── notion-api.spec.ts
│ │ │ ├── notion-api.ts
│ │ │ └── query.ts
│ │ └── typings.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.spec.json
│ ├── typedoc.js
│ └── yarn.lock
├── pinch-zoom
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.js
│ ├── project.json
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── pinch-zoom.spec.ts
│ │ │ └── pinch-zoom.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
├── playground-e2e
│ ├── .eslintrc.json
│ ├── cypress.json
│ ├── project.json
│ ├── src
│ │ ├── fixtures
│ │ │ └── example.json
│ │ ├── integration
│ │ │ └── app.spec.ts
│ │ └── support
│ │ │ ├── app.po.ts
│ │ │ ├── commands.ts
│ │ │ └── index.ts
│ └── tsconfig.json
├── playground
│ ├── .babelrc
│ ├── .browserslistrc
│ ├── .eslintrc.json
│ ├── jest.config.js
│ ├── project.json
│ ├── src
│ │ ├── app
│ │ │ ├── app.spec.tsx
│ │ │ ├── app.tsx
│ │ │ └── nx-welcome.tsx
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.tsx
│ │ └── polyfills.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── state
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── project.json
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── Scheduler.ts
│ │ │ ├── World.ts
│ │ │ ├── WorldProvider.tsx
│ │ │ ├── atomWithCompare.test.ts
│ │ │ ├── atomWithCompare.ts
│ │ │ ├── graph.ts
│ │ │ ├── implicitAtom.test.ts
│ │ │ ├── implicitAtom.ts
│ │ │ ├── implicitCapabilities.spec.ts
│ │ │ ├── implicitCapabilities.ts
│ │ │ ├── proxyCompareAtom.test.ts
│ │ │ ├── proxyCompareAtom.ts
│ │ │ ├── readReducerAtom.test.ts
│ │ │ ├── readReducerAtom.ts
│ │ │ ├── recursiveAtom.tsx
│ │ │ ├── shallowEqualAtom.ts
│ │ │ ├── withJotaiState.test.tsx
│ │ │ └── withJotaiState.tsx
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.spec.json
│ └── yarn.lock
└── util
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── project.json
│ ├── src
│ ├── index.ts
│ └── lib
│ │ ├── map.spec.ts
│ │ ├── map.ts
│ │ ├── memo.test.ts
│ │ ├── memo.ts
│ │ ├── object.test.ts
│ │ ├── object.ts
│ │ ├── typeAssertions.ts
│ │ ├── typeGuards.ts
│ │ └── types.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
├── rebuild-api.sh
├── tools
├── generators
│ └── .gitkeep
└── tsconfig.tools.json
├── tsconfig.base.json
├── workspace.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nrwl/nx", "react-hooks"],
5 | "rules": {
6 | "react-hooks/rules-of-hooks": "error",
7 | "react-hooks/exhaustive-deps": "error"
8 | },
9 | "overrides": [
10 | {
11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
12 | "rules": {
13 | "@nrwl/nx/enforce-module-boundaries": [
14 | "error",
15 | {
16 | "enforceBuildableLibDependency": true,
17 | "allow": [],
18 | "depConstraints": [
19 | {
20 | "sourceTag": "*",
21 | "onlyDependOnLibsWithTags": ["*"]
22 | }
23 | ]
24 | }
25 | ]
26 | }
27 | },
28 | {
29 | "files": ["*.ts", "*.tsx"],
30 | "extends": ["plugin:@nrwl/nx/typescript"],
31 | "rules": {}
32 | },
33 | {
34 | "files": ["*.js", "*.jsx"],
35 | "extends": ["plugin:@nrwl/nx/javascript"],
36 | "rules": {}
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/nx.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | jobs:
9 | main:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.event_name != 'pull_request' }}
12 | steps:
13 | - uses: actions/checkout@v2
14 | name: Checkout [main]
15 | with:
16 | fetch-depth: 0
17 | - name: Derive appropriate SHAs for base and head for `nx affected` commands
18 | uses: nrwl/nx-set-shas@v2
19 | - uses: actions/setup-node@v2
20 | with:
21 | node-version: '14'
22 | cache: yarn
23 | - run: yarn install
24 | - run: yarn nx affected --target=build --parallel --max-parallel=3
25 | - run: yarn nx affected --target=test --parallel --max-parallel=2
26 | pr:
27 | runs-on: ubuntu-latest
28 | if: ${{ github.event_name == 'pull_request' }}
29 | steps:
30 | - uses: actions/checkout@v2
31 | with:
32 | ref: ${{ github.event.pull_request.head.ref }}
33 | fetch-depth: 0
34 | - name: Derive appropriate SHAs for base and head for `nx affected` commands
35 | uses: nrwl/nx-set-shas@v2
36 | - uses: actions/setup-node@v2
37 | with:
38 | node-version: '14'
39 | cache: yarn
40 | - run: yarn install
41 | - run: yarn nx affected --target=build --parallel --max-parallel=3
42 | - run: yarn nx affected --target=test --parallel --max-parallel=2
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | **/node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": true,
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "nrwl.angular-console",
4 | "esbenp.prettier-vscode",
5 | "dbaeumer.vscode-eslint",
6 | "firsttris.vscode-jest-runner"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "jotai",
4 | "Subfilter"
5 | ],
6 | "typescript.tsdk": "node_modules/typescript/lib"
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JitlMonorepo
2 |
3 | This project was generated using [Nx](https://nx.dev).
4 |
5 |
6 |
7 | 🔎 **Smart, Fast and Extensible Build System**
8 |
9 | ## Adding capabilities to your workspace
10 |
11 | Nx supports many plugins which add capabilities for developing different types of applications and different tools.
12 |
13 | These capabilities include generating applications, libraries, etc as well as the devtools to test, and build projects as well.
14 |
15 | Below are our core plugins:
16 |
17 | - [React](https://reactjs.org)
18 | - `npm install --save-dev @nrwl/react`
19 | - Web (no framework frontends)
20 | - `npm install --save-dev @nrwl/web`
21 | - [Angular](https://angular.io)
22 | - `npm install --save-dev @nrwl/angular`
23 | - [Nest](https://nestjs.com)
24 | - `npm install --save-dev @nrwl/nest`
25 | - [Express](https://expressjs.com)
26 | - `npm install --save-dev @nrwl/express`
27 | - [Node](https://nodejs.org)
28 | - `npm install --save-dev @nrwl/node`
29 |
30 | There are also many [community plugins](https://nx.dev/community) you could add.
31 |
32 | ## Generate an application
33 |
34 | Run `nx g @nrwl/react:app my-app` to generate an application.
35 |
36 | > You can use any of the plugins above to generate applications as well.
37 |
38 | When using Nx, you can create multiple applications and libraries in the same workspace.
39 |
40 | ## Generate a library
41 |
42 | Run `nx g @nrwl/react:lib my-lib` to generate a library.
43 |
44 | > You can also use any of the plugins above to generate libraries as well.
45 |
46 | Libraries are shareable across libraries and applications. They can be imported from `@jitl/mylib`.
47 |
48 | ## Development server
49 |
50 | Run `nx serve my-app` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
51 |
52 | ## Code scaffolding
53 |
54 | Run `nx g @nrwl/react:component my-component --project=my-app` to generate a new component.
55 |
56 | ## Build
57 |
58 | Run `nx build my-app` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
59 |
60 | ## Running unit tests
61 |
62 | Run `nx test my-app` to execute the unit tests via [Jest](https://jestjs.io).
63 |
64 | Run `nx affected:test` to execute the unit tests affected by a change.
65 |
66 | ## Running end-to-end tests
67 |
68 | Run `ng e2e my-app` to execute the end-to-end tests via [Cypress](https://www.cypress.io).
69 |
70 | Run `nx affected:e2e` to execute the end-to-end tests affected by a change.
71 |
72 | ## Understand your workspace
73 |
74 | Run `nx dep-graph` to see a diagram of the dependencies of your projects.
75 |
76 | ## Further help
77 |
78 | Visit the [Nx Documentation](https://nx.dev) to learn more.
79 |
80 | ## ☁ Nx Cloud
81 |
82 | ### Distributed Computation Caching & Distributed Task Execution
83 |
84 |
85 |
86 | Nx Cloud pairs with Nx in order to enable you to build and test code more rapidly, by up to 10 times. Even teams that are new to Nx can connect to Nx Cloud and start saving time instantly.
87 |
88 | Teams using Nx gain the advantage of building full-stack applications with their preferred framework alongside Nx’s advanced code generation and project dependency graph, plus a unified experience for both frontend and backend developers.
89 |
90 | Visit [Nx Cloud](https://nx.app/) to learn more.
91 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "babelrcRoots": ["*"]
3 | }
4 |
--------------------------------------------------------------------------------
/esbuild-runner.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'transform', // bundle or transform (see description above)
3 | esbuild: {
4 | // Any esbuild build or transform options go here
5 | // target: 'esnext',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { getJestProjects } = require('@nrwl/jest');
2 |
3 | module.exports = {
4 | projects: getJestProjects(),
5 | };
6 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nrwl/jest/preset');
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@nrwl/workspace/presets/core.json",
3 | "npmScope": "jitl",
4 | "affected": {
5 | "defaultBase": "main"
6 | },
7 | "cli": {
8 | "defaultCollection": "@nrwl/react"
9 | },
10 | "tasksRunnerOptions": {
11 | "default": {
12 | "runner": "@nrwl/nx-cloud",
13 | "options": {
14 | "cacheableOperations": ["build", "lint", "test", "e2e"],
15 | "accessToken": "MjAxNWUzYmUtMjg4ZS00YmM2LWI0YjItNTM3ZjkwOTNjODBlfHJlYWQtd3JpdGU="
16 | }
17 | }
18 | },
19 | "generators": {
20 | "@nrwl/react": {
21 | "application": {
22 | "style": "@emotion/styled",
23 | "linter": "eslint",
24 | "babel": true
25 | },
26 | "component": {
27 | "style": "@emotion/styled"
28 | },
29 | "library": {
30 | "style": "@emotion/styled",
31 | "linter": "eslint"
32 | }
33 | }
34 | },
35 | "defaultProject": "playground"
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/monorepo",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "rebuild-notion": "./rebuild-api.sh"
7 | },
8 | "private": true,
9 | "dependencies": {
10 | "@emotion/react": "11.5.0",
11 | "@emotion/styled": "11.3.0",
12 | "core-js": "^3.6.5",
13 | "jotai": "npm:@jitl/jotai@^1.5.1",
14 | "react": "^17.0.2",
15 | "react-dom": "17.0.2",
16 | "regenerator-runtime": "0.13.7",
17 | "tslib": "^2.0.0"
18 | },
19 | "devDependencies": {
20 | "@emotion/babel-plugin": "11.3.0",
21 | "@nrwl/cli": "13.4.1",
22 | "@nrwl/cypress": "13.4.2",
23 | "@nrwl/eslint-plugin-nx": "13.4.1",
24 | "@nrwl/jest": "13.4.1",
25 | "@nrwl/js": "13.4.1",
26 | "@nrwl/linter": "13.4.1",
27 | "@nrwl/node": "^13.4.3",
28 | "@nrwl/nx-cloud": "latest",
29 | "@nrwl/react": "^13.4.2",
30 | "@nrwl/tao": "13.4.1",
31 | "@nrwl/web": "13.4.2",
32 | "@nrwl/workspace": "13.4.1",
33 | "@swc/cli": "~0.1.52",
34 | "@swc/core": "1.2.118",
35 | "@swc/helpers": "~0.2.14",
36 | "@testing-library/react": "12.1.2",
37 | "@testing-library/react-hooks": "7.0.2",
38 | "@testing-library/user-event": "^13.5.0",
39 | "@types/jest": "27.0.2",
40 | "@types/node": "14.14.33",
41 | "@types/react": "17.0.30",
42 | "@types/react-dom": "17.0.9",
43 | "@typescript-eslint/eslint-plugin": "~5.3.0",
44 | "@typescript-eslint/parser": "~5.3.0",
45 | "babel-jest": "27.2.3",
46 | "cypress": "^9.1.0",
47 | "esbuild": "^0.14.30",
48 | "esbuild-runner": "^2.2.1",
49 | "eslint": "8.2.0",
50 | "eslint-config-prettier": "8.1.0",
51 | "eslint-plugin-cypress": "^2.10.3",
52 | "eslint-plugin-import": "2.25.2",
53 | "eslint-plugin-jsx-a11y": "6.4.1",
54 | "eslint-plugin-react": "7.26.1",
55 | "eslint-plugin-react-hooks": "^4.3.0",
56 | "fast-check": "^2.20.0",
57 | "jest": "27.2.3",
58 | "prettier": "^2.3.1",
59 | "react-test-renderer": "17.0.2",
60 | "ts-jest": "27.0.5",
61 | "typescript": "4.6.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjake/monorepo/d1e87174827005fa7fd6d158a0a1d7e86dd2a396/packages/.gitkeep
--------------------------------------------------------------------------------
/packages/example-library/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/example-library/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/example-library/README.md:
--------------------------------------------------------------------------------
1 | # example-library
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test example-library` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/packages/example-library/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'example-library',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | transform: {
11 | '^.+\\.[tj]sx?$': 'ts-jest',
12 | },
13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
14 | coverageDirectory: '../../coverage/packages/example-library',
15 | };
16 |
--------------------------------------------------------------------------------
/packages/example-library/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/example-library",
3 | "version": "0.0.1",
4 | "dependencies": {
5 | "@stripe/react-stripe-js": "1.2.0",
6 | "@stripe/stripe-js": "1.11.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/example-library/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/example-library",
3 | "sourceRoot": "packages/example-library/src",
4 | "projectType": "library",
5 | "targets": {
6 | "lint": {
7 | "executor": "@nrwl/linter:eslint",
8 | "outputs": ["{options.outputFile}"],
9 | "options": {
10 | "lintFilePatterns": ["packages/example-library/**/*.ts"]
11 | }
12 | },
13 | "test": {
14 | "executor": "@nrwl/jest:jest",
15 | "outputs": ["coverage/packages/example-library"],
16 | "options": {
17 | "jestConfig": "packages/example-library/jest.config.js",
18 | "passWithNoTests": true
19 | }
20 | },
21 | "build": {
22 | "executor": "@nrwl/node:package",
23 | "outputs": ["{options.outputPath}"],
24 | "options": {
25 | "outputPath": "dist/packages/example-library",
26 | "tsConfig": "packages/example-library/tsconfig.lib.json",
27 | "packageJson": "packages/example-library/package.json",
28 | "main": "packages/example-library/src/index.ts",
29 | "assets": ["packages/example-library/*.md"]
30 | }
31 | }
32 | },
33 | "tags": []
34 | }
35 |
--------------------------------------------------------------------------------
/packages/example-library/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/example-library';
2 |
3 | export const DoesThisBreak = import('@stripe/react-stripe-js');
4 |
--------------------------------------------------------------------------------
/packages/example-library/src/lib/example-library.spec.ts:
--------------------------------------------------------------------------------
1 | import { exampleLibrary } from './example-library';
2 |
3 | describe('exampleLibrary', () => {
4 | it('should work', () => {
5 | expect(exampleLibrary()).toEqual('example-library');
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/example-library/src/lib/example-library.ts:
--------------------------------------------------------------------------------
1 | export function exampleLibrary(): string {
2 | return 'example-library';
3 | }
4 |
--------------------------------------------------------------------------------
/packages/example-library/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.lib.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/example-library/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "../../dist/out-tsc",
6 | "declaration": true,
7 | "composite": true,
8 | "types": ["node"]
9 | },
10 | "exclude": ["**/*.spec.ts", "**/*.test.ts"],
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/example-library/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "**/*.test.ts",
10 | "**/*.spec.ts",
11 | "**/*.test.tsx",
12 | "**/*.spec.tsx",
13 | "**/*.test.js",
14 | "**/*.spec.js",
15 | "**/*.test.jsx",
16 | "**/*.spec.jsx",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/example-library/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@stripe/react-stripe-js@1.2.0":
6 | version "1.2.0"
7 | resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.2.0.tgz#01be9eafcaeedd63f1ee3260c793a0c8422fb98d"
8 | integrity sha512-5zl9Tr/yvciSDPxYiWO8lDKlLJ8z0AceA5sqW3Hfe5i6g7nTJAWs1ciFZYFBeRFhdHG7xRFbZFgfKiFHN5yGrA==
9 | dependencies:
10 | prop-types "^15.7.2"
11 |
12 | "@stripe/stripe-js@1.11.0":
13 | version "1.11.0"
14 | resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.11.0.tgz#00e812d72a7760dae08237875066d263671478ee"
15 | integrity sha512-SDNZKuETBEVkernd1tq8tL6wNfVKrl24Txs3p+4NYxoaIbNaEO7mrln/2Y/WRcQBWjagvhDIM5I6+X1rfK0qhQ==
16 |
17 | "js-tokens@^3.0.0 || ^4.0.0":
18 | version "4.0.0"
19 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
20 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
21 |
22 | loose-envify@^1.4.0:
23 | version "1.4.0"
24 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
25 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
26 | dependencies:
27 | js-tokens "^3.0.0 || ^4.0.0"
28 |
29 | object-assign@^4.1.1:
30 | version "4.1.1"
31 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
32 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
33 |
34 | prop-types@^15.7.2:
35 | version "15.8.1"
36 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
37 | integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
38 | dependencies:
39 | loose-envify "^1.4.0"
40 | object-assign "^4.1.1"
41 | react-is "^16.13.1"
42 |
43 | react-is@^16.13.1:
44 | version "16.13.1"
45 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
46 | integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
47 |
--------------------------------------------------------------------------------
/packages/notion-api/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/notion-api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/notion-api/.gitignore:
--------------------------------------------------------------------------------
1 | src/example/cache
2 | src/example/assets
3 |
--------------------------------------------------------------------------------
/packages/notion-api/README.md:
--------------------------------------------------------------------------------
1 | # @jitl/notion-api
2 |
3 | The missing companion library for the official Notion public API.
4 |
5 | - Use Notion as a headless content management system a la Contentful.
6 | - Recursively fetch page content while building backlinks.
7 | - Convenient types like `Page` `Block` ..., plus helpers for tasks like
8 | iterating paginated API results.
9 | - Image, emoji, and content caching specifically designed for NextJS and
10 | incremental static regeneration.
11 |
12 | **This is not an official Notion product**. The current focus of this library is
13 | on _reading_ data from Notion.
14 |
15 | [Github](https://github.com/justjake/monorepo/tree/main/packages/notion-api) | [Full API documentation](https://github.com/justjake/monorepo/blob/main/packages/notion-api/doc/modules.md) | [NPM Package](https://www.npmjs.com/package/@jitl/notion-api)
16 |
17 | ## CMS
18 |
19 | The [CMS class](./doc/classes/CMS.md) is a wrapper around a Notion database. A
20 | CMS instance adds the following features:
21 |
22 | - Page content fetching and caching. Calling CMS methods to retrieve pages from
23 | the Notion API will only re-fetch the contents of the page if the page has been
24 | updated. Cached page content can optionally be persisted to disk as JSON files.
25 | - Optional cover image, icon and image block asset download, including images
26 | for unicode emojis.
27 | - Automatically derive a metadata object called `frontmatter` for each page, to
28 | reduce page property parsing boilerplate, and provide a type-safe API for your
29 | pages to the rest of your app.
30 | - Support for retrieving pages by a special [`slug`](./doc/interfaces/CMSConfig.md#slug) property
31 | suitable for use in a URL.
32 |
33 | ```typescript
34 | import {
35 | NotionClient, // re-exported official Notion client from peer dependencies
36 | NotionClientDebugLogger, // enable logs with DEBUG='@jitl/notion-api:*'
37 | CMS,
38 | richTextAsPlainText,
39 | } from '@jitl/notion-api';
40 |
41 | const Recipes = new CMS({
42 | database_id: 'a3aa29a6b2f242d1b4cf86fb578a5eea',
43 | notion: new NotionClient({
44 | logger: NotionClientDebugLogger,
45 | auth: process.env.NOTION_SECRET,
46 | }),
47 | slug: undefined, // Use page ID
48 | visible: true, // All pages visible
49 | getFrontmatter: (page) => ({
50 | /* TODO: return your custom metadata */
51 | }),
52 | cache: {
53 | directory: path.join(__dirname, './cache'),
54 | },
55 | assets: {
56 | directory: path.join(__dirname, './assets'),
57 | downloadExternalAssets: true,
58 | },
59 | });
60 |
61 | // Download and cache all pages in the Recipes database, and their assets.
62 | for await (const recipe of Recipes.query()) {
63 | console.log(
64 | 'Downloading assets for recipe: ',
65 | richTextAsPlainText(recipe.frontmatter.title)
66 | );
67 | await Recipes.downloadAssets(recipe);
68 | }
69 | ```
70 |
71 | ## API Types & Helpers
72 |
73 | This library exports many type aliases for working with data retrieved from the
74 | [official `@notionhq/client` library](https://github.com/makenotion/notion-sdk-js).
75 |
76 | These types are derived from the official library's publicly exported types.
77 | They will be compatible with @notionhq/client, but may change in unexpected ways
78 | after a @notionhq/client update.
79 |
80 | Abbreviated list of types: `Block`, `Page`, `RichText`,
81 | `RichTextToken`, `Mention`, `Property`, `PropertyFilter`, `User`, etc.
82 |
83 | There are several handy utility functions for working with those types, like
84 | `richTextAsPlainText(text)` and `getPropertyValue(page, propertyPointer)`.
85 |
86 | See the full list in [the API documentation](./doc/modules.md).
87 |
88 | ### iteratePaginatedAPI
89 |
90 | Dealing with pagination is annoying, but [necessary to avoid resource consumption](https://www.notion.so/blog/creating-the-notion-api#:~:text=Paginating%20block%20hierarchies).
91 |
92 | The `iteratePaginatedAPI` helper returns an `AsyncIterable- ` so you can
93 | iterate over Notion API results using the `for await (...) { ... }` syntax. This
94 | should work for any paginated API using Notion's official API client.
95 |
96 | ```typescript
97 | for await (const block of iteratePaginatedAPI(notion.blocks.children.list, {
98 | block_id: parentBlockId,
99 | })) {
100 | // Do something with block.
101 | }
102 | ```
103 |
104 | If you prefer a function approach and don't mind waiting for all values to load
105 | into memory, consider `asyncIterableToArray`:
106 |
107 | ```typescript
108 | const iterator = iteratePaginatedAPI(notion.blocks.children.list, {
109 | block_id: parentBlockId,
110 | });
111 | const blocks = await asyncIterableToArray(iterator);
112 | const paragraphs = blocks.filter((block) => isFullBlock(block, 'paragraph'));
113 | ```
114 |
115 | ### Partial response types
116 |
117 | The Notion API can sometimes return "partial" object data that contain only the block's ID:
118 |
119 | ```typescript
120 | // In @notionhq/client typings:
121 | type PartialBlockObjectResponse = { object: 'block'; id: string };
122 | export type GetBlockResponse = PartialBlockObjectResponse | BlockObjectResponse;
123 | ```
124 |
125 | Checking that a `GetBlockResponse` (or similar type) is a "full" block gets old
126 | pretty fast, so this library exports type guard functions to handle common
127 | cases, like `isFullPage(page)` and `isFullBlock(block)`.
128 |
129 | `isFullBlock` can optionally narrow the _type_ of block as well:
130 |
131 | ```typescript
132 | if (isFullBlock(block, 'paragraph')) {
133 | // It's a full paragraph block
134 | console.log(richTextAsPlainText(block.paragraph.text));
135 | }
136 | ```
137 |
138 | ### Block data
139 |
140 | Notion's API returns block data in a shape that is very difficult to deal with
141 | in a generic way while maintaining type-safety. Each block type has it's own
142 | property with the same name, and that property contains the block's data.
143 | Handling this type-safely means writing a long and annoying switch statement:
144 |
145 | ```typescript
146 | function getBlockTextContentBefore(block: Block): RichText | RichText[] {
147 | switch (block.type) {
148 | case 'paragraph':
149 | return block.paragraph.rich_text;
150 | case 'heading_1':
151 | return block.heading_1.rich_text;
152 | case 'heading_2':
153 | return block.heading_2.rich_text;
154 | // ... etc, for many more block types
155 | default:
156 | assertUnreachable(block); // Assert this switch is exhaustive
157 | }
158 | }
159 | ```
160 |
161 | Enter `getBlockData`. It returns a union of all possible interior data types for
162 | a `block` value. The same function can be re-written in a type-safe but
163 | non-exhaustive way in much fewer lines:
164 |
165 | ```typescript
166 | function getBlockTextContentAfter(block: Block): RichText[] {
167 | const blockData = getBlockData(block);
168 | const results: RichText[] = [];
169 | if ('rich_text' in blockData) {
170 | results.push(blockData.rich_text);
171 | }
172 | if ('caption' in blockData) {
173 | results.push(blockData.caption);
174 | }
175 | // Done.
176 | return results;
177 | }
178 | ```
179 |
180 | But because this function supports narrowed block types, you can still use a
181 | `switch (block.type)` if you want to be exhaustive, and tab completion will
182 | guide you:
183 |
184 | ```typescript
185 | function getBlockTextContentAfterExhaustive(
186 | block: Block
187 | ): RichText | RichText[] {
188 | switch (block.type) {
189 | case 'paragraph': // Fall-through for blocks with only rich_text
190 | case 'heading_1':
191 | case 'heading_2': // ... etc
192 | return getBlockData(block).rich_text;
193 | case 'image':
194 | return getBlockData(block).caption;
195 | case 'code':
196 | return [getBlockData(block).rich_text, getBlockData(block).caption];
197 | // ... etc
198 | default:
199 | assertUnreachable(block); // Assert this switch is exhaustive
200 | }
201 | }
202 | ```
203 |
204 | See the full list of functions in [the API documentation](./doc/modules.md).
205 |
206 | ## Stability & Support
207 |
208 | **API stability**: This library follows SemVer, and currently has a version less that 1.0.0,
209 | meaning [it is under initial development](https://semver.org/#spec-item-4). Do
210 | not expect API stability between versions, so specify an exact version in
211 | `package.json` or use a lockfile (`package-lock.json`, `yarn.loc` etc) to
212 | protect yourself from unexpected breaking changes.
213 |
214 | **Support**: As stated above, **this library not an official Notion product**. I wrote it for
215 | my own use, to support [my website](https://jake.tl) and other projects,
216 | although I welcome contributions of any kind. There are no automated tests yet.
217 |
218 | **TypeScript**: This library is developed with TypeScript 4.5.5, and is untested
219 | with other TypeScript versions.
220 |
221 | ## Development
222 |
223 | ### Monorepo
224 |
225 | This library is developed inside a monorepo, please see the root README.md for
226 | more information.
227 |
228 | ### Running unit tests
229 |
230 | Run `nx test notion-api` to execute the unit tests via [Jest](https://jestjs.io).
231 |
--------------------------------------------------------------------------------
/packages/notion-api/README.typedoc.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | This README documents the source code of `@jitl/notion-api`. For the general
4 | library information, see the [top-level README.md](../README.md).
5 |
6 | # Module `@jitl/notion-api`
7 |
8 | Module `@jitl/notion-api` provides extensions and helpers for the official
9 | Notion public API.
10 |
11 | This library uses `@notionhq/client` as a peer dependency for both types and
12 | to re-use the official client.
13 |
14 | The library is broadly separated into distinct feature sets:
15 |
16 | - A set of helpers for working with the Notion API. This includes
17 | common types derived from the API's response types, and some iteration helpers
18 | for fetching content. See the file [notion-api.ts](../src/lib/notion-api.ts)
19 | for details.
20 |
21 | - A content management system supporting functions for downloading and
22 | caching content from a Notion database. The high-level interface for these
23 | features is the [[CMS]] class in the file
24 | [content-management-system.ts](../src/lib/content-management-system.ts), but
25 | related lower-level tools for working with Notion assets are also exported.
26 |
27 | ## Full API docs
28 |
29 | See [modules.md](./modules.md) for the full API list.
30 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/.nojekyll:
--------------------------------------------------------------------------------
1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
--------------------------------------------------------------------------------
/packages/notion-api/doc/README.md:
--------------------------------------------------------------------------------
1 | @jitl/notion-api / [Exports](modules.md)
2 |
3 |
4 |
5 | This README documents the source code of `@jitl/notion-api`. For the general
6 | library information, see the [top-level README.md](../README.md).
7 |
8 | # Module `@jitl/notion-api`
9 |
10 | Module `@jitl/notion-api` provides extensions and helpers for the official
11 | Notion public API.
12 |
13 | This library uses `@notionhq/client` as a peer dependency for both types and
14 | to re-use the official client.
15 |
16 | The library is broadly separated into distinct feature sets:
17 |
18 | - A set of helpers for working with the Notion API. This includes
19 | common types derived from the API's response types, and some iteration helpers
20 | for fetching content. See the file [notion-api.ts](../src/lib/notion-api.ts)
21 | for details.
22 |
23 | - A content management system supporting functions for downloading and
24 | caching content from a Notion database. The high-level interface for these
25 | features is the [CMS](classes/CMS.md) class in the file
26 | [content-management-system.ts](../src/lib/content-management-system.ts), but
27 | related lower-level tools for working with Notion assets are also exported.
28 |
29 | ## Full API docs
30 |
31 | See [modules.md](./modules.md) for the full API list.
32 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/classes/Backlinks.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / Backlinks
2 |
3 | # Class: Backlinks
4 |
5 | Records links from a page to other pages.
6 | See [buildBacklinks](../modules.md#buildbacklinks).
7 |
8 | ## Table of contents
9 |
10 | ### Constructors
11 |
12 | - [constructor](Backlinks.md#constructor)
13 |
14 | ### Properties
15 |
16 | - [linksToPage](Backlinks.md#linkstopage)
17 |
18 | ### Methods
19 |
20 | - [add](Backlinks.md#add)
21 | - [maybeAddUrl](Backlinks.md#maybeaddurl)
22 | - [maybeAddTextToken](Backlinks.md#maybeaddtexttoken)
23 | - [getLinksToPage](Backlinks.md#getlinkstopage)
24 | - [deleteBacklinksFromPage](Backlinks.md#deletebacklinksfrompage)
25 |
26 | ## Constructors
27 |
28 | ### constructor
29 |
30 | • **new Backlinks**()
31 |
32 | ## Properties
33 |
34 | ### linksToPage
35 |
36 | • **linksToPage**: `Map`<`string`, [`Backlink`](../interfaces/Backlink.md)[]\>
37 |
38 | #### Defined in
39 |
40 | [lib/backlinks.ts:50](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L50)
41 |
42 | ## Methods
43 |
44 | ### add
45 |
46 | ▸ **add**(`args`): [`Backlink`](../interfaces/Backlink.md)
47 |
48 | #### Parameters
49 |
50 | | Name | Type |
51 | | :------ | :------ |
52 | | `args` | [`Backlink`](../interfaces/Backlink.md) |
53 |
54 | #### Returns
55 |
56 | [`Backlink`](../interfaces/Backlink.md)
57 |
58 | #### Defined in
59 |
60 | [lib/backlinks.ts:53](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L53)
61 |
62 | ___
63 |
64 | ### maybeAddUrl
65 |
66 | ▸ **maybeAddUrl**(`url`, `from`): `undefined` \| [`Backlink`](../interfaces/Backlink.md)
67 |
68 | #### Parameters
69 |
70 | | Name | Type |
71 | | :------ | :------ |
72 | | `url` | `string` |
73 | | `from` | [`BacklinkFrom`](../interfaces/BacklinkFrom.md) |
74 |
75 | #### Returns
76 |
77 | `undefined` \| [`Backlink`](../interfaces/Backlink.md)
78 |
79 | #### Defined in
80 |
81 | [lib/backlinks.ts:68](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L68)
82 |
83 | ___
84 |
85 | ### maybeAddTextToken
86 |
87 | ▸ **maybeAddTextToken**(`token`, `from`): `undefined` \| [`Backlink`](../interfaces/Backlink.md)
88 |
89 | #### Parameters
90 |
91 | | Name | Type |
92 | | :------ | :------ |
93 | | `token` | `RichTextItemResponse` |
94 | | `from` | [`BacklinkFrom`](../interfaces/BacklinkFrom.md) |
95 |
96 | #### Returns
97 |
98 | `undefined` \| [`Backlink`](../interfaces/Backlink.md)
99 |
100 | #### Defined in
101 |
102 | [lib/backlinks.ts:91](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L91)
103 |
104 | ___
105 |
106 | ### getLinksToPage
107 |
108 | ▸ **getLinksToPage**(`pageId`): [`Backlink`](../interfaces/Backlink.md)[]
109 |
110 | #### Parameters
111 |
112 | | Name | Type |
113 | | :------ | :------ |
114 | | `pageId` | `string` |
115 |
116 | #### Returns
117 |
118 | [`Backlink`](../interfaces/Backlink.md)[]
119 |
120 | #### Defined in
121 |
122 | [lib/backlinks.ts:112](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L112)
123 |
124 | ___
125 |
126 | ### deleteBacklinksFromPage
127 |
128 | ▸ **deleteBacklinksFromPage**(`mentionedFromPageId`): `void`
129 |
130 | When we re-fetch a page and its children, we need to invalidate the old
131 | backlink data from those trees
132 |
133 | #### Parameters
134 |
135 | | Name | Type |
136 | | :------ | :------ |
137 | | `mentionedFromPageId` | `string` |
138 |
139 | #### Returns
140 |
141 | `void`
142 |
143 | #### Defined in
144 |
145 | [lib/backlinks.ts:120](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L120)
146 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/classes/CMSPropertyResolver.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSPropertyResolver
2 |
3 | # Class: CMSPropertyResolver
4 |
5 | Resolve [CMSConfig](../interfaces/CMSConfig.md) options to property pointers.
6 | This is implemented as a separate class from [CMS](CMS.md) to improve type inference.
7 | See [CMS.propertyResolver](CMS.md#propertyresolver).
8 |
9 | ## Type parameters
10 |
11 | | Name | Type |
12 | | :------ | :------ |
13 | | `CustomFrontmatter` | `CustomFrontmatter` |
14 | | `Schema` | extends [`PartialDatabaseSchema`](../modules.md#partialdatabaseschema) |
15 |
16 | ## Table of contents
17 |
18 | ### Constructors
19 |
20 | - [constructor](CMSPropertyResolver.md#constructor)
21 |
22 | ### Properties
23 |
24 | - [config](CMSPropertyResolver.md#config)
25 |
26 | ### Methods
27 |
28 | - [resolveSlugPropertyPointer](CMSPropertyResolver.md#resolveslugpropertypointer)
29 | - [resolveVisiblePropertyPointer](CMSPropertyResolver.md#resolvevisiblepropertypointer)
30 | - [resolveCustomPropertyPointer](CMSPropertyResolver.md#resolvecustompropertypointer)
31 |
32 | ## Constructors
33 |
34 | ### constructor
35 |
36 | • **new CMSPropertyResolver**<`CustomFrontmatter`, `Schema`\>(`cms`)
37 |
38 | #### Type parameters
39 |
40 | | Name | Type |
41 | | :------ | :------ |
42 | | `CustomFrontmatter` | `CustomFrontmatter` |
43 | | `Schema` | extends [`PartialDatabaseSchema`](../modules.md#partialdatabaseschema) |
44 |
45 | #### Parameters
46 |
47 | | Name | Type |
48 | | :------ | :------ |
49 | | `cms` | [`CMS`](CMS.md)<`CustomFrontmatter`, `Schema`\> |
50 |
51 | #### Defined in
52 |
53 | [lib/content-management-system.ts:1010](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L1010)
54 |
55 | ## Properties
56 |
57 | ### config
58 |
59 | • **config**: [`CMSConfig`](../interfaces/CMSConfig.md)<`CustomFrontmatter`, `Schema`\>
60 |
61 | #### Defined in
62 |
63 | [lib/content-management-system.ts:1009](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L1009)
64 |
65 | ## Methods
66 |
67 | ### resolveSlugPropertyPointer
68 |
69 | ▸ **resolveSlugPropertyPointer**(): `any`
70 |
71 | If `config.slug` is a property pointer, returns it as a [PropertyPointer](../interfaces/PropertyPointer.md).
72 |
73 | #### Returns
74 |
75 | `any`
76 |
77 | #### Defined in
78 |
79 | [lib/content-management-system.ts:1015](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L1015)
80 |
81 | ___
82 |
83 | ### resolveVisiblePropertyPointer
84 |
85 | ▸ **resolveVisiblePropertyPointer**(): `undefined` \| [`PropertyPointerWithOutput`](../modules.md#propertypointerwithoutput)<`boolean` \| {} \| {} \| {} \| {}\>
86 |
87 | If `config.visible` is a property pointer, returns it as a [PropertyPointer](../interfaces/PropertyPointer.md).
88 |
89 | #### Returns
90 |
91 | `undefined` \| [`PropertyPointerWithOutput`](../modules.md#propertypointerwithoutput)<`boolean` \| {} \| {} \| {} \| {}\>
92 |
93 | #### Defined in
94 |
95 | [lib/content-management-system.ts:1022](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L1022)
96 |
97 | ___
98 |
99 | ### resolveCustomPropertyPointer
100 |
101 | ▸ **resolveCustomPropertyPointer**<`T`\>(`customProperty`): `undefined` \| [`PropertyPointerWithOutput`](../modules.md#propertypointerwithoutput)<`T`\>
102 |
103 | #### Type parameters
104 |
105 | | Name |
106 | | :------ |
107 | | `T` |
108 |
109 | #### Parameters
110 |
111 | | Name | Type |
112 | | :------ | :------ |
113 | | `customProperty` | [`CMSCustomProperty`](../modules.md#cmscustomproperty)<`T`, `CustomFrontmatter`, `Schema`\> |
114 |
115 | #### Returns
116 |
117 | `undefined` \| [`PropertyPointerWithOutput`](../modules.md#propertypointerwithoutput)<`T`\>
118 |
119 | #### Defined in
120 |
121 | [lib/content-management-system.ts:1030](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L1030)
122 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/classes/NotionObjectIndex.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / NotionObjectIndex
2 |
3 | # Class: NotionObjectIndex
4 |
5 | Stores values from the Notion API.
6 |
7 | ## Table of contents
8 |
9 | ### Constructors
10 |
11 | - [constructor](NotionObjectIndex.md#constructor)
12 |
13 | ### Properties
14 |
15 | - [page](NotionObjectIndex.md#page)
16 | - [pageWithChildren](NotionObjectIndex.md#pagewithchildren)
17 | - [block](NotionObjectIndex.md#block)
18 | - [blockWithChildren](NotionObjectIndex.md#blockwithchildren)
19 | - [asset](NotionObjectIndex.md#asset)
20 | - [parentId](NotionObjectIndex.md#parentid)
21 | - [parentPageId](NotionObjectIndex.md#parentpageid)
22 |
23 | ### Methods
24 |
25 | - [addBlock](NotionObjectIndex.md#addblock)
26 | - [addPage](NotionObjectIndex.md#addpage)
27 | - [addAsset](NotionObjectIndex.md#addasset)
28 |
29 | ## Constructors
30 |
31 | ### constructor
32 |
33 | • **new NotionObjectIndex**()
34 |
35 | ## Properties
36 |
37 | ### page
38 |
39 | • **page**: `Map`<`string`, {}\>
40 |
41 | Whole pages
42 |
43 | #### Defined in
44 |
45 | [lib/cache.ts:92](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L92)
46 |
47 | ___
48 |
49 | ### pageWithChildren
50 |
51 | • **pageWithChildren**: `Map`<`string`, [`PageWithChildren`](../modules.md#pagewithchildren)\>
52 |
53 | #### Defined in
54 |
55 | [lib/cache.ts:93](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L93)
56 |
57 | ___
58 |
59 | ### block
60 |
61 | • **block**: `Map`<`string`, [`Block`](../modules.md#block)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\>\>
62 |
63 | Whole blocks
64 |
65 | #### Defined in
66 |
67 | [lib/cache.ts:96](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L96)
68 |
69 | ___
70 |
71 | ### blockWithChildren
72 |
73 | • **blockWithChildren**: `Map`<`string`, [`BlockWithChildren`](../modules.md#blockwithchildren)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\>\>
74 |
75 | #### Defined in
76 |
77 | [lib/cache.ts:97](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L97)
78 |
79 | ___
80 |
81 | ### asset
82 |
83 | • **asset**: `Map`<`string`, [`Asset`](../modules.md#asset)\>
84 |
85 | Assets inside a block, page, etc. These are keyed by `getAssetRequestKey`.
86 |
87 | #### Defined in
88 |
89 | [lib/cache.ts:100](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L100)
90 |
91 | ___
92 |
93 | ### parentId
94 |
95 | • **parentId**: `Map`<`string`, `string`\>
96 |
97 | Parent block ID, may also be a page ID.
98 |
99 | #### Defined in
100 |
101 | [lib/cache.ts:103](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L103)
102 |
103 | ___
104 |
105 | ### parentPageId
106 |
107 | • **parentPageId**: `Map`<`string`, `undefined` \| `string`\>
108 |
109 | Parent page ID.
110 |
111 | #### Defined in
112 |
113 | [lib/cache.ts:106](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L106)
114 |
115 | ## Methods
116 |
117 | ### addBlock
118 |
119 | ▸ **addBlock**(`block`, `parent`): `void`
120 |
121 | #### Parameters
122 |
123 | | Name | Type |
124 | | :------ | :------ |
125 | | `block` | [`Block`](../modules.md#block)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\> \| [`BlockWithChildren`](../modules.md#blockwithchildren)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\> |
126 | | `parent` | `undefined` \| `string` \| {} \| [`Block`](../modules.md#block)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\> |
127 |
128 | #### Returns
129 |
130 | `void`
131 |
132 | #### Defined in
133 |
134 | [lib/cache.ts:108](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L108)
135 |
136 | ___
137 |
138 | ### addPage
139 |
140 | ▸ **addPage**(`page`): `void`
141 |
142 | #### Parameters
143 |
144 | | Name | Type |
145 | | :------ | :------ |
146 | | `page` | {} \| [`PageWithChildren`](../modules.md#pagewithchildren) |
147 |
148 | #### Returns
149 |
150 | `void`
151 |
152 | #### Defined in
153 |
154 | [lib/cache.ts:145](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L145)
155 |
156 | ___
157 |
158 | ### addAsset
159 |
160 | ▸ **addAsset**(`request`, `asset`): `void`
161 |
162 | #### Parameters
163 |
164 | | Name | Type |
165 | | :------ | :------ |
166 | | `request` | [`AssetRequest`](../modules.md#assetrequest) |
167 | | `asset` | [`Asset`](../modules.md#asset) |
168 |
169 | #### Returns
170 |
171 | `void`
172 |
173 | #### Defined in
174 |
175 | [lib/cache.ts:163](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/cache.ts#L163)
176 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/AssetRequestNextJSQuery.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / AssetRequestNextJSQuery
2 |
3 | # Interface: AssetRequestNextJSQuery
4 |
5 | ## Hierarchy
6 |
7 | - `NextJSQuery`
8 |
9 | ↳ **`AssetRequestNextJSQuery`**
10 |
11 | ## Table of contents
12 |
13 | ### Properties
14 |
15 | - [asset\_request](AssetRequestNextJSQuery.md#asset_request)
16 |
17 | ## Properties
18 |
19 | ### asset\_request
20 |
21 | • **asset\_request**: [object: string, id: string, field: string]
22 |
23 | #### Defined in
24 |
25 | [lib/assets.ts:143](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/assets.ts#L143)
26 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/Backlink.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / Backlink
2 |
3 | # Interface: Backlink
4 |
5 | A link from one block to another page.
6 |
7 | ## Hierarchy
8 |
9 | - [`BacklinkFrom`](BacklinkFrom.md)
10 |
11 | ↳ **`Backlink`**
12 |
13 | ## Table of contents
14 |
15 | ### Properties
16 |
17 | - [mentionedFromPageId](Backlink.md#mentionedfrompageid)
18 | - [mentionedFromBlockId](Backlink.md#mentionedfromblockid)
19 | - [mentionedPageId](Backlink.md#mentionedpageid)
20 |
21 | ## Properties
22 |
23 | ### mentionedFromPageId
24 |
25 | • **mentionedFromPageId**: `string`
26 |
27 | #### Inherited from
28 |
29 | [BacklinkFrom](BacklinkFrom.md).[mentionedFromPageId](BacklinkFrom.md#mentionedfrompageid)
30 |
31 | #### Defined in
32 |
33 | [lib/backlinks.ts:21](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L21)
34 |
35 | ___
36 |
37 | ### mentionedFromBlockId
38 |
39 | • **mentionedFromBlockId**: `string`
40 |
41 | #### Inherited from
42 |
43 | [BacklinkFrom](BacklinkFrom.md).[mentionedFromBlockId](BacklinkFrom.md#mentionedfromblockid)
44 |
45 | #### Defined in
46 |
47 | [lib/backlinks.ts:22](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L22)
48 |
49 | ___
50 |
51 | ### mentionedPageId
52 |
53 | • **mentionedPageId**: `string`
54 |
55 | #### Defined in
56 |
57 | [lib/backlinks.ts:30](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L30)
58 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/BacklinkFrom.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / BacklinkFrom
2 |
3 | # Interface: BacklinkFrom
4 |
5 | Where a page was mentioned from
6 |
7 | ## Hierarchy
8 |
9 | - **`BacklinkFrom`**
10 |
11 | ↳ [`Backlink`](Backlink.md)
12 |
13 | ## Table of contents
14 |
15 | ### Properties
16 |
17 | - [mentionedFromPageId](BacklinkFrom.md#mentionedfrompageid)
18 | - [mentionedFromBlockId](BacklinkFrom.md#mentionedfromblockid)
19 |
20 | ## Properties
21 |
22 | ### mentionedFromPageId
23 |
24 | • **mentionedFromPageId**: `string`
25 |
26 | #### Defined in
27 |
28 | [lib/backlinks.ts:21](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L21)
29 |
30 | ___
31 |
32 | ### mentionedFromBlockId
33 |
34 | • **mentionedFromBlockId**: `string`
35 |
36 | #### Defined in
37 |
38 | [lib/backlinks.ts:22](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/backlinks.ts#L22)
39 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/BlockFilterFunction.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / BlockFilterFunction
2 |
3 | # Interface: BlockFilterFunction
4 |
5 | ## Type parameters
6 |
7 | | Name | Type |
8 | | :------ | :------ |
9 | | `Type` | extends [`BlockType`](../modules.md#blocktype) |
10 |
11 | ## Callable
12 |
13 | ### BlockFilterFunction
14 |
15 | ▸ **BlockFilterFunction**(`block`): block is Block
16 |
17 | Filter function returned by [isFullBlockFilter](../modules.md#isfullblockfilter).
18 |
19 | #### Parameters
20 |
21 | | Name | Type |
22 | | :------ | :------ |
23 | | `block` | `GetBlockResponse` |
24 |
25 | #### Returns
26 |
27 | block is Block
28 |
29 | #### Defined in
30 |
31 | [lib/notion-api.ts:255](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L255)
32 |
33 | ### BlockFilterFunction
34 |
35 | ▸ **BlockFilterFunction**(`block`): block is BlockWithChildren
36 |
37 | Filter function returned by [isFullBlockFilter](../modules.md#isfullblockfilter).
38 |
39 | #### Parameters
40 |
41 | | Name | Type |
42 | | :------ | :------ |
43 | | `block` | [`BlockWithChildren`](../modules.md#blockwithchildren)<``"paragraph"`` \| ``"heading_1"`` \| ``"heading_2"`` \| ``"heading_3"`` \| ``"bulleted_list_item"`` \| ``"numbered_list_item"`` \| ``"quote"`` \| ``"to_do"`` \| ``"toggle"`` \| ``"template"`` \| ``"synced_block"`` \| ``"child_page"`` \| ``"child_database"`` \| ``"equation"`` \| ``"code"`` \| ``"callout"`` \| ``"divider"`` \| ``"breadcrumb"`` \| ``"table_of_contents"`` \| ``"column_list"`` \| ``"column"`` \| ``"link_to_page"`` \| ``"table"`` \| ``"table_row"`` \| ``"embed"`` \| ``"bookmark"`` \| ``"image"`` \| ``"video"`` \| ``"pdf"`` \| ``"file"`` \| ``"audio"`` \| ``"link_preview"`` \| ``"unsupported"``\> |
44 |
45 | #### Returns
46 |
47 | block is BlockWithChildren
48 |
49 | #### Defined in
50 |
51 | [lib/notion-api.ts:256](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L256)
52 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSCustomPropertyDerived.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSCustomPropertyDerived
2 |
3 | # Interface: CMSCustomPropertyDerived
4 |
5 | Specifies that the CMS should compute a value for the page using a function.
6 |
7 | See [CMSConfig](CMSConfig.md).
8 |
9 | **`source`**
10 |
11 | ```typescript
12 | export interface CMSCustomPropertyDerived {
13 | type: 'derived';
14 | /** Computes the custom property value from the page using a function */
15 | derive: (args: {
16 | page: Page; /* TODO properties: DatabasePropertyValues */
17 | }, cms: CMS) => T | Promise;
18 | }
19 | ```
20 |
21 | ## Type parameters
22 |
23 | | Name | Type |
24 | | :------ | :------ |
25 | | `T` | `T` |
26 | | `CustomFrontmatter` | `CustomFrontmatter` |
27 | | `Schema` | extends [`PartialDatabaseSchema`](../modules.md#partialdatabaseschema) |
28 |
29 | ## Table of contents
30 |
31 | ### Properties
32 |
33 | - [type](CMSCustomPropertyDerived.md#type)
34 |
35 | ### Methods
36 |
37 | - [derive](CMSCustomPropertyDerived.md#derive)
38 |
39 | ## Properties
40 |
41 | ### type
42 |
43 | • **type**: ``"derived"``
44 |
45 | #### Defined in
46 |
47 | [lib/content-management-system.ts:90](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L90)
48 |
49 | ## Methods
50 |
51 | ### derive
52 |
53 | ▸ **derive**(`args`, `cms`): `T` \| `Promise`<`T`\>
54 |
55 | Computes the custom property value from the page using a function
56 |
57 | #### Parameters
58 |
59 | | Name | Type |
60 | | :------ | :------ |
61 | | `args` | `Object` |
62 | | `args.page` | `Object` |
63 | | `cms` | [`CMS`](../classes/CMS.md)<`CustomFrontmatter`, `Schema`\> |
64 |
65 | #### Returns
66 |
67 | `T` \| `Promise`<`T`\>
68 |
69 | #### Defined in
70 |
71 | [lib/content-management-system.ts:92](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L92)
72 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSDefaultFrontmatter.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSDefaultFrontmatter
2 |
3 | # Interface: CMSDefaultFrontmatter
4 |
5 | All [CMSPage](CMSPage.md)s have at least this frontmatter.
6 |
7 | **`source`**
8 |
9 | ```typescript
10 | export interface CMSDefaultFrontmatter {
11 | title: RichText | string;
12 | slug: string;
13 | visible: boolean;
14 | }
15 | ```
16 |
17 | ## Table of contents
18 |
19 | ### Properties
20 |
21 | - [title](CMSDefaultFrontmatter.md#title)
22 | - [slug](CMSDefaultFrontmatter.md#slug)
23 | - [visible](CMSDefaultFrontmatter.md#visible)
24 |
25 | ## Properties
26 |
27 | ### title
28 |
29 | • **title**: `string` \| `RichTextItemResponse`[]
30 |
31 | #### Defined in
32 |
33 | [lib/content-management-system.ts:335](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L335)
34 |
35 | ___
36 |
37 | ### slug
38 |
39 | • **slug**: `string`
40 |
41 | #### Defined in
42 |
43 | [lib/content-management-system.ts:336](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L336)
44 |
45 | ___
46 |
47 | ### visible
48 |
49 | • **visible**: `boolean`
50 |
51 | #### Defined in
52 |
53 | [lib/content-management-system.ts:337](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L337)
54 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSPage.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSPage
2 |
3 | # Interface: CMSPage
4 |
5 | A CMSPage is a Notion page and its computed CMS frontmatter.
6 |
7 | ## Type parameters
8 |
9 | | Name |
10 | | :------ |
11 | | `CustomFrontmatter` |
12 |
13 | ## Table of contents
14 |
15 | ### Properties
16 |
17 | - [frontmatter](CMSPage.md#frontmatter)
18 | - [content](CMSPage.md#content)
19 |
20 | ## Properties
21 |
22 | ### frontmatter
23 |
24 | • **frontmatter**: [`CMSFrontmatter`](../modules.md#cmsfrontmatter)<`CustomFrontmatter`\>
25 |
26 | #### Defined in
27 |
28 | [lib/content-management-system.ts:353](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L353)
29 |
30 | ___
31 |
32 | ### content
33 |
34 | • **content**: [`PageWithChildren`](../modules.md#pagewithchildren)
35 |
36 | #### Defined in
37 |
38 | [lib/content-management-system.ts:354](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L354)
39 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSQueryParametersOptions.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSQueryParametersOptions
2 |
3 | # Interface: CMSQueryParametersOptions
4 |
5 | Options of [CMS.getQueryParameters](../classes/CMS.md#getqueryparameters)
6 |
7 | ## Hierarchy
8 |
9 | - [`CMSRetrieveOptions`](CMSRetrieveOptions.md)
10 |
11 | ↳ **`CMSQueryParametersOptions`**
12 |
13 | ## Table of contents
14 |
15 | ### Properties
16 |
17 | - [showInvisible](CMSQueryParametersOptions.md#showinvisible)
18 | - [slug](CMSQueryParametersOptions.md#slug)
19 |
20 | ## Properties
21 |
22 | ### showInvisible
23 |
24 | • `Optional` **showInvisible**: `boolean`
25 |
26 | If true, ignore the `visible` property of any retrieved [CMSPage](CMSPage.md)s by always considering them visible.
27 |
28 | #### Inherited from
29 |
30 | [CMSRetrieveOptions](CMSRetrieveOptions.md).[showInvisible](CMSRetrieveOptions.md#showinvisible)
31 |
32 | #### Defined in
33 |
34 | [lib/content-management-system.ts:375](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L375)
35 |
36 | ___
37 |
38 | ### slug
39 |
40 | • `Optional` **slug**: `string`
41 |
42 | Get the query used for retrieving this slug
43 |
44 | #### Defined in
45 |
46 | [lib/content-management-system.ts:384](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L384)
47 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSRetrieveOptions.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSRetrieveOptions
2 |
3 | # Interface: CMSRetrieveOptions
4 |
5 | Options for [CMS](../classes/CMS.md) retrieve methods.
6 |
7 | ## Hierarchy
8 |
9 | - **`CMSRetrieveOptions`**
10 |
11 | ↳ [`CMSQueryParametersOptions`](CMSQueryParametersOptions.md)
12 |
13 | ↳ [`CMSScopeOptions`](CMSScopeOptions.md)
14 |
15 | ## Table of contents
16 |
17 | ### Properties
18 |
19 | - [showInvisible](CMSRetrieveOptions.md#showinvisible)
20 |
21 | ## Properties
22 |
23 | ### showInvisible
24 |
25 | • `Optional` **showInvisible**: `boolean`
26 |
27 | If true, ignore the `visible` property of any retrieved [CMSPage](CMSPage.md)s by always considering them visible.
28 |
29 | #### Defined in
30 |
31 | [lib/content-management-system.ts:375](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L375)
32 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSScope.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSScope
2 |
3 | # Interface: CMSScope
4 |
5 | A query scope inside of a [CMS](../classes/CMS.md).
6 | A scope is a way to save and compose common query options.
7 |
8 | ```typescript
9 | const invisibleScope = cms.scope({ filter: cms.getVisibleEqualsFilter(false), showInvisible: true })
10 | const recentlyChanged = invisibleScope.query({ filter: cms.filter.updatedTime.last_week({}) })
11 | ```
12 |
13 | ## Type parameters
14 |
15 | | Name |
16 | | :------ |
17 | | `CustomFrontmatter` |
18 |
19 | ## Implemented by
20 |
21 | - [`CMS`](../classes/CMS.md)
22 |
23 | ## Table of contents
24 |
25 | ### Methods
26 |
27 | - [query](CMSScope.md#query)
28 | - [scope](CMSScope.md#scope)
29 | - [getQueryParameters](CMSScope.md#getqueryparameters)
30 |
31 | ## Methods
32 |
33 | ### query
34 |
35 | ▸ **query**(`args?`, `options?`): `AsyncIterableIterator`<[`CMSPage`](CMSPage.md)<`CustomFrontmatter`\>\>
36 |
37 | Query the database, returning all matching [CMSPage](CMSPage.md)s.
38 |
39 | #### Parameters
40 |
41 | | Name | Type |
42 | | :------ | :------ |
43 | | `args?` | `Object` |
44 | | `args.filter?` | [`Filter`](../modules.md#filter) |
45 | | `args.sorts?` | ({} \| {})[] |
46 | | `options?` | [`CMSRetrieveOptions`](CMSRetrieveOptions.md) |
47 |
48 | #### Returns
49 |
50 | `AsyncIterableIterator`<[`CMSPage`](CMSPage.md)<`CustomFrontmatter`\>\>
51 |
52 | #### Defined in
53 |
54 | [lib/content-management-system.ts:413](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L413)
55 |
56 | ___
57 |
58 | ### scope
59 |
60 | ▸ **scope**(`options`): [`CMSScope`](CMSScope.md)<`CustomFrontmatter`\>
61 |
62 | #### Parameters
63 |
64 | | Name | Type |
65 | | :------ | :------ |
66 | | `options` | [`CMSScopeOptions`](CMSScopeOptions.md) |
67 |
68 | #### Returns
69 |
70 | [`CMSScope`](CMSScope.md)<`CustomFrontmatter`\>
71 |
72 | A child scope within this scope.
73 |
74 | #### Defined in
75 |
76 | [lib/content-management-system.ts:424](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L424)
77 |
78 | ___
79 |
80 | ### getQueryParameters
81 |
82 | ▸ **getQueryParameters**(`options`): `QueryDatabaseParameters`
83 |
84 | #### Parameters
85 |
86 | | Name | Type |
87 | | :------ | :------ |
88 | | `options` | [`CMSQueryParametersOptions`](CMSQueryParametersOptions.md) |
89 |
90 | #### Returns
91 |
92 | `QueryDatabaseParameters`
93 |
94 | the Notion API QueryDatabaseParameters used as the basis for queries made by this object.
95 |
96 | #### Defined in
97 |
98 | [lib/content-management-system.ts:429](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L429)
99 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/CMSScopeOptions.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / CMSScopeOptions
2 |
3 | # Interface: CMSScopeOptions
4 |
5 | Options of [CMS.scope](../classes/CMS.md#scope), [CMSScope.scope](CMSScope.md#scope)
6 |
7 | ## Hierarchy
8 |
9 | - [`CMSRetrieveOptions`](CMSRetrieveOptions.md)
10 |
11 | ↳ **`CMSScopeOptions`**
12 |
13 | ## Table of contents
14 |
15 | ### Properties
16 |
17 | - [showInvisible](CMSScopeOptions.md#showinvisible)
18 | - [filter](CMSScopeOptions.md#filter)
19 | - [sorts](CMSScopeOptions.md#sorts)
20 |
21 | ## Properties
22 |
23 | ### showInvisible
24 |
25 | • `Optional` **showInvisible**: `boolean`
26 |
27 | If true, ignore the `visible` property of any retrieved [CMSPage](CMSPage.md)s by always considering them visible.
28 |
29 | #### Inherited from
30 |
31 | [CMSRetrieveOptions](CMSRetrieveOptions.md).[showInvisible](CMSRetrieveOptions.md#showinvisible)
32 |
33 | #### Defined in
34 |
35 | [lib/content-management-system.ts:375](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L375)
36 |
37 | ___
38 |
39 | ### filter
40 |
41 | • `Optional` **filter**: [`Filter`](../modules.md#filter)
42 |
43 | Apply these filters to all queries made inside the scope
44 |
45 | #### Defined in
46 |
47 | [lib/content-management-system.ts:393](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L393)
48 |
49 | ___
50 |
51 | ### sorts
52 |
53 | • `Optional` **sorts**: ({} \| {})[]
54 |
55 | Apply these sorts to all queries made inside the scope. These take precedence over but do not remove the parent scope's sorts.
56 |
57 | #### Defined in
58 |
59 | [lib/content-management-system.ts:395](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/content-management-system.ts#L395)
60 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/PaginatedArgs.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / PaginatedArgs
2 |
3 | # Interface: PaginatedArgs
4 |
5 | Common arguments for paginated APIs.
6 |
7 | **`source`**
8 | ```typescript
9 | export interface PaginatedArgs {
10 | start_cursor?: string;
11 | page_size?: number;
12 | }
13 | ```
14 |
15 | ## Table of contents
16 |
17 | ### Properties
18 |
19 | - [start\_cursor](PaginatedArgs.md#start_cursor)
20 | - [page\_size](PaginatedArgs.md#page_size)
21 |
22 | ## Properties
23 |
24 | ### start\_cursor
25 |
26 | • `Optional` **start\_cursor**: `string`
27 |
28 | #### Defined in
29 |
30 | [lib/notion-api.ts:103](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L103)
31 |
32 | ___
33 |
34 | ### page\_size
35 |
36 | • `Optional` **page\_size**: `number`
37 |
38 | #### Defined in
39 |
40 | [lib/notion-api.ts:104](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L104)
41 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/PaginatedList.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / PaginatedList
2 |
3 | # Interface: PaginatedList
4 |
5 | A page of results from the Notion API.
6 |
7 | **`source`**
8 | ```typescript
9 | export interface PaginatedList {
10 | object: 'list';
11 | results: T[];
12 | next_cursor: string | null;
13 | has_more: boolean;
14 | }
15 | ```
16 |
17 | ## Type parameters
18 |
19 | | Name |
20 | | :------ |
21 | | `T` |
22 |
23 | ## Table of contents
24 |
25 | ### Properties
26 |
27 | - [object](PaginatedList.md#object)
28 | - [results](PaginatedList.md#results)
29 | - [next\_cursor](PaginatedList.md#next_cursor)
30 | - [has\_more](PaginatedList.md#has_more)
31 |
32 | ## Properties
33 |
34 | ### object
35 |
36 | • **object**: ``"list"``
37 |
38 | #### Defined in
39 |
40 | [lib/notion-api.ts:91](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L91)
41 |
42 | ___
43 |
44 | ### results
45 |
46 | • **results**: `T`[]
47 |
48 | #### Defined in
49 |
50 | [lib/notion-api.ts:92](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L92)
51 |
52 | ___
53 |
54 | ### next\_cursor
55 |
56 | • **next\_cursor**: ``null`` \| `string`
57 |
58 | #### Defined in
59 |
60 | [lib/notion-api.ts:93](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L93)
61 |
62 | ___
63 |
64 | ### has\_more
65 |
66 | • **has\_more**: `boolean`
67 |
68 | #### Defined in
69 |
70 | [lib/notion-api.ts:94](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L94)
71 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/ParsedAssetRequest.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / ParsedAssetRequest
2 |
3 | # Interface: ParsedAssetRequest
4 |
5 | The result of parsing an [AssetRequest](../modules.md#assetrequest) that was encoded as a URL or
6 | partially parsed as a NextJS query object.
7 |
8 | Encoded AssetRequests optionally contain a `last_edited_time`, which is used
9 | for freshness and cache busting.
10 |
11 | ## Table of contents
12 |
13 | ### Properties
14 |
15 | - [assetRequest](ParsedAssetRequest.md#assetrequest)
16 | - [last\_edited\_time](ParsedAssetRequest.md#last_edited_time)
17 |
18 | ## Properties
19 |
20 | ### assetRequest
21 |
22 | • **assetRequest**: [`AssetRequest`](../modules.md#assetrequest)
23 |
24 | #### Defined in
25 |
26 | [lib/assets.ts:156](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/assets.ts#L156)
27 |
28 | ___
29 |
30 | ### last\_edited\_time
31 |
32 | • **last\_edited\_time**: `undefined` \| `string`
33 |
34 | #### Defined in
35 |
36 | [lib/assets.ts:157](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/assets.ts#L157)
37 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/PropertyPointer.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / PropertyPointer
2 |
3 | # Interface: PropertyPointer
4 |
5 | A pointer to a property in a Notion API page. The property will by looked up
6 | by `name`, or `id` if given.
7 |
8 | The database property in Notion must have the matching `propertyType` to
9 | match the pointer. Otherwise, it will be the same as a non-existent property.
10 | See [getPropertyValue](../modules.md#getpropertyvalue).
11 |
12 | ## Type parameters
13 |
14 | | Name | Type |
15 | | :------ | :------ |
16 | | `Type` | extends [`PropertyType`](../modules.md#propertytype) = [`PropertyType`](../modules.md#propertytype) |
17 |
18 | ## Table of contents
19 |
20 | ### Properties
21 |
22 | - [type](PropertyPointer.md#type)
23 | - [name](PropertyPointer.md#name)
24 | - [id](PropertyPointer.md#id)
25 |
26 | ## Properties
27 |
28 | ### type
29 |
30 | • **type**: `Type`
31 |
32 | Type of the property. If the named property doesn't have this type, the PropertyPointer won't match it.
33 |
34 | #### Defined in
35 |
36 | [lib/notion-api.ts:896](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L896)
37 |
38 | ___
39 |
40 | ### name
41 |
42 | • **name**: `string`
43 |
44 | Name of the property
45 |
46 | #### Defined in
47 |
48 | [lib/notion-api.ts:898](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L898)
49 |
50 | ___
51 |
52 | ### id
53 |
54 | • `Optional` **id**: `string`
55 |
56 | ID of the property
57 |
58 | #### Defined in
59 |
60 | [lib/notion-api.ts:900](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/notion-api.ts#L900)
61 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/SortBuilder.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / SortBuilder
2 |
3 | # Interface: SortBuilder
4 |
5 | ## Type parameters
6 |
7 | | Name | Type |
8 | | :------ | :------ |
9 | | `T` | extends [`Sort`](../modules.md#sort) |
10 |
11 | ## Table of contents
12 |
13 | ### Properties
14 |
15 | - [ascending](SortBuilder.md#ascending)
16 | - [descending](SortBuilder.md#descending)
17 |
18 | ## Properties
19 |
20 | ### ascending
21 |
22 | • **ascending**: `T` & { `direction`: ``"ascending"`` }
23 |
24 | #### Defined in
25 |
26 | [lib/query.ts:510](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/query.ts#L510)
27 |
28 | ___
29 |
30 | ### descending
31 |
32 | • **descending**: `T` & { `direction`: ``"descending"`` }
33 |
34 | #### Defined in
35 |
36 | [lib/query.ts:511](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/query.ts#L511)
37 |
--------------------------------------------------------------------------------
/packages/notion-api/doc/interfaces/TimestampSortBuilder.md:
--------------------------------------------------------------------------------
1 | [@jitl/notion-api](../README.md) / [Exports](../modules.md) / TimestampSortBuilder
2 |
3 | # Interface: TimestampSortBuilder
4 |
5 | ## Table of contents
6 |
7 | ### Properties
8 |
9 | - [created\_time](TimestampSortBuilder.md#created_time)
10 | - [last\_edited\_time](TimestampSortBuilder.md#last_edited_time)
11 |
12 | ## Properties
13 |
14 | ### created\_time
15 |
16 | • **created\_time**: [`SortBuilder`](SortBuilder.md)<{}\>
17 |
18 | #### Defined in
19 |
20 | [lib/query.ts:518](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/query.ts#L518)
21 |
22 | ___
23 |
24 | ### last\_edited\_time
25 |
26 | • **last\_edited\_time**: [`SortBuilder`](SortBuilder.md)<{}\>
27 |
28 | #### Defined in
29 |
30 | [lib/query.ts:519](https://github.com/justjake/monorepo/blob/main/packages/notion-api/src/lib/query.ts#L519)
31 |
--------------------------------------------------------------------------------
/packages/notion-api/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'notion-api',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | transform: {
11 | '^.+\\.[tj]sx?$': 'ts-jest',
12 | },
13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
14 | coverageDirectory: '../../coverage/packages/notion-api',
15 | };
16 |
--------------------------------------------------------------------------------
/packages/notion-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/notion-api",
3 | "version": "0.2.2",
4 | "license": "Apache-2.0",
5 | "description": "The missing companion library for the official Notion public API.",
6 | "keywords": [
7 | "Notion",
8 | "API",
9 | "public API",
10 | "notion client",
11 | "cache",
12 | "@notionhq/client",
13 | "notion API",
14 | "CMS",
15 | "notion CMS",
16 | "typescript"
17 | ],
18 | "author": {
19 | "name": "Jake Teton-Landis",
20 | "email": "just.1.jake@gmail.com",
21 | "url": "https://jake.tl/"
22 | },
23 | "types": "./src/index.d.ts",
24 | "homepage": "https://github.com/justjake/monorepo/tree/main/packages/notion-api",
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/justjake/monorepo.git",
28 | "directory": "packages/notion-api"
29 | },
30 | "peerDependencies": {
31 | "@notionhq/client": "^1.0.4",
32 | "emoji-datasource-apple": "^7.0.2"
33 | },
34 | "peerDependenciesMeta": {
35 | "emoji-datasource-apple": {
36 | "optional": true
37 | }
38 | },
39 | "scripts": {
40 | "rebuild": "cd ../.. && ./rebuild-api.sh",
41 | "doc": "typedoc",
42 | "ship": "cd ../.. && ./rebuild-api.sh && cd dist/packages/notion-api && npm publish"
43 | },
44 | "dependencies": {
45 | "@types/debug": "^4.1.7",
46 | "@types/mime-types": "^2.1.1",
47 | "debug": "^4.3.3",
48 | "emoji-unicode": "^2.0.1",
49 | "fast-safe-stringify": "^2.1.1",
50 | "mime-types": "^2.1.34",
51 | "node-emoji": "^1.11.0"
52 | },
53 | "devDependencies": {
54 | "@notionhq/client": "^1.0.4",
55 | "emoji-datasource-apple": "^7.0.2",
56 | "esbuild-runner": "^2.2.1",
57 | "markserv": "^1.17.4",
58 | "typedoc": "^0.22.12",
59 | "typedoc-plugin-inline-sources": "^1.0.1",
60 | "typedoc-plugin-markdown": "^3.11.14",
61 | "typedoc-plugin-missing-exports": "^0.22.6",
62 | "typedoc-plugin-toc-group": "^0.0.5"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/notion-api/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/notion-api",
3 | "sourceRoot": "packages/notion-api/src",
4 | "projectType": "library",
5 | "targets": {
6 | "lint": {
7 | "executor": "@nrwl/linter:eslint",
8 | "outputs": ["{options.outputFile}"],
9 | "options": {
10 | "lintFilePatterns": ["packages/notion-api/**/*.ts"]
11 | }
12 | },
13 | "test": {
14 | "executor": "@nrwl/jest:jest",
15 | "outputs": ["coverage/packages/notion-api"],
16 | "options": {
17 | "jestConfig": "packages/notion-api/jest.config.js",
18 | "passWithNoTests": true
19 | }
20 | },
21 | "build": {
22 | "executor": "@nrwl/node:package",
23 | "outputs": ["{options.outputPath}"],
24 | "options": {
25 | "outputPath": "dist/packages/notion-api",
26 | "tsConfig": "packages/notion-api/tsconfig.lib.json",
27 | "packageJson": "packages/notion-api/package.json",
28 | "main": "packages/notion-api/src/index.ts",
29 | "assets": ["packages/notion-api/*.md"]
30 | }
31 | }
32 | },
33 | "tags": []
34 | }
35 |
--------------------------------------------------------------------------------
/packages/notion-api/src/example/blockData.ts:
--------------------------------------------------------------------------------
1 | import { Block, getBlockData, RichText } from '..';
2 |
3 | function getBlockTextContentBefore(block: Block): RichText | RichText[] {
4 | switch (block.type) {
5 | case 'paragraph':
6 | return block.paragraph.rich_text;
7 | case 'heading_1':
8 | return block.heading_1.rich_text;
9 | case 'heading_2':
10 | return block.heading_2.rich_text;
11 | // ... etc, for many more block types
12 | default:
13 | throw new Error(`unknown block type: ${block.type}`);
14 | }
15 | }
16 |
17 | function getBlockTextContentAfter(block: Block): RichText[] {
18 | const blockData = getBlockData(block);
19 | const results: RichText[] = [];
20 | if ('rich_text' in blockData) {
21 | results.push(blockData.rich_text);
22 | }
23 | if ('caption' in blockData) {
24 | results.push(blockData.caption);
25 | }
26 | // Done.
27 | return results;
28 | }
29 |
30 | function getBlockTextContentAfterExhaustive(block: Block): RichText | RichText[] {
31 | switch (block.type) {
32 | case 'paragraph': // Fall-through for blocks with only rich_text
33 | case 'heading_1':
34 | case 'heading_2': // ... etc
35 | return getBlockData(block).rich_text;
36 | case 'image':
37 | return getBlockData(block).caption;
38 | case 'code':
39 | return [getBlockData(block).rich_text, getBlockData(block).caption];
40 | // ... etc, for many more block types
41 | default:
42 | throw new Error(`unknown block type: ${block.type}`);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/notion-api/src/example/databaseSchema.ts:
--------------------------------------------------------------------------------
1 | import {
2 | databaseSchemaDiffToString,
3 | diffDatabaseSchemas,
4 | getPropertyValue,
5 | inferDatabaseSchema,
6 | isFullPage,
7 | iteratePaginatedAPI,
8 | NotionClient,
9 | richTextAsPlainText,
10 | } from '..';
11 | import { getAllProperties } from '../lib/notion-api';
12 | import { databaseFilterBuilder } from '../lib/query';
13 | import { runExample } from './exampleHelpers';
14 |
15 | console.log('database schema');
16 |
17 | runExample(module, 'Database schemas', async ({ notion, database_id, page_id }) => {
18 | const mySchema = inferDatabaseSchema({
19 | Title: { type: 'title' },
20 | SubTitle: { type: 'rich_text', name: 'Subtitle' },
21 | PublishedDate: { type: 'date', name: 'Published Date' },
22 | IsPublished: {
23 | type: 'checkbox',
24 | name: 'Show In Production',
25 | id: 'asdf123',
26 | },
27 | });
28 |
29 | // inferDatabaseSchema infers a concrete type with the same shape as the input,
30 | // so you can reference properties easily. It also adds `name` to each [[PropertySchema]]
31 | // based on the key name.
32 | console.log(mySchema.Title.name); // "Title"
33 |
34 | // You can use the properties in the inferred schema to access the corresponding
35 | // property value on a Page.
36 | for await (const page of iteratePaginatedAPI(notion.databases.query, {
37 | database_id,
38 | })) {
39 | if (isFullPage(page)) {
40 | const titleRichText = getPropertyValue(page, mySchema.Title);
41 | console.log('Title: ', richTextAsPlainText(titleRichText));
42 | const isPublished = getPropertyValue(page, mySchema.IsPublished);
43 | console.log('Is published: ', isPublished);
44 | }
45 | }
46 |
47 | // Print schema differences between our literal and the API.
48 | const database = await notion.databases.retrieve({ database_id });
49 | const diffs = diffDatabaseSchemas({ before: mySchema, after: database.properties });
50 | for (const change of diffs) {
51 | console.log(
52 | databaseSchemaDiffToString(change, { beforeName: 'mySchema', afterName: 'API database' })
53 | );
54 | }
55 |
56 | // Sketch
57 | const db = databaseFilterBuilder(mySchema);
58 | notion.databases.query({
59 | database_id,
60 | filter: db.or(db.IsPublished.equals(true), db.PublishedDate.after('2020-01-01')),
61 | });
62 | db.IsPublished.schema.id;
63 |
64 | const page = await notion.pages.retrieve({ page_id });
65 | if (isFullPage(page)) {
66 | const props = getAllProperties(page, db.schema);
67 | console.log(props.Title);
68 | }
69 | });
70 |
--------------------------------------------------------------------------------
/packages/notion-api/src/example/exampleHelpers.ts:
--------------------------------------------------------------------------------
1 | import { NotionClient } from '..';
2 |
3 | let done: Promise = Promise.resolve();
4 |
5 | export interface ExampleContext {
6 | name: string;
7 | notion: NotionClient;
8 | database_id: string;
9 | page_id: string;
10 | }
11 |
12 | export function runExample(
13 | module: NodeJS.Module,
14 | name: string,
15 | fn: (context: ExampleContext) => void | Promise
16 | ) {
17 | if (module !== require.main) {
18 | console.error('Skipping example "' + name + '" because it is not the main module.');
19 | return;
20 | }
21 |
22 | const notion = new NotionClient({
23 | auth: process.env.NOTION_SECRET,
24 | });
25 |
26 | const exampleContext: ExampleContext = {
27 | notion,
28 | name,
29 | database_id: process.env.NOTION_DATABASE_ID || 'No NOTION_DATABASE_ID',
30 | page_id: process.env.NOTION_PAGE_ID || 'No NOTION_PAGE_ID',
31 | };
32 |
33 | console.error('Running example: ' + name);
34 | done.then(() => {
35 | done = (async function nextExample() {
36 | const begin = Date.now();
37 | try {
38 | await fn(exampleContext);
39 | const end = Date.now();
40 | console.error(`Example completed: ${name} in`, end - begin, 'ms');
41 | } catch (error) {
42 | console.error('Example failed:', name, 'error: ', error);
43 | }
44 | })();
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/packages/notion-api/src/example/iteration.ts:
--------------------------------------------------------------------------------
1 | import { QueryDatabaseParameters } from '@notionhq/client/build/src/api-endpoints';
2 | import { asyncIterableToArray, isFullPage, iteratePaginatedAPI, NotionClient, Page } from '..';
3 |
4 | declare const notion: NotionClient;
5 | declare const databaseId: string;
6 |
7 | async function iterateDB() {
8 | for await (const page of iteratePaginatedAPI(notion.databases.query, {
9 | database_id: databaseId,
10 | })) {
11 | if (isFullPage(page)) {
12 | // TODO
13 | }
14 | }
15 | }
16 |
17 | declare const getNotionClient: () => NotionClient;
18 | declare const logger: typeof console;
19 |
20 | /**
21 | * Query all pages and return all records from a Notion database object
22 | * Will log a warning if database has no records
23 | * @param parameters To specify database id, control sorting, filtering and
24 | * pagination, directly using Notion's sdk types
25 | * @returns A list of all the results from the database or an empty array
26 | */
27 | const queryAll = async (parameters: QueryDatabaseParameters): Promise => {
28 | const params: typeof parameters = { ...parameters };
29 | const resultsWithPartialPages = await asyncIterableToArray(
30 | // getNotionClient() returns an authenticated instance of the notion SDK
31 | iteratePaginatedAPI(getNotionClient().databases.query, parameters)
32 | );
33 |
34 | // Filter out partial pages
35 | const fullPages = resultsWithPartialPages.filter(isFullPage);
36 |
37 | if (!fullPages.length) {
38 | logger.warn(`No results found in database ${params.database_id}`);
39 | }
40 | return fullPages;
41 | };
42 |
--------------------------------------------------------------------------------
/packages/notion-api/src/example/recipeCMS.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { DEBUG, NotionClient, NotionClientDebugLogger, richTextAsPlainText } from '..';
3 | import { CMS } from '../lib/content-management-system';
4 | import { runExample } from './exampleHelpers';
5 |
6 | const DEBUG_EXAMPLE = DEBUG.extend('example');
7 |
8 | runExample(module, 'Recipe CMS', async ({ notion }) => {
9 | const Recipes = new CMS({
10 | database_id: 'a3aa29a6b2f242d1b4cf86fb578a5eea',
11 | notion,
12 | slug: undefined, // Use page ID
13 | visible: true, // All pages visible
14 | getFrontmatter: () => ({}),
15 | schema: {},
16 | cache: {
17 | directory: path.join(__dirname, './cache'),
18 | },
19 | assets: {
20 | directory: path.join(__dirname, './assets'),
21 | downloadExternalAssets: true,
22 | },
23 | });
24 |
25 | if (!process.env.DEBUG) {
26 | process.env.DEBUG = '@jitl/notion-api:*';
27 | console.log('DEBUG', process.env.DEBUG);
28 | }
29 |
30 | // Download and cache all pages in the Recipes database, and their assets.
31 | for await (const recipe of Recipes.query()) {
32 | const s = richTextAsPlainText(recipe.frontmatter.title);
33 |
34 | DEBUG_EXAMPLE('page "%s" with children %d', s, recipe.content.children.length);
35 | await Recipes.downloadAssets(recipe);
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/packages/notion-api/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module
3 | * @packageDocumentation
4 | *
5 | * Module `@jitl/notion-api` provides extensions and helpers for the official
6 | * Notion public API.
7 | *
8 | * This library uses `@notionhq/client` as a peer dependency for both types and
9 | * to re-use the official client.
10 | *
11 | * The library is broadly separated into distinct feature sets:
12 | *
13 | * - A set of helpers for working with the Notion API. This includes
14 | * common types derived from the API's response types, and some iteration helpers
15 | * for fetching content. See the file [./lib/notion-api.ts](./lib/notion-api.ts)
16 | * for details.
17 | *
18 | * - A content management system supporting functions for downloading and
19 | * caching content from a Notion database. The high-level interface for these
20 | * features is the [[CMS]] class in the file
21 | * [./lib/content-management-system.ts](./lib/content-management-system.ts), but
22 | * related lower-level tools for working with Notion assets are also exported.
23 | */
24 | export * from './lib/notion-api';
25 | export * from './lib/content-management-system';
26 | export * from './lib/assets';
27 | export * from './lib/cache';
28 | export * from './lib/backlinks';
29 | export * from './lib/query';
30 |
--------------------------------------------------------------------------------
/packages/notion-api/src/lib/backlinks.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Tools for building up a set of backlinks in-memory, because the API doesn't
3 | * provide backlink information yet.
4 | * @category Backlink
5 | * @module
6 | */
7 | import {
8 | DEBUG,
9 | getBlockData,
10 | PageWithChildren,
11 | RichTextToken,
12 | visitChildBlocks,
13 | visitTextTokens,
14 | } from './notion-api';
15 |
16 | /**
17 | * Where a page was mentioned from
18 | * @category Backlink
19 | */
20 | export interface BacklinkFrom {
21 | mentionedFromPageId: string;
22 | mentionedFromBlockId: string;
23 | }
24 |
25 | /**
26 | * A link from one block to another page.
27 | * @category Backlink
28 | */
29 | export interface Backlink extends BacklinkFrom {
30 | mentionedPageId: string;
31 | }
32 |
33 | const NOTION_DOMAINS = ['.notion.so', '.notion.site', '.notion.com'];
34 |
35 | /**
36 | * @category API
37 | */
38 | export function isNotionDomain(domain: string): boolean {
39 | return NOTION_DOMAINS.some((suffix) => domain.endsWith(suffix));
40 | }
41 |
42 | const DEBUG_BACKLINKS = DEBUG.extend('backlinks');
43 |
44 | /**
45 | * Records links from a page to other pages.
46 | * See [[buildBacklinks]].
47 | * @category Backlink
48 | */
49 | export class Backlinks {
50 | linksToPage = new Map();
51 | private pageLinksToPageIds = new Map>();
52 |
53 | add(args: Backlink) {
54 | const { mentionedPageId, mentionedFromPageId } = args;
55 | const backlinks = this.linksToPage.get(mentionedPageId) || [];
56 | this.linksToPage.set(mentionedPageId, backlinks);
57 | backlinks.push(args);
58 |
59 | const forwardLinks =
60 | this.pageLinksToPageIds.get(mentionedFromPageId) || new Set();
61 | this.pageLinksToPageIds.set(mentionedFromPageId, forwardLinks);
62 | forwardLinks.add(mentionedPageId);
63 |
64 | DEBUG_BACKLINKS('added %s <-- %s', mentionedPageId, mentionedFromPageId);
65 | return args;
66 | }
67 |
68 | maybeAddUrl(url: string, from: BacklinkFrom): Backlink | undefined {
69 | try {
70 | const urlObject = new URL(url, 'https://www.notion.so');
71 | if (!isNotionDomain(urlObject.host)) {
72 | return undefined;
73 | }
74 | const path = urlObject.searchParams.get('p') || urlObject.pathname;
75 | const idWithoutDashes = path.substring(path.length - 32);
76 | if (idWithoutDashes.length !== 32) {
77 | return undefined;
78 | }
79 | const uuid = uuidWithDashes(idWithoutDashes);
80 | DEBUG_BACKLINKS('url %s --> %s', url, uuid);
81 | return this.add({
82 | ...from,
83 | mentionedPageId: uuid,
84 | });
85 | } catch (error) {
86 | console.warn('Invalid URL ', url, '', error);
87 | return undefined;
88 | }
89 | }
90 |
91 | maybeAddTextToken(token: RichTextToken, from: BacklinkFrom) {
92 | if (token.type === 'mention') {
93 | switch (token.mention.type) {
94 | case 'database':
95 | return this.add({
96 | mentionedPageId: token.mention.database.id,
97 | ...from,
98 | });
99 | case 'page':
100 | return this.add({
101 | mentionedPageId: token.mention.page.id,
102 | ...from,
103 | });
104 | }
105 | }
106 |
107 | if (token.href) {
108 | return this.maybeAddUrl(token.href, from);
109 | }
110 | }
111 |
112 | getLinksToPage(pageId: string): Backlink[] {
113 | return this.linksToPage.get(pageId) || [];
114 | }
115 |
116 | /**
117 | * When we re-fetch a page and its children, we need to invalidate the old
118 | * backlink data from those trees
119 | */
120 | deleteBacklinksFromPage(mentionedFromPageId: string) {
121 | const pagesToScan = this.pageLinksToPageIds.get(mentionedFromPageId);
122 | this.pageLinksToPageIds.delete(mentionedFromPageId);
123 | if (!pagesToScan) {
124 | return;
125 | }
126 | for (const mentionedPageId of pagesToScan) {
127 | const backlinks = this.linksToPage.get(mentionedPageId);
128 | if (!backlinks) {
129 | continue;
130 | }
131 | const newBacklinks = backlinks.filter(
132 | (backlink) => backlink.mentionedFromPageId !== mentionedFromPageId
133 | );
134 | if (newBacklinks.length === 0) {
135 | this.linksToPage.delete(mentionedPageId);
136 | DEBUG_BACKLINKS(
137 | 'removed all %s <-- %s',
138 | mentionedPageId,
139 | mentionedFromPageId
140 | );
141 | } else if (newBacklinks.length !== backlinks.length) {
142 | this.linksToPage.set(mentionedPageId, newBacklinks);
143 | DEBUG_BACKLINKS(
144 | 'removed all %s <-- %s',
145 | mentionedPageId,
146 | mentionedFromPageId
147 | );
148 | }
149 | }
150 | }
151 | }
152 |
153 | /**
154 | * Crawl the given `pages` and build all the backlinks between them into `backlinks`.
155 | * If no [[Backlinks]] is given, a new one will be created and returned.
156 | * @category Backlink
157 | */
158 | export function buildBacklinks(
159 | pages: PageWithChildren[],
160 | backlinks = new Backlinks()
161 | ): Backlinks {
162 | for (const page of pages) {
163 | const fromPage: BacklinkFrom = {
164 | mentionedFromPageId: page.id,
165 | mentionedFromBlockId: page.id,
166 | };
167 |
168 | visitTextTokens(page, (token) =>
169 | backlinks.maybeAddTextToken(token, fromPage)
170 | );
171 |
172 | visitChildBlocks(page.children, (block) => {
173 | const fromBlock = {
174 | ...fromPage,
175 | mentionedFromBlockId: block.id,
176 | };
177 | visitTextTokens(block, (token) =>
178 | backlinks.maybeAddTextToken(token, fromBlock)
179 | );
180 | switch (block.type) {
181 | case 'link_to_page': {
182 | backlinks.add({
183 | ...fromBlock,
184 | mentionedPageId:
185 | block.link_to_page.type === 'page_id'
186 | ? block.link_to_page.page_id
187 | : block.link_to_page.database_id,
188 | });
189 | break;
190 | }
191 | case 'bookmark':
192 | case 'link_preview':
193 | case 'embed': {
194 | const blockData = getBlockData(block);
195 | backlinks.maybeAddUrl(blockData.url, fromBlock);
196 | break;
197 | }
198 | }
199 | });
200 | }
201 | return backlinks;
202 | }
203 |
204 | /**
205 | * Ensure a UUID has dashes, since sometimes Notion IDs don't have dashes.
206 | * @category API
207 | */
208 | export function uuidWithDashes(id: string) {
209 | if (id.includes('-')) {
210 | return id;
211 | }
212 | return [
213 | id.slice(0, 8),
214 | id.slice(8, 12),
215 | id.slice(12, 16),
216 | id.slice(16, 20),
217 | id.slice(20, 32),
218 | ].join('-');
219 | }
220 |
--------------------------------------------------------------------------------
/packages/notion-api/src/lib/cache.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file implements a cache and cache implementation helpers.
3 | * @category Cache
4 | * @module
5 | */
6 | import { AssetRequest, getAssetRequestKey } from '..';
7 | import { Asset } from './assets';
8 | import { Block, BlockWithChildren, Page, PageWithChildren } from './notion-api';
9 |
10 | /**
11 | * @category Cache
12 | * @source
13 | */
14 | export type CacheBehavior =
15 | /** Read from the cache, but don't update it */
16 | | 'read-only'
17 | /** Read from the cache, or update it if needed. */
18 | | 'fill'
19 | /** Don't read from the cache, and update it with new values */
20 | | 'refresh';
21 |
22 | /**
23 | * Either returns a value by calling `fromCache`, or by calling `fromScratch`,
24 | * depending on `cacheBehavior`.
25 | * @category Cache
26 | * @param cacheBehavior `"fill"` by default.
27 | * @param fromCache Function to read the value from the cache.
28 | * @param fromScratch Function to compute the value from scratch.
29 | * @returns `[value, hit]` where `hit` is `true` if the value was fetched from the cache.
30 | */
31 | export function getFromCache(
32 | cacheBehavior: CacheBehavior | undefined,
33 | fromCache: () => T1 | undefined,
34 | fromScratch: () => Promise
35 | ): [T1, true] | Promise<[T2, false]> {
36 | const cached = cacheBehavior !== 'refresh' ? fromCache() : undefined;
37 | if (cached !== undefined) {
38 | return [cached, true];
39 | }
40 |
41 | return fromScratch().then((value) => [value, false]);
42 | }
43 |
44 | /**
45 | * Possibly call `fill` to fill the cache, depending on `cacheBehavior`.
46 | * @param cacheBehavior `"fill"` by default.
47 | * @param fill Function to fill the cache.
48 | * @category Cache
49 | */
50 | export function fillCache(
51 | cacheBehavior: CacheBehavior | undefined,
52 | fill: () => void
53 | ): void;
54 | /**
55 | * Possibly call `fill` to fill the cache, depending on `cacheBehavior` and `hit`.
56 | * If `hit` is true, or `cacheBehavior` is `"read-only"`, then `fill` is not called.
57 | * @param cacheBehavior `"fill"` by default.
58 | * @param fill Function to fill the cache.
59 | * @category Cache
60 | */
61 | export function fillCache(
62 | cacheBehavior: CacheBehavior | undefined,
63 | hit: boolean,
64 | fill: () => void
65 | ): void;
66 | export function fillCache(
67 | cacheBehavior: CacheBehavior | undefined,
68 | hitOrFill: boolean | (() => void),
69 | maybeFill?: () => void
70 | ) {
71 | const hit = typeof hitOrFill === 'boolean' ? hitOrFill : false;
72 | const fill: () => void =
73 | typeof maybeFill === 'function' ? maybeFill : (hitOrFill as () => void);
74 |
75 | if (cacheBehavior === 'read-only') {
76 | return;
77 | }
78 |
79 | if (hit && cacheBehavior !== 'refresh') {
80 | return;
81 | }
82 |
83 | fill();
84 | }
85 |
86 | /**
87 | * Stores values from the Notion API.
88 | * @category Cache
89 | */
90 | export class NotionObjectIndex {
91 | /** Whole pages */
92 | page: Map = new Map();
93 | pageWithChildren: Map = new Map();
94 |
95 | /** Whole blocks */
96 | block: Map = new Map();
97 | blockWithChildren: Map = new Map();
98 |
99 | /** Assets inside a block, page, etc. These are keyed by `getAssetRequestKey`. */
100 | asset: Map = new Map();
101 |
102 | /** Parent block ID, may also be a page ID. */
103 | parentId: Map = new Map();
104 |
105 | /** Parent page ID. */
106 | parentPageId: Map = new Map();
107 |
108 | addBlock(
109 | block: Block | BlockWithChildren,
110 | parent: Block | Page | string | undefined
111 | ) {
112 | const oldBlockWithChildren = this.blockWithChildren.get(block.id);
113 | this.block.set(block.id, block);
114 | if ('children' in block) {
115 | this.blockWithChildren.set(block.id, block);
116 | } else if (oldBlockWithChildren) {
117 | // Try to upgrade to a block with children by re-using old children
118 | const asBlockWithChildren = block as BlockWithChildren;
119 | asBlockWithChildren.children = oldBlockWithChildren.children;
120 | this.blockWithChildren.set(block.id, asBlockWithChildren);
121 | }
122 |
123 | const parentId =
124 | typeof parent === 'string'
125 | ? parent
126 | : typeof parent === 'object'
127 | ? parent.id
128 | : undefined;
129 | if (parentId) {
130 | this.parentId.set(block.id, parentId);
131 | }
132 |
133 | // If we don't know parent type, ignore.
134 | const parentPageId =
135 | typeof parent === 'object'
136 | ? parent.object === 'page'
137 | ? parent.id
138 | : parentId && this.parentPageId.get(parentId)
139 | : undefined;
140 | if (parentPageId) {
141 | this.parentPageId.set(block.id, parentPageId);
142 | }
143 | }
144 |
145 | addPage(page: Page | PageWithChildren): void {
146 | this.page.set(page.id, page);
147 | if ('children' in page) {
148 | this.pageWithChildren.set(page.id, page);
149 | }
150 | // Note: we don't try to upgrade `Page` since preserving old children can be more sketchy.
151 | switch (page.parent.type) {
152 | case 'page_id':
153 | this.parentId.set(page.id, page.parent.page_id);
154 | this.parentPageId.set(page.id, page.parent.page_id);
155 | break;
156 | case 'database_id':
157 | this.parentId.set(page.id, page.parent.database_id);
158 | this.parentPageId.set(page.id, page.parent.database_id);
159 | break;
160 | }
161 | }
162 |
163 | addAsset(request: AssetRequest, asset: Asset): void {
164 | const key = getAssetRequestKey(request);
165 | this.asset.set(key, asset);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/packages/notion-api/src/lib/notion-api.spec.ts:
--------------------------------------------------------------------------------
1 | // import { notionApi } from './notion-api';
2 |
3 | // describe('notionApi', () => {
4 | // it('should work', () => {
5 | // expect(notionApi()).toEqual('notion-api');
6 | // });
7 | // });
8 |
--------------------------------------------------------------------------------
/packages/notion-api/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'emoji-unicode' {
2 | declare const emojiFn: (emoji: string) => string;
3 | export = emojiFn;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/notion-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "esModuleInterop": true
5 | },
6 | "files": [],
7 | "include": [],
8 | "references": [
9 | {
10 | "path": "./tsconfig.lib.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/notion-api/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "../../dist/out-tsc",
6 | "declaration": true,
7 | "types": ["node"]
8 | },
9 | "exclude": ["**/*.spec.ts", "**/*.test.ts"],
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/notion-api/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "**/*.test.ts",
10 | "**/*.spec.ts",
11 | "**/*.test.tsx",
12 | "**/*.spec.tsx",
13 | "**/*.test.js",
14 | "**/*.spec.js",
15 | "**/*.test.jsx",
16 | "**/*.spec.jsx",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/notion-api/typedoc.js:
--------------------------------------------------------------------------------
1 | const name = require('./package.json').name;
2 | const NO_THANKS = ['**/node_modules/**', './src/example/**'];
3 |
4 | const API_RELATED_CATEGORIES = [
5 | 'API',
6 | 'Page',
7 | 'Block',
8 | 'Rich Text',
9 | 'Property',
10 | 'Date',
11 | 'Query',
12 | 'User',
13 | ];
14 |
15 | const CMS_RELATED_CATEGORIES = ['CMS', 'Asset', 'Backlink', 'Cache'];
16 |
17 | module.exports = {
18 | // disable package version in doc headers
19 | name,
20 | readme: './README.typedoc.md',
21 | entryPoints: ['./src/index.ts'],
22 | entryPointStrategy: 'resolve',
23 | // link to master instead of the current git SHA
24 | // which is borked with our strategy of deploying the docs
25 | // in the repo.
26 | gitRevision: 'main',
27 | out: './doc',
28 | // mode: 'file',
29 | exclude: NO_THANKS,
30 | externalPattern: NO_THANKS[0],
31 | // excludeNotExported: true,
32 | excludePrivate: true,
33 | listInvalidSymbolLinks: true,
34 | plugin: [
35 | 'typedoc-plugin-markdown',
36 | 'typedoc-plugin-inline-sources',
37 | // 'typedoc-plugin-missing-exports',
38 | // 'typedoc-plugin-toc-group',
39 | ],
40 | categorizeByGroup: true,
41 | categoryOrder: [...API_RELATED_CATEGORIES, ...CMS_RELATED_CATEGORIES],
42 | sort: ['source-order'],
43 | };
44 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/README.md:
--------------------------------------------------------------------------------
1 | # pinch-zoom
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test pinch-zoom` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'pinch-zoom',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | transform: {
10 | '^.+\\.[tj]s$': 'ts-jest',
11 | },
12 | moduleFileExtensions: ['ts', 'js', 'html'],
13 | coverageDirectory: '../../coverage/packages/pinch-zoom',
14 | };
15 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/pinch-zoom",
3 | "sourceRoot": "packages/pinch-zoom/src",
4 | "projectType": "library",
5 | "targets": {
6 | "lint": {
7 | "executor": "@nrwl/linter:eslint",
8 | "outputs": ["{options.outputFile}"],
9 | "options": {
10 | "lintFilePatterns": ["packages/pinch-zoom/**/*.ts"]
11 | }
12 | },
13 | "test": {
14 | "executor": "@nrwl/jest:jest",
15 | "outputs": ["coverage/packages/pinch-zoom"],
16 | "options": {
17 | "jestConfig": "packages/pinch-zoom/jest.config.js",
18 | "passWithNoTests": true
19 | }
20 | }
21 | },
22 | "tags": []
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/pinch-zoom';
2 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/src/lib/pinch-zoom.spec.ts:
--------------------------------------------------------------------------------
1 | import { pinchZoom } from './pinch-zoom';
2 |
3 | describe('pinchZoom', () => {
4 | it('should work', () => {
5 | expect(pinchZoom()).toEqual('pinch-zoom');
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/src/lib/pinch-zoom.ts:
--------------------------------------------------------------------------------
1 | export function pinchZoom(): string {
2 | return 'pinch-zoom';
3 | }
4 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "noImplicitOverride": true,
8 | "noPropertyAccessFromIndexSignature": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true
11 | },
12 | "files": [],
13 | "include": [],
14 | "references": [
15 | {
16 | "path": "./tsconfig.lib.json"
17 | },
18 | {
19 | "path": "./tsconfig.spec.json"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "declaration": true,
6 | "types": []
7 | },
8 | "include": ["**/*.ts"],
9 | "exclude": ["**/*.spec.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/pinch-zoom/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/playground-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["src/plugins/index.js"],
11 | "rules": {
12 | "@typescript-eslint/no-var-requires": "off",
13 | "no-undef": "off"
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/playground-e2e/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": ".",
3 | "fixturesFolder": "./src/fixtures",
4 | "integrationFolder": "./src/integration",
5 | "modifyObstructiveCode": false,
6 | "supportFile": "./src/support/index.ts",
7 | "pluginsFile": false,
8 | "video": true,
9 | "videosFolder": "../../dist/cypress/packages/playground-e2e/videos",
10 | "screenshotsFolder": "../../dist/cypress/packages/playground-e2e/screenshots",
11 | "chromeWebSecurity": false
12 | }
13 |
--------------------------------------------------------------------------------
/packages/playground-e2e/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/playground-e2e",
3 | "sourceRoot": "packages/playground-e2e/src",
4 | "projectType": "application",
5 | "targets": {
6 | "e2e": {
7 | "executor": "@nrwl/cypress:cypress",
8 | "options": {
9 | "cypressConfig": "packages/playground-e2e/cypress.json",
10 | "devServerTarget": "playground:serve"
11 | },
12 | "configurations": {
13 | "production": {
14 | "devServerTarget": "playground:serve:production"
15 | }
16 | }
17 | },
18 | "lint": {
19 | "executor": "@nrwl/linter:eslint",
20 | "outputs": ["{options.outputFile}"],
21 | "options": {
22 | "lintFilePatterns": ["packages/playground-e2e/**/*.{js,ts}"]
23 | }
24 | }
25 | },
26 | "tags": [],
27 | "implicitDependencies": ["playground"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/playground-e2e/src/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/playground-e2e/src/integration/app.spec.ts:
--------------------------------------------------------------------------------
1 | import { getGreeting } from '../support/app.po';
2 |
3 | describe('playground', () => {
4 | beforeEach(() => cy.visit('/'));
5 |
6 | it('should display welcome message', () => {
7 | // Custom command example, see `../support/commands.ts` file
8 | cy.login('my-email@something.com', 'myPassword');
9 |
10 | // Function helper example, see `../support/app.po.ts` file
11 | getGreeting().contains('Welcome playground');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/packages/playground-e2e/src/support/app.po.ts:
--------------------------------------------------------------------------------
1 | export const getGreeting = () => cy.get('h1');
2 |
--------------------------------------------------------------------------------
/packages/playground-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/packages/playground-e2e/src/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
--------------------------------------------------------------------------------
/packages/playground-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "sourceMap": false,
5 | "outDir": "../../dist/out-tsc",
6 | "allowJs": true,
7 | "types": ["cypress", "node"]
8 | },
9 | "include": ["src/**/*.ts", "src/**/*.js"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/playground/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nrwl/react/babel",
5 | // {
6 | // "runtime": "automatic",
7 | // "importSource": "@emotion/react"
8 | // }
9 | ]
10 | ]
11 | // "plugins": ["@emotion/babel-plugin"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/playground/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by:
2 | # 1. autoprefixer to adjust CSS to support the below specified browsers
3 | # 2. babel preset-env to adjust included polyfills
4 | #
5 | # For additional information regarding the format and rule options, please see:
6 | # https://github.com/browserslist/browserslist#queries
7 | #
8 | # If you need to support different browsers in production, you may tweak the list below.
9 |
10 | last 1 Chrome version
11 | last 1 Firefox version
12 | last 2 Edge major versions
13 | last 2 Safari major version
14 | last 2 iOS major versions
15 | Firefox ESR
16 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/packages/playground/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/playground/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'playground',
3 | preset: '../../jest.preset.js',
4 | transform: {
5 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
6 | '^.+\\.[tj]sx?$': 'babel-jest',
7 | },
8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
9 | coverageDirectory: '../../coverage/packages/playground',
10 | };
11 |
--------------------------------------------------------------------------------
/packages/playground/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/playground",
3 | "sourceRoot": "packages/playground/src",
4 | "projectType": "application",
5 | "targets": {
6 | "build": {
7 | "executor": "@nrwl/web:webpack",
8 | "outputs": ["{options.outputPath}"],
9 | "defaultConfiguration": "production",
10 | "options": {
11 | "compiler": "babel",
12 | "outputPath": "dist/packages/playground",
13 | "index": "packages/playground/src/index.html",
14 | "baseHref": "/",
15 | "main": "packages/playground/src/main.tsx",
16 | "polyfills": "packages/playground/src/polyfills.ts",
17 | "tsConfig": "packages/playground/tsconfig.app.json",
18 | "assets": [
19 | "packages/playground/src/favicon.ico",
20 | "packages/playground/src/assets"
21 | ],
22 | "styles": [],
23 | "scripts": [],
24 | "webpackConfig": "@nrwl/react/plugins/webpack"
25 | },
26 | "configurations": {
27 | "production": {
28 | "fileReplacements": [
29 | {
30 | "replace": "packages/playground/src/environments/environment.ts",
31 | "with": "packages/playground/src/environments/environment.prod.ts"
32 | }
33 | ],
34 | "optimization": true,
35 | "outputHashing": "all",
36 | "sourceMap": false,
37 | "namedChunks": false,
38 | "extractLicenses": true,
39 | "vendorChunk": false
40 | }
41 | }
42 | },
43 | "serve": {
44 | "executor": "@nrwl/web:dev-server",
45 | "options": {
46 | "buildTarget": "playground:build",
47 | "hmr": true
48 | },
49 | "configurations": {
50 | "production": {
51 | "buildTarget": "playground:build:production",
52 | "hmr": false
53 | }
54 | }
55 | },
56 | "lint": {
57 | "executor": "@nrwl/linter:eslint",
58 | "outputs": ["{options.outputFile}"],
59 | "options": {
60 | "lintFilePatterns": ["packages/playground/**/*.{ts,tsx,js,jsx}"]
61 | }
62 | },
63 | "test": {
64 | "executor": "@nrwl/jest:jest",
65 | "outputs": ["coverage/packages/playground"],
66 | "options": {
67 | "jestConfig": "packages/playground/jest.config.js",
68 | "passWithNoTests": true
69 | }
70 | }
71 | },
72 | "tags": []
73 | }
74 |
--------------------------------------------------------------------------------
/packages/playground/src/app/app.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 |
3 | import App from './app';
4 |
5 | describe('App', () => {
6 | it('should render successfully', () => {
7 | const { baseElement } = render( );
8 |
9 | expect(baseElement).toBeTruthy();
10 | });
11 |
12 | it('should have a greeting as the title', () => {
13 | const { getByText } = render( );
14 |
15 | expect(getByText(/Welcome playground/gi)).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/packages/playground/src/app/app.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { useAtomsDevtools } from 'jotai/devtools';
3 | import { atom, useAtom } from 'jotai';
4 | import { useEffect } from 'react';
5 | import { computedAtom, implicitAtom } from '@jitl/state';
6 |
7 | const StyledApp = styled.div`
8 | // Your style here
9 | `;
10 |
11 | const helloAtom = atom('hello');
12 | const worldAtom = atom('world');
13 | const capitalize = atom(null, (get, set) => {
14 | set(worldAtom, get(worldAtom).toUpperCase());
15 | set(helloAtom, get(helloAtom).toUpperCase());
16 | });
17 | const computed = atom((get) => `${get(helloAtom)} to the ${get(worldAtom)}`);
18 |
19 | export function App() {
20 | useAtomsDevtools('World');
21 | const [hello, setHello] = useAtom(helloAtom);
22 | const [, setCapitalize] = useAtom(capitalize);
23 | const [world] = useAtom(worldAtom);
24 | const [computedState] = useAtom(computed);
25 |
26 | useEffect(() => {
27 | setTimeout(() => {
28 | setHello('goodbye');
29 | }, 50);
30 |
31 | setTimeout(setCapitalize, 100);
32 | }, [setHello, setCapitalize]);
33 |
34 | return (
35 |
36 | {hello} {world}; {computedState}
37 |
38 | );
39 | }
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/packages/playground/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjake/monorepo/d1e87174827005fa7fd6d158a0a1d7e86dd2a396/packages/playground/src/assets/.gitkeep
--------------------------------------------------------------------------------
/packages/playground/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/packages/playground/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // When building for production, this file is replaced with `environment.prod.ts`.
3 |
4 | export const environment = {
5 | production: false,
6 | };
7 |
--------------------------------------------------------------------------------
/packages/playground/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjake/monorepo/d1e87174827005fa7fd6d158a0a1d7e86dd2a396/packages/playground/src/favicon.ico
--------------------------------------------------------------------------------
/packages/playground/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/playground/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { enterTopLevelCapability, World, WorldProvider } from '@jitl/state';
2 | import { StrictMode } from 'react';
3 | import * as ReactDOM from 'react-dom';
4 |
5 | // We need a dynamic process variable for Jotai devtools:
6 | // `process.env` will be compiled to a raw object literal
7 | const DYNAMIC_PROCESS_ENV = process.env;
8 | const DYNAMIC_PROCESS = { env: DYNAMIC_PROCESS_ENV };
9 | Object.freeze(DYNAMIC_PROCESS);
10 | Object.defineProperty(globalThis, 'process', {
11 | value: DYNAMIC_PROCESS,
12 | writable: false,
13 | configurable: false,
14 | enumerable: true,
15 | });
16 |
17 | const world = World.reactWorld();
18 | enterTopLevelCapability(world.capabilities);
19 |
20 | console.log('process', process.env);
21 | console.log('process.env', process.env);
22 |
23 | async function main() {
24 | const { App } = await import('./app/app');
25 |
26 | ReactDOM.render(
27 |
28 |
29 |
30 |
31 | ,
32 | document.getElementById('root')
33 | );
34 | }
35 |
36 | main();
37 |
--------------------------------------------------------------------------------
/packages/playground/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
3 | *
4 | * See: https://github.com/zloirock/core-js#babel
5 | */
6 | import 'core-js/stable';
7 | import 'regenerator-runtime/runtime';
8 |
--------------------------------------------------------------------------------
/packages/playground/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "files": [
8 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
9 | "../../node_modules/@nrwl/react/typings/image.d.ts"
10 | ],
11 | "exclude": [
12 | "**/*.spec.ts",
13 | "**/*.test.ts",
14 | "**/*.spec.tsx",
15 | "**/*.test.tsx",
16 | "**/*.spec.js",
17 | "**/*.test.js",
18 | "**/*.spec.jsx",
19 | "**/*.test.jsx"
20 | ],
21 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "@emotion/react",
6 | "allowJs": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "strict": true,
11 | "noImplicitOverride": true,
12 | "noPropertyAccessFromIndexSignature": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true
15 | },
16 | "files": [],
17 | "include": [],
18 | "references": [
19 | {
20 | "path": "./tsconfig.app.json"
21 | },
22 | {
23 | "path": "./tsconfig.spec.json"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/playground/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "**/*.test.ts",
10 | "**/*.spec.ts",
11 | "**/*.test.tsx",
12 | "**/*.spec.tsx",
13 | "**/*.test.js",
14 | "**/*.spec.js",
15 | "**/*.test.jsx",
16 | "**/*.spec.jsx",
17 | "**/*.d.ts"
18 | ],
19 | "files": [
20 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
21 | "../../node_modules/@nrwl/react/typings/image.d.ts"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/state/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nrwl/react/babel",
5 | {
6 | "runtime": "automatic",
7 | "useBuiltIns": "usage",
8 | "importSource": "@emotion/react"
9 | }
10 | ]
11 | ],
12 | "plugins": ["@emotion/babel-plugin"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/state/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/state/README.md:
--------------------------------------------------------------------------------
1 | TODOs:
2 |
3 | - Move back to TSC based build process, but with JSX support. This whole "rollup/react" preset was a mistake
4 |
5 | # state
6 |
7 | This library was generated with [Nx](https://nx.dev).
8 |
9 | ## Building
10 |
11 | Run `nx build state` to build the library.
12 |
13 | ## Running unit tests
14 |
15 | Run `nx test state` to execute the unit tests via [Jest](https://jestjs.io).
16 |
--------------------------------------------------------------------------------
/packages/state/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'state',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | transform: {
10 | '^.+\\.[tj]sx?$': 'ts-jest',
11 | },
12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
13 | coverageDirectory: '../../coverage/packages/state',
14 | };
15 |
--------------------------------------------------------------------------------
/packages/state/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/state",
3 | "version": "0.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@jitl/state",
9 | "version": "0.0.1",
10 | "dependencies": {
11 | "jotai": "npm:@jitl/jotai@^1.4.9",
12 | "proxy-compare": "^2.0.2"
13 | },
14 | "devDependencies": {
15 | "@types/node": "^17.0.4",
16 | "@types/react": "^17.0.38"
17 | }
18 | },
19 | "node_modules/@types/node": {
20 | "version": "17.0.4",
21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.4.tgz",
22 | "integrity": "sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog==",
23 | "dev": true
24 | },
25 | "node_modules/@types/prop-types": {
26 | "version": "15.7.4",
27 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
28 | "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
29 | "dev": true
30 | },
31 | "node_modules/@types/react": {
32 | "version": "17.0.38",
33 | "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
34 | "integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
35 | "dev": true,
36 | "dependencies": {
37 | "@types/prop-types": "*",
38 | "@types/scheduler": "*",
39 | "csstype": "^3.0.2"
40 | }
41 | },
42 | "node_modules/@types/scheduler": {
43 | "version": "0.16.2",
44 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
45 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
46 | "dev": true
47 | },
48 | "node_modules/csstype": {
49 | "version": "3.0.10",
50 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
51 | "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
52 | "dev": true
53 | },
54 | "node_modules/jotai": {
55 | "version": "1.4.9",
56 | "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.4.9.tgz",
57 | "integrity": "sha512-NHh3UIIelfUBUjcJi+oXHoL9rISkj8LZu3eeXR5KL1VBYLOuPVK0TTTDfbgdwm5B83zpxNAhTlhb/aKlJBtP0g==",
58 | "engines": {
59 | "node": ">=12.7.0"
60 | },
61 | "peerDependencies": {
62 | "@babel/core": "*",
63 | "@babel/template": "*",
64 | "@urql/core": "*",
65 | "immer": "*",
66 | "optics-ts": "*",
67 | "react": ">=16.8",
68 | "react-query": "*",
69 | "valtio": "*",
70 | "wonka": "*",
71 | "xstate": "*"
72 | },
73 | "peerDependenciesMeta": {
74 | "@babel/core": {
75 | "optional": true
76 | },
77 | "@babel/template": {
78 | "optional": true
79 | },
80 | "@urql/core": {
81 | "optional": true
82 | },
83 | "immer": {
84 | "optional": true
85 | },
86 | "optics-ts": {
87 | "optional": true
88 | },
89 | "react-query": {
90 | "optional": true
91 | },
92 | "valtio": {
93 | "optional": true
94 | },
95 | "wonka": {
96 | "optional": true
97 | },
98 | "xstate": {
99 | "optional": true
100 | }
101 | }
102 | },
103 | "node_modules/proxy-compare": {
104 | "version": "2.0.2",
105 | "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
106 | "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
107 | }
108 | },
109 | "dependencies": {
110 | "@types/node": {
111 | "version": "17.0.4",
112 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.4.tgz",
113 | "integrity": "sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog==",
114 | "dev": true
115 | },
116 | "@types/prop-types": {
117 | "version": "15.7.4",
118 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
119 | "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
120 | "dev": true
121 | },
122 | "@types/react": {
123 | "version": "17.0.38",
124 | "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
125 | "integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
126 | "dev": true,
127 | "requires": {
128 | "@types/prop-types": "*",
129 | "@types/scheduler": "*",
130 | "csstype": "^3.0.2"
131 | }
132 | },
133 | "@types/scheduler": {
134 | "version": "0.16.2",
135 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
136 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
137 | "dev": true
138 | },
139 | "csstype": {
140 | "version": "3.0.10",
141 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
142 | "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
143 | "dev": true
144 | },
145 | "jotai": {
146 | "version": "1.4.9",
147 | "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.4.9.tgz",
148 | "integrity": "sha512-NHh3UIIelfUBUjcJi+oXHoL9rISkj8LZu3eeXR5KL1VBYLOuPVK0TTTDfbgdwm5B83zpxNAhTlhb/aKlJBtP0g==",
149 | "requires": {}
150 | },
151 | "proxy-compare": {
152 | "version": "2.0.2",
153 | "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
154 | "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/packages/state/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/state",
3 | "version": "0.0.1",
4 | "type": "commonjs",
5 | "dependencies": {
6 | "proxy-compare": "^2.0.2"
7 | },
8 | "devDependencies": {
9 | "@types/node": "^17.0.4"
10 | },
11 | "peerDependencies": {
12 | "@types/react": "^17.0.38",
13 | "@types/react-dom": "^17.0.11",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/state/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/state",
3 | "sourceRoot": "packages/state/src",
4 | "projectType": "library",
5 | "targets": {
6 | "lint": {
7 | "executor": "@nrwl/linter:eslint",
8 | "outputs": ["{options.outputFile}"],
9 | "options": {
10 | "lintFilePatterns": ["packages/state/**/*.{ts,tsx,js,jsx}"]
11 | }
12 | },
13 | "test": {
14 | "executor": "@nrwl/jest:jest",
15 | "outputs": ["coverage/packages/state"],
16 | "options": {
17 | "jestConfig": "packages/state/jest.config.js",
18 | "passWithNoTests": true
19 | }
20 | },
21 | "build": {
22 | "executor": "@nrwl/node:package",
23 | "outputs": ["{options.outputPath}"],
24 | "options": {
25 | "outputPath": "dist/packages/state",
26 | "tsConfig": "packages/state/tsconfig.lib.json",
27 | "packageJson": "packages/state/package.json",
28 | "main": "packages/state/src/index.ts",
29 | "assets": ["packages/state/*.md"]
30 | }
31 | }
32 | },
33 | "tags": []
34 | }
35 |
--------------------------------------------------------------------------------
/packages/state/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/implicitAtom';
2 | export * from './lib/implicitCapabilities';
3 | export * from './lib/proxyCompareAtom';
4 | export * from './lib/Scheduler';
5 | export * from './lib/shallowEqualAtom';
6 | export * from './lib/withJotaiState';
7 | export * from './lib/World';
8 | export * from './lib/WorldProvider';
9 |
--------------------------------------------------------------------------------
/packages/state/src/lib/Scheduler.ts:
--------------------------------------------------------------------------------
1 | import { mapGetOrCreate } from '@jitl/util';
2 | import { Atom } from 'jotai';
3 | import { ReadCapability, SubscribeCapability } from '..';
4 |
5 | type AnyAtom = Atom;
6 | type Effect = () => void;
7 |
8 | // Eg, setTimeout(() => {}, 0);
9 | export type ScheduleFn = (effect: Effect) => void;
10 |
11 | interface SchedulerConfig {
12 | scheduleCompute: ScheduleFn;
13 | scheduleEffect: ScheduleFn;
14 | capabilities: ReadCapability & SubscribeCapability;
15 | }
16 |
17 | class AtomChangeSubscription {
18 | prev: unknown;
19 | unsubscribe: Effect;
20 | listeners = new Set();
21 | constructor(public atom: AnyAtom, public config: SchedulerConfig) {
22 | this.prev = this.config.capabilities.get(atom);
23 | this.unsubscribe = this.config.capabilities.subscribe(
24 | atom,
25 | this.handleAtomMaybeChange
26 | );
27 | }
28 | recompute = () => {
29 | const cur = this.config.capabilities.get(this.atom);
30 | if (!Object.is(cur, this.prev)) {
31 | this.prev = cur;
32 | this.listeners.forEach(this.config.scheduleEffect);
33 | }
34 | };
35 | handleAtomMaybeChange = () => {
36 | this.config.scheduleCompute(this.recompute);
37 | };
38 | }
39 |
40 | export class ChangeSubscriber {
41 | private subscriptions = new Map();
42 | constructor(private config: SchedulerConfig) {}
43 |
44 | subscribeChangeEffect(atom: AnyAtom, callback: Effect): () => void {
45 | const changeTracker = mapGetOrCreate(this.subscriptions, atom, () => {
46 | const t = new AtomChangeSubscription(atom, this.config);
47 | this.config.capabilities.subscribe(atom, t.handleAtomMaybeChange);
48 | return t;
49 | }).value;
50 |
51 | changeTracker.listeners.add(callback);
52 |
53 | return () => {
54 | changeTracker.listeners.delete(callback);
55 | if (changeTracker.listeners.size === 0) {
56 | changeTracker.unsubscribe();
57 | this.subscriptions.delete(atom);
58 | }
59 | };
60 | }
61 |
62 | getSubscribedAtoms(): IterableIterator {
63 | return this.subscriptions.keys();
64 | }
65 | }
66 |
67 | export class Scheduler {
68 | private readonly computeQueue = new Set();
69 | private readonly effectQueue = new Set();
70 | private queued = false;
71 |
72 | constructor(
73 | private config: {
74 | scheduleFlush: ScheduleFn;
75 | batchEffects: ScheduleFn;
76 | onError: (error: unknown) => void;
77 | }
78 | ) {
79 | this.config = config;
80 | }
81 |
82 | scheduleCompute: SchedulerConfig['scheduleCompute'] = (effect) => {
83 | this.computeQueue.add(effect);
84 | this.enqueue();
85 | };
86 |
87 | scheduleEffect: SchedulerConfig['scheduleEffect'] = (effect) => {
88 | this.effectQueue.add(effect);
89 | this.enqueue();
90 | };
91 |
92 | flush = () => {
93 | do {
94 | const toCompute = [...this.computeQueue];
95 | this.computeQueue.clear();
96 | for (const effect of toCompute) {
97 | try {
98 | effect();
99 | } catch (error) {
100 | this.config.onError(error);
101 | }
102 | }
103 |
104 | const effects = [...this.effectQueue];
105 | this.effectQueue.clear();
106 | this.config.batchEffects(() => {
107 | for (const effect of effects) {
108 | try {
109 | effect();
110 | } catch (error) {
111 | this.config.onError(error);
112 | }
113 | }
114 | });
115 | } while (this.computeQueue.size > 0 || this.effectQueue.size > 0);
116 | this.queued = false;
117 | };
118 |
119 | enqueue() {
120 | if (!this.queued) {
121 | this.queued = true;
122 | this.config.scheduleFlush(this.flush);
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/packages/state/src/lib/World.ts:
--------------------------------------------------------------------------------
1 | import { createStore, Store } from 'jotai';
2 | import { jotaiStoreCapabilities } from './implicitCapabilities';
3 | import { ChangeSubscriber, ScheduleFn, Scheduler } from './Scheduler';
4 | import * as ReactDOM from 'react-dom';
5 |
6 | export class World {
7 | readonly store: Store;
8 | readonly capabilities: ReturnType;
9 | readonly scheduler: Scheduler;
10 | readonly changeSubscriber: ChangeSubscriber;
11 |
12 | constructor(
13 | private config: {
14 | onError: (error: unknown) => void;
15 | scheduleFlush: ScheduleFn;
16 | batchEffects: ScheduleFn;
17 | }
18 | ) {
19 | this.store = createStore();
20 | this.capabilities = jotaiStoreCapabilities(this.store);
21 | this.scheduler = new Scheduler(this.config);
22 | this.changeSubscriber = new ChangeSubscriber({
23 | capabilities: this.capabilities,
24 | scheduleCompute: this.scheduler.scheduleCompute,
25 | scheduleEffect: this.scheduler.scheduleEffect,
26 | });
27 | }
28 |
29 | static syncWorld() {
30 | return new this({
31 | batchEffects: (effects) => effects(),
32 | scheduleFlush: (flush) => flush(),
33 | onError(error) {
34 | throw error;
35 | },
36 | });
37 | }
38 |
39 | static reactWorld() {
40 | const scheduleFlush: ScheduleFn =
41 | typeof window === 'object'
42 | ? window.requestAnimationFrame
43 | : (flush) => flush();
44 | const onError =
45 | typeof window === 'object'
46 | ? (error: unknown) => {
47 | // Browser: throw async. This should reach the developer console, but
48 | // not block the compute queue.
49 | new Promise((resolve, reject) => reject(error));
50 | }
51 | : (error: unknown) => {
52 | throw error;
53 | };
54 | return new this({
55 | batchEffects: ReactDOM.unstable_batchedUpdates,
56 | scheduleFlush,
57 | onError,
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/state/src/lib/WorldProvider.tsx:
--------------------------------------------------------------------------------
1 | import { Provider } from 'jotai';
2 | import { createContext, useCallback, useContext } from 'react';
3 | import { World } from './World';
4 |
5 | const WorldContext = createContext(undefined);
6 |
7 | /** Use the current world */
8 | export function useWorld() {
9 | const world = useContext(WorldContext);
10 | if (!world) {
11 | throw new Error('useWorld must be used within a WorldProvider');
12 | }
13 | return world;
14 | }
15 |
16 | /** Provide a World via React context. Provides the world's Jotai store to Jotai consumers. */
17 | export function WorldProvider({
18 | children,
19 | value: world,
20 | }: {
21 | children: React.ReactNode;
22 | value: World;
23 | }) {
24 | const getStore = useCallback(() => world.store, [world.store]);
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/state/src/lib/atomWithCompare.test.ts:
--------------------------------------------------------------------------------
1 | import { atomWithCompare } from './atomWithCompare';
2 | import { jotaiStoreCapabilities } from '..';
3 | import { atom, createStore } from 'jotai';
4 |
5 | const consoleWarn = console.warn;
6 | beforeEach(() => {
7 | console.warn = jest.fn();
8 | });
9 | afterEach(() => {
10 | console.warn = consoleWarn;
11 | });
12 |
13 | type Styles = {
14 | color: string;
15 | fontSize: number;
16 | border: string;
17 | };
18 |
19 | function stylesAreEqual(a: Styles, b: Styles): boolean {
20 | return (
21 | a.color === b.color && a.fontSize === b.fontSize && a.border === b.border
22 | );
23 | }
24 |
25 | it('behaves like a normal atom with Object.is comparison', async () => {
26 | const stylesAtom = atomWithCompare(
27 | { color: 'red', fontSize: 12, border: 'none' },
28 | Object.is
29 | );
30 |
31 | const called = jest.fn();
32 |
33 | const derivedAtom = atom((get) => {
34 | called();
35 | return get(stylesAtom);
36 | });
37 |
38 | const world = jotaiStoreCapabilities(createStore());
39 | const unsubscribe = world.subscribe(derivedAtom, () =>
40 | world.get(derivedAtom)
41 | );
42 |
43 | world.get(derivedAtom);
44 | expect(called).toBeCalledTimes(1);
45 |
46 | world.set(stylesAtom, { ...world.get(stylesAtom) });
47 | expect(called).toBeCalledTimes(2);
48 |
49 | expect(console.warn).toHaveBeenCalledTimes(0);
50 |
51 | unsubscribe();
52 | });
53 |
54 | it('no unnecessary updates when updating atoms', async () => {
55 | const stylesAtom = atomWithCompare(
56 | { color: 'red', fontSize: 12, border: 'none' },
57 | stylesAreEqual
58 | );
59 |
60 | const called = jest.fn();
61 |
62 | const derivedAtom = atom((get) => {
63 | called();
64 | return get(stylesAtom);
65 | });
66 |
67 | const world = jotaiStoreCapabilities(createStore());
68 | const unsubscribe = world.subscribe(derivedAtom, () =>
69 | world.get(derivedAtom)
70 | );
71 |
72 | world.get(derivedAtom);
73 | expect(called).toBeCalledTimes(1);
74 |
75 | world.set(stylesAtom, { ...world.get(stylesAtom) });
76 | expect(called).toBeCalledTimes(1);
77 |
78 | world.set(stylesAtom, { ...world.get(stylesAtom), fontSize: 15 });
79 | expect(called).toBeCalledTimes(2);
80 |
81 | expect(console.warn).toHaveBeenCalledTimes(0);
82 |
83 | unsubscribe();
84 | });
85 |
86 | it('Warns if Object.is disagrees with equality', async () => {
87 | const stylesAtom = atomWithCompare(
88 | { color: 'red', fontSize: 12, border: 'none' },
89 | () => false
90 | );
91 |
92 | const called = jest.fn();
93 |
94 | const derivedAtom = atom((get) => {
95 | called();
96 | return get(stylesAtom);
97 | });
98 |
99 | const world = jotaiStoreCapabilities(createStore());
100 | const unsubscribe = world.subscribe(derivedAtom, () =>
101 | world.get(derivedAtom)
102 | );
103 |
104 | world.get(derivedAtom);
105 | expect(called).toBeCalledTimes(1);
106 |
107 | world.set(stylesAtom, world.get(stylesAtom));
108 | expect(called).toBeCalledTimes(1);
109 |
110 | expect(console.warn).toHaveBeenCalledTimes(1);
111 |
112 | unsubscribe();
113 | });
114 |
--------------------------------------------------------------------------------
/packages/state/src/lib/atomWithCompare.ts:
--------------------------------------------------------------------------------
1 | import { WritableAtom } from 'jotai';
2 | import { atomWithReducer } from 'jotai/utils';
3 |
4 | /**
5 | * Create an atom with a custom comparison function that only triggers updates
6 | * when `areEqual(prev, next)` is false.
7 | *
8 | * Note: Jotai uses `Object.is` internally to compare values when changes occur.
9 | * If `areEqual(a, b)` returns false, but `Object.is(a, b)` returns true, Jotai
10 | * will not trigger an update.
11 | *
12 | * @param initialValue Initial value.
13 | * @param areEqual Custom compare function. It should return true if the two values are considered equal.
14 | * @returns a writable atom.
15 | */
16 | export function atomWithCompare(
17 | initialValue: Value,
18 | areEqual: (prev: Value, next: Value) => boolean
19 | ): WritableAtom {
20 | return atomWithReducer(initialValue, (prev: Value, next: Value) => {
21 | if (areEqual(prev, next)) {
22 | return prev;
23 | }
24 |
25 | // Jotai limitation:
26 | // areEqual considers values different, but the rest of Jotai considers them the same,
27 | // so downstream computations will not re-run.
28 | if (
29 | typeof process === 'object' &&
30 | process.env['NODE_ENV'] !== 'production' &&
31 | Object.is(prev, next)
32 | ) {
33 | console.warn(
34 | 'atomWithCompare: areEqual(',
35 | prev,
36 | ', ',
37 | next,
38 | ') returned false, but the values are the same. State will not be updated.',
39 | areEqual
40 | );
41 | }
42 |
43 | return next;
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/packages/state/src/lib/implicitAtom.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from 'jotai';
2 | import {
3 | callWithCapability,
4 | currentCapabilities,
5 | dropTopLevelCapability,
6 | enterTopLevelCapability,
7 | jotaiStoreCapabilities,
8 | } from './implicitCapabilities';
9 | import { computedAtom, implicitAtom } from './implicitAtom';
10 |
11 | function useTestWorld() {
12 | const state: { world: ReturnType } = {
13 | world: undefined as any,
14 | };
15 |
16 | beforeEach(() => {
17 | state.world = jotaiStoreCapabilities(createStore());
18 | enterTopLevelCapability(state.world);
19 | });
20 |
21 | afterEach(() => {
22 | dropTopLevelCapability(state.world);
23 | });
24 |
25 | return state;
26 | }
27 |
28 | describe('implicit atoms', () => {
29 | const state = useTestWorld();
30 |
31 | describe('.state', () => {
32 | it('reads from current capability', () => {
33 | const atom1 = implicitAtom(() => 1);
34 | expect(atom1.state).toBe(1);
35 | currentCapabilities.must.set(atom1, 'outer');
36 |
37 | callWithCapability(jotaiStoreCapabilities(createStore()), () => {
38 | expect(atom1.state).toBe(1);
39 | currentCapabilities.must.set(atom1, 'inner');
40 | expect(atom1.state).toBe('inner');
41 | });
42 |
43 | expect(atom1.state).toBe('outer');
44 |
45 | dropTopLevelCapability(state.world);
46 | expect(() => atom1.state).toThrow();
47 | });
48 | });
49 |
50 | it('passes a smoketest', () => {
51 | const itemsAtom = implicitAtom(() => [
52 | {
53 | id: 0,
54 | name: 'hello',
55 | done: false,
56 | },
57 | {
58 | id: 1,
59 | name: 'hello',
60 | done: false,
61 | },
62 | {
63 | id: 2,
64 | name: 'hello',
65 | done: false,
66 | },
67 | ]);
68 |
69 | const searchAtom = implicitAtom(() => '');
70 |
71 | const firstItemAtom = computedAtom(() =>
72 | itemsAtom.state.find((item) => item.name.includes(searchAtom.state))
73 | );
74 |
75 | function addItem(name: string) {
76 | itemsAtom.update((state) => [
77 | ...state,
78 | { id: state.length, name, done: false },
79 | ]);
80 | }
81 |
82 | function completeSelected() {
83 | const selected = firstItemAtom.state;
84 | if (selected) {
85 | const items = itemsAtom.state.map((item) => {
86 | if (item === selected) {
87 | return {
88 | ...item,
89 | done: true,
90 | };
91 | }
92 | return item;
93 | });
94 | itemsAtom.setState(items);
95 | searchAtom.reset();
96 | }
97 | }
98 |
99 | addItem('goodbye');
100 | expect(itemsAtom.state).toContainEqual({
101 | id: 3,
102 | name: 'goodbye',
103 | done: false,
104 | });
105 |
106 | searchAtom.setState('good');
107 | expect(firstItemAtom.state).toEqual({
108 | id: 3,
109 | name: 'goodbye',
110 | done: false,
111 | });
112 |
113 | completeSelected();
114 | expect(itemsAtom.state).toContainEqual({
115 | id: 3,
116 | name: 'goodbye',
117 | done: true,
118 | });
119 | });
120 |
121 | describe('toString', () => {
122 | it('returns a string', () => {
123 | const atom = implicitAtom(() => 'hello');
124 | expect(atom.toString()).toMatch(/^WritableImplicitAtom \d \(created at/);
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/packages/state/src/lib/implicitAtom.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Implicit atoms are Jotai atoms that can be accessed outside React using the
3 | * current implicit capabilities.
4 | *
5 | * There are two goals of implicit atoms:
6 | *
7 | * - Substantially reduce the boilerplate to use Jotai by providing an API
8 | * closer to normal Flux stores.
9 | * - Provide a way to migrate to Jotai gradually from more simple state
10 | * management containers.
11 | *
12 | * The migration path is as follows, conceptually:
13 | *
14 | * 1. Create a global jotai Store and set it as the baseline capability.
15 | * 2. Replace flux stores with implicit atoms, which have a flux store interface but
16 | * store and compute state using the global Jotai store capability.
17 | * 3. Slowly replace computed implicit atoms with (vanilla Jotai) explicit atoms.
18 | * 4. Slowly wrap global actions in implicit capability actions.
19 | * 5. Stop providing global capability to access your jotai Store.
20 | */
21 |
22 | import { Atom, WritableAtom, atom, Getter, Setter } from 'jotai';
23 | import {
24 | currentCapabilities,
25 | callWithCapability,
26 | } from './implicitCapabilities';
27 | import { proxyCompareAtom } from './proxyCompareAtom';
28 | import { shallowEqual } from './shallowEqualAtom';
29 |
30 | function isDebugMode() {
31 | return (
32 | typeof process === 'object' &&
33 | typeof process.env === 'object' &&
34 | process.env['NODE_ENV'] !== 'production'
35 | );
36 | }
37 |
38 | function relevantStackTrace(above: number) {
39 | const stack = new Error().stack || '';
40 | if (stack.length === 0) {
41 | return stack;
42 | }
43 |
44 | const lines = stack.split('\n').slice(above);
45 | const isModuleBoundary = (line: string) =>
46 | line.match(/node_modules\/|__webpack_require__/);
47 |
48 | const firstFrameworkLine = lines.findIndex(isModuleBoundary);
49 | const lastFrameworkLine =
50 | lines.length - lines.slice().reverse().findIndex(isModuleBoundary);
51 |
52 | if (firstFrameworkLine > 0) {
53 | return lines.slice(0, firstFrameworkLine).join('\n').trim();
54 | }
55 |
56 | if (firstFrameworkLine === 0) {
57 | return lines.slice(lastFrameworkLine).join('\n').trim();
58 | }
59 |
60 | return lines.join('\n').trim();
61 | }
62 |
63 | let ImplicitAtomId = 0;
64 |
65 | /**
66 | * An implicit atom can be read using the current scope's capabilities without
67 | * needing access to a `get` function.
68 | */
69 | abstract class ImplicitAtom implements Atom {
70 | #debugInfo?: string;
71 | #id: number;
72 |
73 | constructor() {
74 | this.#id = ++ImplicitAtomId;
75 | if (isDebugMode()) {
76 | const relevant = relevantStackTrace(5);
77 | this.#debugInfo = `${this.constructor.name} ${
78 | this.#id
79 | } (created ${relevant})`;
80 | }
81 | }
82 |
83 | get state(): T {
84 | return currentCapabilities.must.get(this);
85 | }
86 |
87 | subscribe(callback: () => void): () => void {
88 | return currentCapabilities.must.subscribe(this, callback);
89 | }
90 |
91 | // Implement Atom
92 | debugLabel?: string;
93 | init?: T;
94 |
95 | abstract get read(): Atom['read'];
96 |
97 | toString() {
98 | return this.debugLabel || this.#debugInfo || `implicitAtom${this.#id}`;
99 | }
100 | }
101 |
102 | /**
103 | * A computed implicit atom.
104 | */
105 | export class ComputedAtom extends ImplicitAtom implements Atom {
106 | private readonly atom: Atom;
107 |
108 | constructor(compute: (get: Getter) => T) {
109 | super();
110 | this.atom = proxyCompareAtom((get) =>
111 | callWithCapability({ get }, () => compute(get))
112 | );
113 | this.atom.debugLabel = `internal atom for ${this.toString()}`;
114 | }
115 |
116 | override get read() {
117 | return this.atom.read;
118 | }
119 | }
120 |
121 | // TODO: support writable derived atoms?
122 | export function computedAtom(read: (getter: Getter) => T): ComputedAtom {
123 | return new ComputedAtom(read);
124 | }
125 |
126 | export class WritableImplicitAtom
127 | extends ImplicitAtom
128 | implements WritableAtom
129 | {
130 | setState(value: T) {
131 | const shouldSet = currentCapabilities.canGet()
132 | ? true
133 | : !shallowEqual(value, this.state);
134 |
135 | if (shouldSet) {
136 | currentCapabilities.must.set(this, value);
137 | }
138 | }
139 |
140 | update(update: (value: T) => T) {
141 | this.setState(update(this.state));
142 | }
143 |
144 | reset() {
145 | this.setState(this.getInitialState());
146 | }
147 |
148 | constructor(public getInitialState: () => T) {
149 | super();
150 | this.init = this.getInitialState();
151 | }
152 |
153 | // Interface
154 | override read = (get: Getter): T => get(this);
155 | write: WritableAtom['write'] = (get, set, value): void =>
156 | set(this, value);
157 | }
158 |
159 | export function implicitAtom(
160 | getInitialState: () => T
161 | ): WritableImplicitAtom & WritableAtom {
162 | return new WritableImplicitAtom(getInitialState);
163 | }
164 |
165 | function example() {
166 | const itemsAtom = implicitAtom(() => [
167 | {
168 | id: 1,
169 | name: 'hello',
170 | done: false,
171 | },
172 | {
173 | id: 2,
174 | name: 'hello',
175 | done: false,
176 | },
177 | {
178 | id: 3,
179 | name: 'hello',
180 | done: false,
181 | },
182 | ]);
183 |
184 | const searchAtom = implicitAtom(() => '');
185 |
186 | const firstItemAtom = computedAtom(() =>
187 | itemsAtom.state.find((item) => item.name.includes(searchAtom.state))
188 | );
189 |
190 | function addItem(name: string) {
191 | itemsAtom.update((state) => [
192 | ...state,
193 | { id: state.length, name, done: false },
194 | ]);
195 | }
196 |
197 | function completeSelected() {
198 | const selected = firstItemAtom.state;
199 | if (selected) {
200 | const items = itemsAtom.state.filter((item) => item !== selected);
201 | itemsAtom.setState(items);
202 | searchAtom.reset();
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/packages/state/src/lib/implicitCapabilities.spec.ts:
--------------------------------------------------------------------------------
1 | import { atom, createStore } from 'jotai';
2 | import {
3 | callWithCapability,
4 | currentCapabilities,
5 | dropTopLevelCapability,
6 | enterTopLevelCapability,
7 | jotaiStoreCapabilities,
8 | } from './implicitCapabilities';
9 |
10 | export function useTestWorld() {
11 | const state: { world: ReturnType } = {
12 | world: undefined as any,
13 | };
14 |
15 | beforeEach(() => {
16 | state.world = jotaiStoreCapabilities(createStore());
17 | enterTopLevelCapability(state.world);
18 | });
19 |
20 | afterEach(() => {
21 | dropTopLevelCapability(state.world);
22 | });
23 |
24 | return state;
25 | }
26 |
27 | describe('current capabilities', () => {
28 | it('update for function with `callWithCapability`', () => {
29 | const capabilities = jotaiStoreCapabilities(createStore());
30 |
31 | const atom1 = atom(1);
32 |
33 | expect(currentCapabilities.canGet()).toBe(false);
34 | expect(currentCapabilities.canSet()).toBe(false);
35 | expect(currentCapabilities.canSubscribe()).toBe(false);
36 |
37 | let fnRan = false;
38 | let subscriptionRan = false;
39 | callWithCapability(capabilities, () => {
40 | fnRan = true;
41 | expect(currentCapabilities.canGet()).toBe(true);
42 | expect(currentCapabilities.get?.(atom1)).toBe(1);
43 |
44 | expect(currentCapabilities.canSet()).toBe(true);
45 | currentCapabilities.set?.(atom1, 2);
46 | expect(currentCapabilities.must.get(atom1)).toBe(2);
47 |
48 | expect(currentCapabilities.canSubscribe()).toBe(true);
49 | currentCapabilities.subscribe?.(atom1, () => {
50 | subscriptionRan = true;
51 | });
52 | currentCapabilities.must.set(atom1, 3);
53 | });
54 |
55 | expect(currentCapabilities.canGet()).toBe(false);
56 | expect(currentCapabilities.canSet()).toBe(false);
57 | expect(currentCapabilities.canSubscribe()).toBe(false);
58 |
59 | expect(fnRan).toBe(true);
60 | expect(subscriptionRan).toBe(true);
61 | });
62 |
63 | describe('top-level capabilities', () => {
64 | it('can enter and leave capabilities', () => {
65 | const outerWorld = jotaiStoreCapabilities(createStore());
66 | const innerWorld = jotaiStoreCapabilities(createStore());
67 |
68 | const atom1 = atom(1);
69 |
70 | enterTopLevelCapability(outerWorld);
71 | currentCapabilities.must.set(atom1, 'outer');
72 | expect(currentCapabilities.must.get(atom1)).toBe('outer');
73 |
74 | enterTopLevelCapability(innerWorld);
75 | expect(currentCapabilities.must.get(atom1)).toBe(1);
76 | currentCapabilities.must.set(atom1, 'inner');
77 | expect(currentCapabilities.must.get(atom1)).toBe('inner');
78 |
79 | dropTopLevelCapability(innerWorld);
80 | expect(currentCapabilities.must.get(atom1)).toBe('outer');
81 |
82 | dropTopLevelCapability(outerWorld);
83 | expect(() => currentCapabilities.must.get(atom1)).toThrow();
84 | });
85 | });
86 | });
87 |
88 | describe('jotaiStoreCapabilities', () => {
89 | it('get capability', () => {
90 | const atom1 = atom(1);
91 | const capabilities = jotaiStoreCapabilities(createStore());
92 | expect(capabilities.get(atom1)).toBe(1);
93 |
94 | const computedAtom = atom((get) => get(atom1) + get(atom1));
95 | expect(capabilities.get(computedAtom)).toBe(2);
96 | });
97 |
98 | it('set capability', () => {
99 | const atom1 = atom(1);
100 | const capabilities = jotaiStoreCapabilities(createStore());
101 | capabilities.set(atom1, 2);
102 | expect(capabilities.get(atom1)).toBe(2);
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/packages/state/src/lib/implicitCapabilities.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Access Jotai atoms from outside React using global implicit capabilities.
3 | * This allows you to migrate to Jotai without major restructuring of code that
4 | * uses global Flux stores.
5 | */
6 |
7 | import {
8 | Atom,
9 | atom,
10 | WritableAtom,
11 | READ_ATOM,
12 | Store,
13 | SUBSCRIBE_ATOM,
14 | WRITE_ATOM,
15 | } from 'jotai';
16 | import { proxyCompareAtom } from './proxyCompareAtom';
17 |
18 | export interface WriteCapability {
19 | set(atom: WritableAtom, value: T): void;
20 | }
21 |
22 | export interface ReadCapability {
23 | get(atom: Atom): T;
24 | }
25 |
26 | export interface SubscribeCapability {
27 | subscribe(atom: Atom, callback: () => void): () => void;
28 | }
29 |
30 | export interface ResumableCapability {
31 | canResume: true;
32 | }
33 |
34 | export type Capabilities = (
35 | | WriteCapability
36 | | ReadCapability
37 | | SubscribeCapability
38 | ) &
39 | Partial<
40 | WriteCapability & ReadCapability & SubscribeCapability & ResumableCapability
41 | >;
42 |
43 | export type TopLevelCapabilities = Capabilities & ResumableCapability;
44 |
45 | /**
46 | * The current global capabilities that you can assign.
47 | * We present a facade in front of them to make them easier to use.
48 | */
49 | let STACK_CAPABILITIES: Capabilities | undefined = undefined;
50 |
51 | /**
52 | * A stack of top-level, resumable capabilities.
53 | * TODO: split our state into SYNC_STACK_CAPABILITY and TOP_LEVEL_CAPABILITY,
54 | * and refuse to modify the TOP_LEVEL_CAPABILITY as long as there is something on
55 | * the stack.
56 | */
57 | let TOP_LEVEL_CAPABILITIES: Array = [];
58 |
59 | function getLastCapability(): Capabilities | undefined {
60 | return (
61 | STACK_CAPABILITIES ||
62 | TOP_LEVEL_CAPABILITIES[TOP_LEVEL_CAPABILITIES.length - 1]
63 | );
64 | }
65 |
66 | class CurrentCapabilities {
67 | public readonly must: ReadCapability & WriteCapability & SubscribeCapability =
68 | {
69 | get: (atom) => {
70 | if (!this.canGet()) {
71 | throw new Error('Current context cannot read atoms');
72 | }
73 | return this.get(atom);
74 | },
75 | set: (atom, value) => {
76 | if (!this.canSet()) {
77 | throw new Error('Current context cannot set atoms');
78 | }
79 | return this.set(atom, value);
80 | },
81 | subscribe: (atom, callback) => {
82 | if (!this.canSubscribe()) {
83 | throw new Error('Current context cannot subscribe to atoms');
84 | }
85 | return this.subscribe(atom, callback);
86 | },
87 | } as const;
88 |
89 | get set(): WriteCapability['set'] | undefined {
90 | return getLastCapability()?.set;
91 | }
92 |
93 | canSet(): this is WriteCapability {
94 | return !!this.set;
95 | }
96 |
97 | get get(): ReadCapability['get'] | undefined {
98 | return getLastCapability()?.get;
99 | }
100 |
101 | canGet(): this is ReadCapability {
102 | return !!this.get;
103 | }
104 |
105 | get subscribe(): SubscribeCapability['subscribe'] | undefined {
106 | return getLastCapability()?.subscribe;
107 | }
108 |
109 | canSubscribe(): this is SubscribeCapability {
110 | return !!this.subscribe;
111 | }
112 | }
113 |
114 | /**
115 | * Capabilities to read, write, and subscribe to atoms.
116 | */
117 | export const currentCapabilities = new CurrentCapabilities();
118 |
119 | /**
120 | * Call `fn` with capabilities `capability`.
121 | * @param capability
122 | * @param fn
123 | * @returns the result of `fn`
124 | */
125 | export function callWithCapability(
126 | capability: Capabilities | undefined,
127 | fn: () => T
128 | ): T {
129 | const prev = STACK_CAPABILITIES;
130 | STACK_CAPABILITIES = capability;
131 | try {
132 | return fn();
133 | } finally {
134 | STACK_CAPABILITIES = prev;
135 | }
136 | }
137 |
138 | // IDK wtf the API should be for this.
139 | export function enterTopLevelCapability(capability: TopLevelCapabilities) {
140 | TOP_LEVEL_CAPABILITIES.push(capability);
141 | }
142 |
143 | export function dropTopLevelCapability(capability: TopLevelCapabilities) {
144 | TOP_LEVEL_CAPABILITIES = TOP_LEVEL_CAPABILITIES.filter(
145 | (c) => c !== capability
146 | );
147 | }
148 |
149 | class JotaiStoreCapabilities
150 | implements
151 | ReadCapability,
152 | WriteCapability,
153 | SubscribeCapability,
154 | ResumableCapability
155 | {
156 | constructor(public store: Store) {}
157 |
158 | readonly canResume = true;
159 |
160 | get = (atom: Atom): T => {
161 | // TODO: disallow reads inside React component stacks during development.
162 | const state = this.store[READ_ATOM](atom);
163 | if ('v' in state) {
164 | return state.v as T;
165 | }
166 | throw new Error('no atom init');
167 | };
168 |
169 | set = (atom: WritableAtom, value: T): void => {
170 | this.store[WRITE_ATOM](atom, value);
171 | };
172 |
173 | subscribe = (atom: Atom, callback: () => void): (() => void) => {
174 | return this.store[SUBSCRIBE_ATOM](atom, callback);
175 | };
176 | }
177 |
178 | /**
179 | * Get all capabilities for a Jotai store.
180 | */
181 | export function jotaiStoreCapabilities(
182 | store: Store
183 | ): ReadCapability &
184 | WriteCapability &
185 | SubscribeCapability &
186 | ResumableCapability {
187 | return new JotaiStoreCapabilities(store);
188 | }
189 |
190 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
191 | export function createAction(
192 | name: string,
193 | capability: WriteCapability & Capabilities,
194 | perform: (...args: Args) => void
195 | ) {
196 | const writableAtom = atom(void 0, (get, set, update) => {
197 | callWithCapability({ ...capability, get, set }, () => perform(...update));
198 | });
199 |
200 | const action = (...args: Args) => {
201 | capability.set(writableAtom, args);
202 | };
203 |
204 | Object.defineProperty(action, 'name', {
205 | value: `action(${name}) of ${perform.name})`,
206 | configurable: true,
207 | enumerable: false,
208 | writable: false,
209 | });
210 |
211 | return action;
212 | }
213 |
214 | function example() {
215 | const itemsAtom = atom([
216 | {
217 | id: 1,
218 | name: 'hello',
219 | done: false,
220 | },
221 | {
222 | id: 2,
223 | name: 'hello',
224 | done: false,
225 | },
226 | {
227 | id: 3,
228 | name: 'hello',
229 | done: false,
230 | },
231 | ]);
232 |
233 | const searchAtom = atom('');
234 |
235 | const firstItemAtom = proxyCompareAtom(
236 | (get) =>
237 | get(itemsAtom).find((item) => item.name.includes(get(searchAtom)))?.id
238 | );
239 |
240 | // Examples of actions that use the current capabilities to update atoms.
241 | // TODO: use action constructor
242 | function addItem(name: string) {
243 | const items = [...currentCapabilities.must.get(itemsAtom)];
244 | items.push({ id: items.length + 1, name, done: false });
245 | currentCapabilities.must.set(itemsAtom, items);
246 | }
247 |
248 | function completeSelected() {
249 | const selected = currentCapabilities.must.get(firstItemAtom);
250 | const items = currentCapabilities.must
251 | .get(itemsAtom)
252 | .filter((item) => item.id !== selected);
253 | currentCapabilities.must.set(itemsAtom, items);
254 | currentCapabilities.must.set(searchAtom, '');
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/packages/state/src/lib/proxyCompareAtom.test.ts:
--------------------------------------------------------------------------------
1 | import { atom, createStore } from 'jotai';
2 | import { proxyCompareAtom } from './proxyCompareAtom';
3 | import { jotaiStoreCapabilities } from '..';
4 |
5 | describe('proxyCompareAtom', () => {
6 | it('example of subscription behavior', () => {
7 | const store = jotaiStoreCapabilities(createStore());
8 | const called = jest.fn();
9 | const updated = jest.fn();
10 | const a = atom('a');
11 | const b = atom('b');
12 | const computed = proxyCompareAtom((get) => {
13 | called();
14 | return get(a) + get(b);
15 | });
16 |
17 | // Computed atoms are not computed by default
18 | expect(called).toHaveBeenCalledTimes(0);
19 |
20 | // Computed atoms are not computed when their dependencies are updated
21 | store.set(a, 'c');
22 | expect(called).toHaveBeenCalledTimes(0);
23 |
24 | // NOT TRUE: Computed atoms are computed each time by default
25 | // ACTUALLY TRY: even without subscribers, computed atoms are memoized
26 | // (Note: is this leakier than permitted for Notion?)
27 | store.get(computed);
28 | expect(called).toHaveBeenCalledTimes(1);
29 | store.get(computed);
30 | expect(called).toHaveBeenCalledTimes(1);
31 |
32 | called.mockReset();
33 | const unsubscribe = store.subscribe(computed, updated);
34 |
35 | // Computed atoms are not computed when a subscriber is attached
36 | store.set(a, 'a');
37 | expect(called).toHaveBeenCalledTimes(0);
38 | // Computed atoms call subscriber when a dependency changes,
39 | expect(updated).toHaveBeenCalledTimes(1);
40 |
41 | // Computed atoms with subscriber are memoized
42 | store.get(computed);
43 | expect(called).toHaveBeenCalledTimes(1);
44 | store.get(computed);
45 | expect(called).toHaveBeenCalledTimes(1);
46 |
47 | unsubscribe();
48 | });
49 |
50 | it('should compute once per change', () => {
51 | const store = jotaiStoreCapabilities(createStore());
52 | const called = jest.fn();
53 | const updated = jest.fn();
54 | const a = atom('a');
55 | const b = atom('b');
56 | const computed = proxyCompareAtom((get) => {
57 | called();
58 | return get(a) + get(b);
59 | });
60 | const unsubscribe = store.subscribe(computed, updated);
61 | expect(store.get(computed)).toBe('ab');
62 | expect(called).toBeCalledTimes(1);
63 |
64 | store.set(a, 'c');
65 | expect(store.get(computed)).toBe('cb');
66 | expect(store.get(computed)).toBe('cb');
67 | expect(called).toBeCalledTimes(2);
68 |
69 | unsubscribe();
70 | });
71 |
72 | describe('when dependencies are plain objects', () => {
73 | it('updates when an important property changes', () => {
74 | const store = jotaiStoreCapabilities(createStore());
75 | const called = jest.fn();
76 | const a = atom({ important: 'a', ignored: 1 });
77 | const b = atom({ important: 'b', ignored: 2 });
78 | const computed = proxyCompareAtom((get) => {
79 | called();
80 | return get(a).important + get(b).important;
81 | });
82 |
83 | expect(store.get(computed)).toBe('ab');
84 | expect(called).toBeCalledTimes(1);
85 |
86 | store.set(a, { important: 'a', ignored: 3 });
87 | expect(store.get(computed)).toBe('ab');
88 | expect(called).toBeCalledTimes(1);
89 | });
90 |
91 | it('updates when a nested object identity changes', () => {
92 | const store = jotaiStoreCapabilities(createStore());
93 | const ORIGINAL = { name: 'frob' };
94 | const COPY = { ...ORIGINAL };
95 |
96 | const called = jest.fn();
97 | const a = atom({ ref: ORIGINAL, unimportant: 'foo' });
98 | const computed = proxyCompareAtom((get) => {
99 | called();
100 | return { ref: get(a).ref };
101 | });
102 |
103 | expect(store.get(computed).ref).toBe(ORIGINAL);
104 | expect(called).toBeCalledTimes(1);
105 |
106 | store.set(a, { ref: ORIGINAL, unimportant: 'bar' });
107 | expect(store.get(computed).ref).toBe(ORIGINAL);
108 | expect(called).toBeCalledTimes(1);
109 |
110 | store.set(a, { ref: COPY, unimportant: 'baz' });
111 | expect(store.get(computed).ref).toBe(COPY);
112 | expect(called).toBeCalledTimes(2);
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/packages/state/src/lib/proxyCompareAtom.ts:
--------------------------------------------------------------------------------
1 | import { Atom, atom } from 'jotai';
2 | import {
3 | getUntracked,
4 | isChanged,
5 | createProxy,
6 | affectedToPathList,
7 | } from 'proxy-compare';
8 | import { readReducerAtom } from './readReducerAtom';
9 | import { shallowEqual } from './shallowEqualAtom';
10 |
11 | const DEBUG = false;
12 | function debug(...args: unknown[]) {
13 | if (DEBUG) console.log(...args);
14 | }
15 |
16 | /**
17 | * We can proxy plain objects or plain arrays. Non-objects or special object
18 | * classes are never proxied.
19 | */
20 | function canProxy(value: unknown): value is object | Array {
21 | if (typeof value !== 'object' || value === null) {
22 | return false;
23 | }
24 |
25 | // Allow proxying objects without prototype.
26 | return (
27 | Object.getPrototypeOf(value) === null ||
28 | Object.getPrototypeOf(value) === Object.prototype ||
29 | (Object.getPrototypeOf(value) === Array.prototype && Array.isArray(value))
30 | );
31 | }
32 |
33 | /**
34 | * Deep unwrap any proxies in `value` by mutating `value`. Does not recurse into
35 | * class instances.
36 | */
37 | function untrack(x: T, seen: Set): T {
38 | if (!canProxy(x)) return x;
39 | const untrackedObj = getUntracked(x);
40 | if (untrackedObj !== null) {
41 | return untrackedObj;
42 | }
43 | if (!seen.has(x)) {
44 | seen.add(x);
45 | Object.entries(x).forEach(([k, v]) => {
46 | const vv = untrack(v, seen);
47 | if (!Object.is(vv, v)) x[k as keyof T] = vv;
48 | });
49 | }
50 | return x;
51 | }
52 | interface ProxyState {
53 | deps: Set>;
54 | previousValues: WeakMap, unknown>;
55 | affected: WeakMap>;
56 | proxyCache: WeakMap;
57 | value: T;
58 | }
59 |
60 | class ProxyCompareAtomState implements ProxyState {
61 | deps = new Set>();
62 | previousValues = new WeakMap, unknown>();
63 | affected = new WeakMap>();
64 |
65 | proxyCache: WeakMap;
66 | value: T = undefined as never;
67 |
68 | constructor(proxyCache: WeakMap = new WeakMap()) {
69 | this.proxyCache = proxyCache;
70 | }
71 |
72 | [Symbol.toStringTag] = 'ProxyCompareAtomState';
73 | }
74 |
75 | const Initial = Symbol('initial');
76 |
77 | /**
78 | * Create a Jotai atom that uses `proxy-compare` for fine-grained dependency
79 | * tracking. It will only recompute when the properties you depend on change.
80 | *
81 | * Your derived state should contain only primitives, plain objects, plain
82 | * arrays, and references to existing class instances. Do not return new
83 | * instances that contain state you read from other atoms, or you will leak
84 | * proxy objects.
85 | */
86 | export function proxyCompareAtom(read: Atom['read']): Atom {
87 | const stateAtom: Atom> = readReducerAtom<
88 | undefined,
89 | ProxyState
90 | >(undefined, (previousState, rawGet) => {
91 | if (previousState) {
92 | const {
93 | deps: previousDeps,
94 | previousValues,
95 | affected: previousAffected,
96 | } = previousState;
97 |
98 | debug('have previous state', previousState);
99 |
100 | // Check that dependencies have actually changed, otherwise skip computation.
101 | let changed = false;
102 | for (const depAtom of previousDeps) {
103 | // TODO: this can leak a Jotai core dependency for a cycle, since we need to read
104 | // each previous dependency to check if it changed. If a dependency did change,
105 | // we may not actually need a previous dependency this cycle.
106 | //
107 | // This seems okay since we'll skip the computation anyways since we
108 | // don't take a proxyCompareAtom dependency.
109 | const newValue = rawGet(depAtom);
110 | const previousValue = previousValues.get(depAtom);
111 | if (
112 | newValue !== previousValue &&
113 | isChanged(previousValue, newValue, previousAffected, new WeakMap())
114 | ) {
115 | debug(
116 | 'dependency changed',
117 | atom,
118 | 'from',
119 | previousValue,
120 | '->',
121 | newValue,
122 | 'at',
123 | affectedToPathList(newValue, previousAffected),
124 | previousAffected.get(previousValue as any)
125 | );
126 | changed = true;
127 | }
128 | }
129 |
130 | if (!changed) {
131 | return previousState;
132 | }
133 | } else {
134 | debug('no previous state');
135 | }
136 |
137 | // Perform computation and track a new set of proxy dependencies.
138 | const newState: ProxyState = new ProxyCompareAtomState(
139 | previousState?.proxyCache
140 | );
141 |
142 | function proxyGet(atom: Atom): T {
143 | const newValue = rawGet(atom);
144 | newState.deps.add(atom);
145 | newState.previousValues.set(atom, newValue);
146 |
147 | if (!canProxy(newValue)) {
148 | return newValue;
149 | }
150 |
151 | return createProxy(newValue, newState.affected, newState.proxyCache);
152 | }
153 |
154 | const value = untrack(read(proxyGet), new Set());
155 | if (previousState && shallowEqual(value, previousState.value)) {
156 | return previousState;
157 | }
158 |
159 | newState.value = value;
160 | return newState;
161 | });
162 |
163 | const publicAtom = atom((get) => get(stateAtom).value);
164 | stateAtom.toString = () =>
165 | `proxyCompareAtom internal state for ${publicAtom.toString()}`;
166 |
167 | return publicAtom;
168 | }
169 |
--------------------------------------------------------------------------------
/packages/state/src/lib/readReducerAtom.test.ts:
--------------------------------------------------------------------------------
1 | import { atom, createStore } from 'jotai';
2 | import { jotaiStoreCapabilities } from './implicitCapabilities';
3 | import { readReducerAtom } from './readReducerAtom';
4 |
5 | it('is called with the init value on first read', () => {
6 | const store = jotaiStoreCapabilities(createStore());
7 | const init = 'init' as const;
8 | const derived = 'derived' as const;
9 | const reducer = jest.fn(() => derived);
10 | const atom = readReducerAtom(init, reducer);
11 |
12 | const result = store.get(atom);
13 | expect(reducer).toHaveBeenCalledWith(init, expect.anything());
14 | expect(reducer).toHaveBeenCalledTimes(1);
15 | expect(result).toBe(derived);
16 |
17 | store.get(atom);
18 | expect(reducer).toHaveBeenCalledTimes(1);
19 | });
20 |
21 | it('is called with the previous derived value', () => {
22 | const store = jotaiStoreCapabilities(createStore());
23 | const dep = atom('foo');
24 | const reducer = jest.fn((prev, get) => {
25 | get(dep);
26 | return prev + 1;
27 | });
28 | const reducerAtom = readReducerAtom(0, reducer);
29 |
30 | expect(store.get(reducerAtom)).toBe(1);
31 | expect(reducer).toHaveBeenNthCalledWith(1, 0, expect.anything());
32 |
33 | store.set(dep, '???');
34 | expect(store.get(reducerAtom)).toBe(2);
35 | expect(reducer).toHaveBeenNthCalledWith(2, 1, expect.anything());
36 |
37 | expect(store.get(reducerAtom)).toBe(2);
38 | expect(reducer).toHaveBeenCalledTimes(2);
39 | });
40 |
--------------------------------------------------------------------------------
/packages/state/src/lib/readReducerAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom, Atom, Getter } from 'jotai';
2 |
3 | /**
4 | * A derived atom that can also considers it's previous value to produce the
5 | * next derived value.
6 | */
7 | export function readReducerAtom(
8 | init: Init,
9 | getter: (prev: Init | Derived, get: Getter) => Derived
10 | ): Atom {
11 | const derived = atom((get) => {
12 | const prev = get(derived);
13 | return getter(prev, get);
14 | }) as Atom & { init: Init };
15 | // XXX: using a private API
16 | derived.init = init;
17 | return derived;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/state/src/lib/recursiveAtom.tsx:
--------------------------------------------------------------------------------
1 | import { memoizeWithLRU, memoizeWithWeakMap } from '@jitl/util';
2 | import { Atom, atom, useAtom, WritableAtom } from 'jotai';
3 | import { atomFamily, selectAtom } from 'jotai/utils';
4 |
5 | type AnyAtom = Atom;
6 |
7 | type RecurseAny = (
8 | initialValue: I,
9 | key: (value: I) => unknown,
10 | transform: Transform
11 | ) => WritableRecursiveAtom;
12 |
13 | type Transform = (
14 | initial: I,
15 | recursiveAtomChild: RecurseAny,
16 | self: Transform
17 | ) => O;
18 |
19 | type WritableRecursiveAtom = WritableAtom; /* & {
20 | TODO: how can we support unwrapping this back to the original input type?
21 | unwrapped: Atom;
22 | }; */
23 |
24 | function recursiveAtomRoot(
25 | initialValue: T,
26 | key: (value: T) => unknown,
27 | transform: Transform
28 | ): WritableRecursiveAtom {
29 | const cache = new Map();
30 | const recurse: RecurseAny = (initialValue, getKey, innerTransform) => {
31 | const key = getKey(initialValue);
32 | const cached = cache.get(key);
33 | if (cached) {
34 | return cached as any;
35 | }
36 |
37 | const newAtom = atom(innerTransform(initialValue, recurse, innerTransform));
38 | cache.set(key, newAtom);
39 | return newAtom;
40 | };
41 | return recurse(initialValue, key, transform);
42 | }
43 | /** Example recursive tree */
44 | interface ExampleNode {
45 | id: string;
46 | value?: string;
47 | children?: ExampleNode[];
48 | editedBy?: Array;
49 | }
50 |
51 | interface ExampleUser {
52 | name: string;
53 | email: string;
54 | }
55 |
56 | interface ExampleNodeR {
57 | id: string;
58 | value?: string;
59 | children?: Array>;
60 | editedBy?: Array>;
61 | }
62 |
63 | const DATA: ExampleNode = {
64 | id: 'root',
65 | value: 'hello',
66 | children: [
67 | {
68 | id: 'child1',
69 | children: [
70 | {
71 | id: 'grandchild1',
72 | value: 'blue',
73 | },
74 | ],
75 | },
76 | ],
77 | };
78 |
79 | const root = recursiveAtomRoot(
80 | DATA,
81 | (it) => it.id,
82 | (data, recursiveChild, exampleNodeTransform) => {
83 | return {
84 | ...data,
85 | children: data.children?.map((child) =>
86 | recursiveChild(child, (it) => it.id, exampleNodeTransform)
87 | ),
88 | editedBy: data.editedBy?.map((user) =>
89 | recursiveChild(
90 | user,
91 | (it) => it.email,
92 | (it) => it
93 | )
94 | ),
95 | };
96 | }
97 | );
98 |
99 | // Another tactic
100 | class Normalizer {
101 | normalizeToFamily(
102 | value: T,
103 | atomFamily: (value: T) => O
104 | ): O {}
105 | }
106 |
107 | function NodeRender(props: { nodeAtom: Atom }) {
108 | const { nodeAtom } = props;
109 |
110 | return (
111 |
112 |
{nodeId}
113 |
{nodeValue}
114 |
115 | {children.map((childAtom) => (
116 |
117 |
118 |
119 | ))}
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/packages/state/src/lib/shallowEqualAtom.ts:
--------------------------------------------------------------------------------
1 | import { memoizeWithWeakMap } from '@jitl/util';
2 | import { Atom } from 'jotai';
3 | import { atomWithReducer } from 'jotai/utils';
4 | import { readReducerAtom } from './readReducerAtom';
5 |
6 | export function shallowEqual(old: T, update: T): boolean {
7 | return old === update; // TODO
8 | }
9 |
10 | /**
11 | * An atom that only updates when set to a value that does not shallow-equal the
12 | * current value.
13 | */
14 | export function shallowEqualAtom(initialState: T) {
15 | return atomWithReducer(initialState, (current: T, update: T) => {
16 | if (shallowEqual(current, update)) {
17 | return current;
18 | }
19 | return update;
20 | });
21 | }
22 |
23 | const EMPTY = Symbol('empty');
24 |
25 | export function asShallowEqualAtom(atom: Atom): Atom {
26 | return readReducerAtom(EMPTY, (prev, get) => {
27 | const next = get(atom);
28 | if (prev !== EMPTY && shallowEqual(prev, next)) {
29 | return prev;
30 | }
31 | return next;
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/packages/state/src/lib/withJotaiState.test.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | getByText,
3 | render,
4 | screen,
5 | waitFor,
6 | waitForElementToBeRemoved,
7 | } from '@testing-library/react';
8 | import userEvent from '@testing-library/user-event';
9 |
10 | import { atom, WritableAtom } from 'jotai';
11 | import { ChangeEventHandler, Component } from 'react';
12 | import { act } from 'react-dom/test-utils';
13 | import { classComponentWithJotai, WithJotaiProps } from './withJotaiState';
14 | import { World } from './World';
15 | import { WorldProvider } from './WorldProvider';
16 |
17 | const name = atom('jake');
18 | const capitalizedName = atom(
19 | (get) => get(name).toUpperCase(),
20 | (get, set, str: string) => set(name, str)
21 | );
22 |
23 | class ClassBasedComponent extends Component<
24 | WithJotaiProps & { nameAtom: WritableAtom }
25 | > {
26 | override render() {
27 | return (
28 |
29 | Hello{' '}
30 |
35 |
36 | );
37 | }
38 |
39 | onInputChange: ChangeEventHandler = (event) => {
40 | this.props.act((get, set) => set(this.props.nameAtom, event.target.value));
41 | };
42 | }
43 |
44 | const WrappedComponent = classComponentWithJotai(ClassBasedComponent);
45 |
46 | describe('with console.error muted', () => {
47 | const originalError = console.error;
48 | beforeEach(() => {
49 | console.error = jest.fn();
50 | });
51 |
52 | afterEach(() => {
53 | console.error = originalError;
54 | });
55 |
56 | it('requires World provider', () => {
57 | // Error.
58 | expect(() => render( )).toThrowError(
59 | /WorldProvider/
60 | );
61 |
62 | // Ok.
63 | const world = World.syncWorld();
64 | render(
65 |
66 |
67 |
68 | );
69 | });
70 | });
71 |
72 | it('rerenders when atom changes', async () => {
73 | const world = World.syncWorld();
74 | render(
75 |
76 |
77 |
78 | );
79 | expect(Array.from(world.changeSubscriber.getSubscribedAtoms())).toEqual([
80 | name,
81 | ]);
82 | act(() => world.capabilities.set(name, 'nora'));
83 | await waitFor(() => screen.getByDisplayValue('nora'));
84 | });
85 |
86 | it('rerenders when derived atom changes', async () => {
87 | const world = World.syncWorld();
88 | render(
89 |
90 |
91 |
92 | );
93 | act(() => world.capabilities.set(name, 'nora'));
94 | await waitFor(() => screen.getByDisplayValue('NORA'));
95 | });
96 |
97 | it('can change atoms', () => {
98 | const world = World.syncWorld();
99 | render(
100 |
101 |
102 |
103 | );
104 |
105 | userEvent.type(screen.getByTitle('name'), ' tl');
106 |
107 | expect(world.capabilities.get(name)).toBe('jake tl');
108 | });
109 |
110 | it('does not leak effects', async () => {
111 | const leaky = atom('leaky');
112 | const world = World.syncWorld();
113 | const { unmount } = render(
114 |
115 |
116 |
117 | );
118 | await waitFor(() => screen.getByTitle('name'));
119 | unmount();
120 |
121 | expect(Array.from(world.changeSubscriber.getSubscribedAtoms())).toEqual([]);
122 | });
123 |
--------------------------------------------------------------------------------
/packages/state/src/lib/withJotaiState.tsx:
--------------------------------------------------------------------------------
1 | import { atom, Atom, Getter, Setter } from 'jotai';
2 | import * as React from 'react';
3 | import { useCallback, useEffect, useRef, useState } from 'react';
4 | import { callWithCapability } from '..';
5 | import { useWorld } from './WorldProvider';
6 |
7 | type AnyAtom = Atom;
8 | type RunAction = (perform: (get: Getter, set: Setter) => R) => R;
9 |
10 | /**
11 | * Props provided to class-based Jotai components by `classComponentWithJotai`.
12 | */
13 | export type WithJotaiProps = {
14 | /** Get state of atom, and possibly subscribe to it */
15 | get: Getter;
16 | /** Perform an action */
17 | act: RunAction;
18 | /** Forces a re-render of pure components */
19 | _revision: number;
20 | /** Used to make render reactive */
21 | _render: (render: () => React.ReactNode) => React.ReactNode;
22 | };
23 |
24 | /**
25 | * Higher order component that wraps a class component's `render` method to make
26 | * it more reactive.
27 | *
28 | * These components require a to be present in the component tree.
29 | *
30 | * Status: crazy, untested idea.
31 | */
32 | export function classComponentWithJotai<
33 | Props,
34 | ComponentT extends React.Component
35 | >(
36 | Component: React.ComponentClass,
37 | displayName?: string
38 | ): React.FunctionComponent> {
39 | // Wrap the render method to make it reactive.
40 | if (!(typeof Component.prototype.render === 'function')) {
41 | throw new Error(
42 | 'Component must have a render method on its prototype we can make reactive'
43 | );
44 | }
45 |
46 | class ReactiveSubclass extends Component {
47 | override render() {
48 | return this.props._render(() => super.render());
49 | }
50 | }
51 |
52 | Object.defineProperty(ReactiveSubclass, 'name', {
53 | value: displayName ?? Component.name,
54 | configurable: true,
55 | writable: false,
56 | enumerable: false,
57 | });
58 |
59 | const WrappedComponent = React.forwardRef(
60 | (props: Props, ref: React.Ref) => {
61 | const world = useWorld();
62 | const [revision, setRevision] = useState(0);
63 | const mounted = useRef(true);
64 | const iteration = useRef(0);
65 | const rendering = useRef(false);
66 | const subscriptions = useRef(
67 | new Map void }>()
68 | );
69 |
70 | const forceRender = useCallback(() => {
71 | if (mounted.current) {
72 | setRevision((r) => r + 1);
73 | }
74 | }, []);
75 |
76 | const getAndSubscribe = useCallback(
77 | function (atom: Atom): T {
78 | const subscription = subscriptions.current.get(atom) || {
79 | iteration: iteration.current,
80 | unsubscribe: world.changeSubscriber.subscribeChangeEffect(
81 | atom,
82 | forceRender
83 | ),
84 | };
85 | subscription.iteration = iteration.current;
86 | subscriptions.current.set(atom, subscription);
87 |
88 | return world.capabilities.get(atom);
89 | },
90 | [world, forceRender]
91 | );
92 |
93 | const getAtom: Getter = useCallback(
94 | (atom: AnyAtom) => {
95 | if (rendering.current && mounted.current) {
96 | return getAndSubscribe(atom);
97 | } else {
98 | return world.capabilities.get(atom);
99 | }
100 | },
101 | [getAndSubscribe, world]
102 | );
103 |
104 | const act: RunAction = useCallback(
105 | function (perform: (getter: Getter, setter: Setter) => R): R {
106 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
107 | let result: any;
108 | const writeOnlyAtom = atom(
109 | () => undefined,
110 | (get, set) => {
111 | result = callWithCapability({ get, set }, () =>
112 | perform(get, set)
113 | );
114 | }
115 | );
116 | world.capabilities.set(writeOnlyAtom, undefined);
117 | return result;
118 | },
119 | [world]
120 | );
121 |
122 | const _render = useCallback(
123 | (renderFn: () => React.ReactNode) => {
124 | rendering.current = true;
125 | iteration.current++;
126 | try {
127 | const result = callWithCapability({ get: getAtom }, renderFn);
128 | for (const [atom, subscription] of subscriptions.current) {
129 | if (subscription.iteration !== iteration.current) {
130 | subscription.unsubscribe();
131 | subscriptions.current.delete(atom);
132 | }
133 | }
134 | return result;
135 | } finally {
136 | rendering.current = false;
137 | }
138 | },
139 | [getAtom]
140 | );
141 |
142 | useEffect(() => {
143 | const res = () => {
144 | mounted.current = false;
145 | // eslint-disable-next-line react-hooks/exhaustive-deps
146 | for (const { unsubscribe } of subscriptions.current.values()) {
147 | unsubscribe();
148 | }
149 | };
150 | return res;
151 | }, []);
152 |
153 | const enhancedProps: WithJotaiProps = {
154 | get: getAtom,
155 | act,
156 | _revision: revision,
157 | _render,
158 | };
159 |
160 | return ;
161 | }
162 | );
163 |
164 | const componentDisplayName =
165 | displayName || `withJotaiState(${Component.displayName || Component.name})`;
166 | WrappedComponent.displayName = componentDisplayName;
167 | Object.defineProperty(WrappedComponent, 'name', {
168 | value: componentDisplayName,
169 | configurable: true,
170 | enumerable: false,
171 | writable: false,
172 | });
173 |
174 | return WrappedComponent as any;
175 | }
176 |
--------------------------------------------------------------------------------
/packages/state/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "allowJs": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "noImplicitOverride": true,
11 | "noPropertyAccessFromIndexSignature": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "downlevelIteration": true
15 | },
16 | "files": [],
17 | "include": [],
18 | "references": [
19 | {
20 | "path": "./tsconfig.lib.json"
21 | },
22 | {
23 | "path": "./tsconfig.spec.json"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/state/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "../../dist/out-tsc",
6 | "declaration": true,
7 | "types": ["node"]
8 | },
9 | "exclude": [
10 | "**/*.spec.ts",
11 | "**/*.test.ts",
12 | "**/*.spec.tsx",
13 | "**/*.test.tsx",
14 | "**/*.spec.js",
15 | "**/*.test.js",
16 | "**/*.spec.jsx",
17 | "**/*.test.jsx"
18 | ],
19 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/state/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "**/*.test.ts",
10 | "**/*.spec.ts",
11 | "**/*.test.tsx",
12 | "**/*.spec.tsx",
13 | "**/*.test.js",
14 | "**/*.spec.js",
15 | "**/*.test.jsx",
16 | "**/*.spec.jsx",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/state/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/node@^17.0.4":
6 | version "17.0.6"
7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.6.tgz#cc1589c9ee853b389e67e8fb4384e0f250a139b9"
8 | integrity sha512-+XBAjfZmmivILUzO0HwBJoYkAyyySSLg5KCGBDFLomJo0sV6szvVLAf4ANZZ0pfWzgEds5KmGLG9D5hfEqOhaA==
9 |
10 | "jotai@npm:@jitl/jotai@^1.5.1":
11 | version "1.5.1"
12 | resolved "https://registry.yarnpkg.com/@jitl/jotai/-/jotai-1.5.1.tgz#b68875d3333b8ee7100eeab33f715cdd0fd2085c"
13 | integrity sha512-JsrQM+F2bQtm7/etSW8xvKa91GsgIb3Vv3Gs5B/KlqXSjvT7IcWrzs9PW50o+0BooWPbSsuKO20uUqEYdp+CTA==
14 |
15 | proxy-compare@^2.0.2:
16 | version "2.0.2"
17 | resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.0.2.tgz#343e624d0ec399dfbe575f1d365d4fa042c9fc69"
18 | integrity sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A==
19 |
--------------------------------------------------------------------------------
/packages/util/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/util/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/util/README.md:
--------------------------------------------------------------------------------
1 | # @jitl/util
2 |
3 | Miscellaneous TypeScript utilities.
4 |
5 | ## Development
6 |
7 | ### Running unit tests
8 |
9 | Run `nx test util` to execute the unit tests via [Jest](https://jestjs.io).
10 |
--------------------------------------------------------------------------------
/packages/util/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'util',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | transform: {
11 | '^.+\\.[tj]sx?$': 'ts-jest',
12 | },
13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
14 | coverageDirectory: '../../coverage/packages/util',
15 | };
16 |
--------------------------------------------------------------------------------
/packages/util/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jitl/util",
3 | "version": "0.0.3"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/util/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "packages/util",
3 | "sourceRoot": "packages/util/src",
4 | "projectType": "library",
5 | "targets": {
6 | "lint": {
7 | "executor": "@nrwl/linter:eslint",
8 | "outputs": ["{options.outputFile}"],
9 | "options": {
10 | "lintFilePatterns": ["packages/util/**/*.ts"]
11 | }
12 | },
13 | "test": {
14 | "executor": "@nrwl/jest:jest",
15 | "outputs": ["coverage/packages/util"],
16 | "options": {
17 | "jestConfig": "packages/util/jest.config.js",
18 | "passWithNoTests": true
19 | }
20 | },
21 | "build": {
22 | "executor": "@nrwl/node:package",
23 | "outputs": ["{options.outputPath}"],
24 | "options": {
25 | "outputPath": "dist/packages/util",
26 | "tsConfig": "packages/util/tsconfig.lib.json",
27 | "packageJson": "packages/util/package.json",
28 | "main": "packages/util/src/index.ts",
29 | "assets": ["packages/util/*.md"]
30 | }
31 | }
32 | },
33 | "tags": []
34 | }
35 |
--------------------------------------------------------------------------------
/packages/util/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/object';
2 | export * from './lib/map';
3 | export * from './lib/memo';
4 | export * from './lib/typeAssertions';
5 | export * from './lib/typeGuards';
6 | export * from './lib/types';
7 |
--------------------------------------------------------------------------------
/packages/util/src/lib/map.spec.ts:
--------------------------------------------------------------------------------
1 | import { mapMustGet } from '..';
2 | import { mapGetOrCreate } from './map';
3 |
4 | describe(mapGetOrCreate, () => {
5 | it('should return existing', () => {
6 | const map = new Map([[1, 1]]);
7 | const result = mapGetOrCreate(map, 1, () => 2);
8 | expect(result).toEqual({ value: 1, created: false });
9 | });
10 |
11 | it('should create if missing', () => {
12 | const map = new Map();
13 | const result = mapGetOrCreate(map, 1, () => 2);
14 | expect(result).toEqual({ value: 2, created: true });
15 | expect(map.has(1)).toBe(true);
16 | expect(map.get(1)).toBe(2);
17 | });
18 | });
19 |
20 | describe(mapMustGet, () => {
21 | it('should return existing', () => {
22 | const map = new Map([[1, 1]]);
23 | const result = mapMustGet(map, 1);
24 | expect(result).toBe(1);
25 | });
26 |
27 | it('should throw if missing', () => {
28 | const map = new Map();
29 | expect(() => mapMustGet(map, 1)).toThrowError(/Missing key 1/);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/util/src/lib/map.ts:
--------------------------------------------------------------------------------
1 | export interface MapLike {
2 | get(key: K): V | undefined;
3 | has(key: K): boolean;
4 | set(key: K, value: V): void;
5 | }
6 |
7 | /**
8 | * Get a value from a map-like object if present, or create the value if missing.
9 | * @param map map-like object
10 | * @param key key
11 | * @param create produce a value if missing
12 | * @returns existing or newly-created value
13 | */
14 | export function mapGetOrCreate(
15 | map: MapLike,
16 | key: K,
17 | create: () => V
18 | ): { value: V; created: boolean } {
19 | if (map.has(key)) {
20 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21 | return { value: map.get(key)!, created: false };
22 | }
23 | const value = create();
24 | map.set(key, value);
25 | return { value, created: true };
26 | }
27 |
28 | export function mapMustGet(map: MapLike, key: K): V {
29 | if (!map.has(key)) {
30 | throw new Error(`Missing key ${key}`);
31 | }
32 |
33 | return map.get(key) as V;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/util/src/lib/memo.test.ts:
--------------------------------------------------------------------------------
1 | import { memoizeWithWeakMap, memoizeWithLRU } from './memo';
2 |
3 | describe(memoizeWithWeakMap, () => {
4 | it('should memoize with weak map', () => {
5 | const fn = jest.fn((a: object, b: object) => Object.keys(a).concat(Object.keys(b)));
6 | const memoized = memoizeWithWeakMap(fn);
7 | const foo = { foo: 1 };
8 | const bar = { bar: 2 };
9 | const baz = { baz: 3 };
10 |
11 | expect(memoized(foo, bar)).toEqual(['foo', 'bar']);
12 | expect(memoized(foo, bar)).toEqual(['foo', 'bar']);
13 | expect(fn).toHaveBeenCalledTimes(1);
14 |
15 | expect(memoized(foo, baz)).toEqual(['foo', 'baz']);
16 | expect(memoized(foo, baz)).toEqual(['foo', 'baz']);
17 | expect(fn).toHaveBeenCalledTimes(2);
18 | });
19 |
20 | it('throws when called with non-object', () => {
21 | const fn = jest.fn();
22 | const memoized = memoizeWithWeakMap(fn);
23 | expect(() => memoized(1, 2)).toThrow(/weak map key/);
24 | });
25 |
26 | it('has no size limit', () => {
27 | const fn = jest.fn((a: object, b: object) => Object.keys(a).concat(Object.keys(b)));
28 | const memoized = memoizeWithWeakMap(fn);
29 |
30 | const N = 10000;
31 | const pairs = [];
32 | for (let i = 0; i < N; i++) {
33 | const a = { a: 1 };
34 | const b = { b: 2 };
35 | pairs.push([a, b]);
36 | expect(memoized(a, b)).toEqual(['a', 'b']);
37 | expect(memoized(a, b)).toEqual(['a', 'b']);
38 | }
39 | for (const [a, b] of pairs) {
40 | // expect(memoized(a, b)).toEqual(['a', 'b']);
41 | }
42 | expect(fn).toBeCalledTimes(N);
43 | });
44 | });
45 |
46 | describe(memoizeWithLRU, () => {
47 | function withNEntries(N: number) {
48 | describe(`with ${N} entries`, () => {
49 | it('can remember a function with several arguments', () => {
50 | const sum = (...args: number[]) => args.reduce((a, b) => a + b, 0);
51 | const fn = jest.fn(sum);
52 | const memo = memoizeWithLRU(N, fn);
53 |
54 | for (let i = 0; i < N; i++) {
55 | expect(memo(i, N - i)).toBe(i + N - i);
56 | }
57 | for (let i = 0; i < N; i++) {
58 | expect(memo(i, N - i)).toBe(i + N - i);
59 | }
60 | expect(fn).toHaveBeenCalledTimes(N);
61 |
62 | memo(-1, -2);
63 | expect(fn).toHaveBeenCalledTimes(N + 1);
64 | if (N > 1) {
65 | memo(-1, -2);
66 | expect(fn).toHaveBeenCalledTimes(N + 1);
67 | }
68 |
69 | fn.mockClear();
70 |
71 | const args = [];
72 | for (let i = 0; i < N; i++) {
73 | args.push(i * i);
74 | expect(memo(...args)).toEqual(sum(...args));
75 | expect(memo(...args)).toEqual(sum(...args));
76 | }
77 | expect(fn).toHaveBeenCalledTimes(N);
78 | });
79 |
80 | it(`can remember ${N} previous calls`, () => {
81 | const fn = jest.fn((val: number) => val * val);
82 | const memo = memoizeWithLRU(N, fn);
83 | for (let i = 0; i < N; i++) {
84 | expect(memo(i)).toBe(i * i);
85 | }
86 | for (let i = 0; i < N; i++) {
87 | expect(memo(i)).toBe(i * i);
88 | }
89 | expect(fn).toHaveBeenCalledTimes(N);
90 |
91 | // This should force i = 0 to recompute
92 | memo(N);
93 | fn.mockClear();
94 |
95 | // This will drop i = 1
96 | memo(0);
97 | expect(fn).toHaveBeenCalledTimes(1);
98 |
99 | if (N > 2) {
100 | memo(2);
101 | expect(fn).toHaveBeenCalledTimes(1);
102 | }
103 | });
104 | });
105 | }
106 |
107 | withNEntries(0);
108 | withNEntries(1);
109 | withNEntries(2);
110 | withNEntries(11);
111 | withNEntries(100);
112 | });
113 |
--------------------------------------------------------------------------------
/packages/util/src/lib/memo.ts:
--------------------------------------------------------------------------------
1 | import { mapMustGet } from '..';
2 | import { mapGetOrCreate, MapLike } from './map';
3 | import { assertDefined, mustBeDefined } from './typeAssertions';
4 |
5 | interface MapDeleteLike extends MapLike {
6 | size?: number;
7 | delete(key: K): boolean;
8 | }
9 |
10 | interface MemoNode {
11 | memo?: R;
12 | children?: MapDeleteLike>;
13 | }
14 |
15 | export function memoizeWithWeakMap(
16 | fn: (...args: Args) => R
17 | ): (...args: Args) => R {
18 | const root: MemoNode = {};
19 | return (...args: Args): R => {
20 | let node = root;
21 | for (const arg of args) {
22 | node.children ??= new WeakMap();
23 | node = mapGetOrCreate(node.children, arg, () => ({})).value;
24 | }
25 |
26 | if ('memo' in node) {
27 | return node.memo as R;
28 | }
29 |
30 | node.memo = fn(...args);
31 | return node.memo;
32 | };
33 | }
34 |
35 | function memoIsEmpty(node: MemoNode) {
36 | if ('memo' in node) {
37 | return false;
38 | }
39 |
40 | if (node.children) {
41 | return node.children.size === 0;
42 | }
43 |
44 | return true;
45 | }
46 |
47 | export function memoizeWithLRU(
48 | limit: number,
49 | fn: (...args: Args) => R
50 | ): (...args: Args) => R {
51 | if (limit < 1) {
52 | return fn;
53 | }
54 |
55 | const root: MemoNode<{ result: R; args: Args }> = {};
56 | const lru: Args[] = [];
57 | const infinite = limit === Infinity;
58 |
59 | return (...args: Args) => {
60 | let node = root;
61 | for (const arg of args) {
62 | node.children ??= new Map();
63 | node = mapGetOrCreate(node.children, arg, () => ({})).value;
64 | }
65 |
66 | if (node.memo) {
67 | if (!infinite) {
68 | lru.splice(lru.indexOf(node.memo.args), 1);
69 | lru.push(node.memo.args);
70 | }
71 | return node.memo.result;
72 | }
73 |
74 | const result = fn(...args);
75 | node.memo = {
76 | args,
77 | result,
78 | };
79 | if (!infinite) {
80 | lru.push(args);
81 | }
82 |
83 | if (lru.length > limit) {
84 | const del = lru.shift();
85 | if (del) {
86 | let delNode = root;
87 | const parents: Array = [];
88 | for (const arg of del) {
89 | assertDefined(delNode.children);
90 | parents.push(delNode);
91 | delNode = mapMustGet(delNode.children, arg);
92 | }
93 |
94 | delete delNode.memo;
95 |
96 | for (let i = del.length - 1; i >= 0; i--) {
97 | const arg = del[i];
98 | const argParent = mustBeDefined(parents[i]);
99 | if (memoIsEmpty(delNode)) {
100 | assertDefined(argParent.children);
101 | argParent.children.delete(arg);
102 | delNode = argParent;
103 | } else {
104 | break;
105 | }
106 | }
107 | }
108 | }
109 |
110 | return result;
111 | };
112 | }
113 |
--------------------------------------------------------------------------------
/packages/util/src/lib/object.test.ts:
--------------------------------------------------------------------------------
1 | import { objectKeys, objectEntries } from './object';
2 | import * as fc from 'fast-check';
3 |
4 | describe(objectKeys, () => {
5 | it('should return all keys', () => {
6 | fc.assert(
7 | fc.property(fc.object(), (value) => {
8 | expect(objectKeys(value)).toEqual(Object.keys(value));
9 | })
10 | );
11 | });
12 | });
13 |
14 | describe(objectEntries, () => {
15 | it('should return all entries', () => {
16 | fc.assert(
17 | fc.property(fc.object(), (value) => {
18 | expect(objectEntries(value)).toEqual(Object.entries(value));
19 | })
20 | );
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/util/src/lib/object.ts:
--------------------------------------------------------------------------------
1 | export function objectKeys(value: T): Array {
2 | return Object.keys(value) as Array;
3 | }
4 |
5 | export type ObjectEntries = { [K in keyof T]: [K, T[K]] }[keyof T];
6 |
7 | export function objectEntries(value: T): Array> {
8 | return Object.entries(value) as Array>;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/util/src/lib/typeAssertions.ts:
--------------------------------------------------------------------------------
1 | export function unreachable(value: never): never {
2 | throw new Error(`Should never occur: ${value}`);
3 | }
4 |
5 | export function mustBeDefined(value: T | undefined): T {
6 | assertDefined(value);
7 | return value;
8 | }
9 |
10 | export function assertDefined(value: T | undefined): asserts value is T {
11 | if (value === undefined) {
12 | throw new Error('Value is undefined');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/util/src/lib/typeGuards.ts:
--------------------------------------------------------------------------------
1 | export function isDefined(value: T | undefined): value is T {
2 | return value !== undefined;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/util/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Assert that U is assignable to T.
3 | */
4 | export type Assert = U;
5 |
6 | /**
7 | * If used for inference, validate that T is exactly Shape
8 | */
9 | export type ValidateShape = T extends Shape
10 | ? Exclude extends never
11 | ? T
12 | : never
13 | : never;
14 |
15 | export interface ExactShapeHelper {
16 | /** Declare a value that has exact type `T`. */
17 | of<
18 | /**
19 | * Inferred type of the declaration. Allowing this to be inferred is important to provide validation.
20 | * The default type provides auto-completion.
21 | */
22 | U = T
23 | >(
24 | value: ValidateShape
25 | ): U;
26 | }
27 |
28 | const STATIC_SHAPE_HELPER: ExactShapeHelper = Object.freeze({
29 | of: (value: any) => value,
30 | });
31 |
32 | // If only we could do this without a function call.
33 | // https://github.com/microsoft/TypeScript/issues/35986#issuecomment-1035310444
34 | /**
35 | * Helper to declare values of exact type `T`.
36 | *
37 | * ```typescript
38 | * const extra = { extra: true } as const
39 | * const problem: { good: true, valid: true } = {
40 | * good: true,
41 | * valid: true,
42 | * ...extra, // This is okay in Typescript
43 | * }
44 | * const solution = Exact<{ good: true, valid: true }>().of({
45 | * good: true,
46 | * valid: true,
47 | * ...extra, // Error: Not assignable to parameter of type 'never'.
48 | * })
49 | * ```
50 | */
51 | export function Exact(): ExactShapeHelper {
52 | return STATIC_SHAPE_HELPER;
53 | }
54 |
--------------------------------------------------------------------------------
/packages/util/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.lib.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/util/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "../../dist/out-tsc",
6 | "declaration": true,
7 | "types": ["node"]
8 | },
9 | "exclude": ["**/*.spec.ts", "**/*.test.ts"],
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/util/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "**/*.test.ts",
10 | "**/*.spec.ts",
11 | "**/*.test.tsx",
12 | "**/*.spec.tsx",
13 | "**/*.test.js",
14 | "**/*.spec.js",
15 | "**/*.test.jsx",
16 | "**/*.spec.jsx",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/rebuild-api.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eo pipefail
4 |
5 | DIR="dist/packages/notion-api"
6 | TEMPDIR="dist/notion-api-temp"
7 | SAVE=(node_modules yarn.lock)
8 |
9 | mkdir -p "$TEMPDIR"
10 |
11 | for file in "${SAVE[@]}" ; do
12 | if [[ -e "$DIR/$file" ]]; then
13 | mv "$DIR/$file" "$TEMPDIR/$file"
14 | fi
15 | done
16 |
17 | yarn nx build notion-api
18 |
19 | for file in "${SAVE[@]}" ; do
20 | if [[ -e "$TEMPDIR/$file" ]]; then
21 | mv -v "$TEMPDIR/$file" "$DIR/$file"
22 | fi
23 | done
24 |
25 | cd "$DIR"
26 | yarn install
27 | yarn link
28 | yarn link @notionhq/client
29 |
--------------------------------------------------------------------------------
/tools/generators/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjake/monorepo/d1e87174827005fa7fd6d158a0a1d7e86dd2a396/tools/generators/.gitkeep
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"],
9 | "importHelpers": false
10 | },
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "target": "es2019",
14 | "module": "esnext",
15 | "lib": ["es2019", "dom", "DOM.Iterable"],
16 | "skipLibCheck": true,
17 | "skipDefaultLibCheck": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "@jitl/example-library": ["packages/example-library/src/index.ts"],
21 | "@jitl/notion-api": ["packages/notion-api/src/index.ts"],
22 | "@jitl/pinch-zoom": ["packages/pinch-zoom/src/index.ts"],
23 | "@jitl/state": ["packages/state/src/index.ts"],
24 | "@jitl/util": ["packages/util/src/index.ts"]
25 | }
26 | },
27 | "exclude": ["node_modules", "tmp"]
28 | }
29 |
--------------------------------------------------------------------------------
/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "projects": {
4 | "example-library": "packages/example-library",
5 | "notion-api": "packages/notion-api",
6 | "pinch-zoom": "packages/pinch-zoom",
7 | "playground": "packages/playground",
8 | "playground-e2e": "packages/playground-e2e",
9 | "state": "packages/state",
10 | "util": "packages/util"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------