├── .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 | --------------------------------------------------------------------------------