diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000..444fd0e --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,39 @@ +name: build-docs +on: + release: + types: [created] + push: + branches: + - "**" + tags: + - "**" +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - run: npm ci + - name: Build docs + run: npm run docs + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/build + + deploy: + if: ${{ startsWith(github.ref, 'refs/tags/') }} + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index eb62582..058d03e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +docs # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 4baef9f..b4af306 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,99 @@ -# JavaScript library for micro:bit connections in browsers via USB and Bluetooth +# micro:bit connection library -This project is a work in progress. We are extracting WebUSB and Web Bluetooth code from the [micro:bit Python Editor](https://github.com/microbit-foundation/python-editor-v3/) and other projects. +[This documentation is best viewed on the documentation site rather than GitHub](https://microbit-foundation.github.io/microbit-connection/). -It is intended to be used by other Micro:bit Educational Foundation projects that need to connect to a BBC micro:bit. +This is a JavaScript library for micro:bit connections in browsers via USB and Bluetooth. -The API is not stable and it's not yet recommended that third parties use this project unless they are happy to update usage as the API evolves. +This project is a work in progress. We are extracting WebUSB and Web Bluetooth code from the [micro:bit Python Editor](https://github.com/microbit-foundation/python-editor-v3/) and other projects. The API is not stable and it's not yet recommended that third parties use this project unless they are happy to update usage as the API evolves. + +[Demo page](https://microbit-connection.pages.dev/) for this library. [Alpha releases are now on NPM](https://www.npmjs.com/package/@microbit/microbit-connection). +[micro:bit CreateAI](https://github.com/microbit-foundation/ml-trainer/) is already using this library for WebUSB and Web Bluetooth. + [This Python Editor PR](https://github.com/microbit-foundation/python-editor-v3/pull/1190) tracks updating the micro:bit Python Editor to use this library. -[micro:bit CreateAI](https://github.com/microbit-foundation/ml-trainer/) is already using this library for WebUSB and Web Bluetooth. +## Usage + +### Flash a micro:bit + +Instantiate a WebUSB connection using {@link MicrobitWebUSBConnection} class and use it to connect to a micro:bit. + +```ts +import { MicrobitWebUSBConnection } from "@microbit/microbit-connection"; + +const usb = new MicrobitWebUSBConnection(); +const connectionStatus = await usb.connect(); + +console.log("Connection status: ", connectionStatus); +``` + +{@link ConnectionStatus | Connection status} is `"CONNECTED"` if connection succeeds. + +Flash a universal hex that supports both V1 and V2: + +```ts +import { createUniversalHexFlashDataSource } from "@microbit/microbit-connection"; + +await usb.flash(createUniversalHexFlashDataSource(universalHexString), { + partial: true, + progress: (percentage: number | undefined) => { + console.log(percentage); + }, +}); +``` + +This code will also work for non-universal hex files so is a good default for unknown hex files. + +Alternatively, you can create and flash a hex for a specific micro:bit version by providing a function that takes a {@link BoardVersion} and returns a hex. +This can reduce download size or help integrate with APIs that produce a hex for a particular device version. +This example uses the [@microbit/microbit-fs library](https://microbit-foundation.github.io/microbit-fs/) which can return a hex based on board id. + +```ts +import { MicropythonFsHex, microbitBoardId } from "@microbit/microbit-fs"; +import { BoardId } from "@microbit/microbit-connection"; + +const micropythonFs = new MicropythonFsHex([ + { hex: microPythonV1HexFile, boardId: microbitBoardId.V1 }, + { hex: microPythonV2HexFile, boardId: microbitBoardId.V2 }, +]); +// Add files to MicroPython file system here (omitted for simplicity) +// Flash the device +await usb.flash( + async (boardVersion) => { + const boardId = BoardId.forVersion(boardVersion).id; + return micropythonFs.getIntelHex(boardId); + }, + { + partial: true, + progress: (percentage: number | undefined) => { + console.log(percentage); + }, + }, +); +``` + +For more examples of using other methods in the {@link MicrobitWebUSBConnection} class, see our [demo code](https://github.com/microbit-foundation/microbit-connection/blob/main/src/demo.ts) for our [demo site](https://microbit-connection.pages.dev/). + +### Connect via Bluetooth + +By default, the micro:bit's Bluetooth service is not enabled. Visit our [Bluetooth tech site page](https://tech.microbit.org/bluetooth/) to download a hex file that would enable the bluetooth service. + +Instantiate a Bluetooth connection using {@link MicrobitWebBluetoothConnection} class and use it to connect to a micro:bit. + +```ts +import { MicrobitWebBluetoothConnection } from "@microbit/microbit-connection"; + +const bluetooth = new MicrobitWebBluetoothConnection(); +const connectionStatus = await bluetooth.connect(); + +console.log("Connection status: ", connectionStatus); +``` + +{@link ConnectionStatus | Connection status} is `"CONNECTED"` if connection succeeds. + +For more examples of using other methods in the {@link MicrobitWebBluetoothConnection} class, see our [demo code](https://github.com/microbit-foundation/microbit-connection/blob/main/src/demo.ts) for our [demo site](https://microbit-connection.pages.dev/). ## License @@ -28,7 +111,7 @@ $ npx license-checker --direct --summary --production Omit the flags as desired to obtain more detail. -## Code of Conduct +## Code of conduct Trust, partnership, simplicity and passion are our core values we live and breathe in our daily work life and within our projects. Our open-source diff --git a/lib/bluetooth.ts b/lib/bluetooth.ts index 1a98759..e0398f1 100644 --- a/lib/bluetooth.ts +++ b/lib/bluetooth.ts @@ -49,7 +49,7 @@ export class MicrobitWebBluetoothConnection private device: BluetoothDevice | undefined; private logging: Logging; - connection: BluetoothDeviceWrapper | undefined; + private connection: BluetoothDeviceWrapper | undefined; private availabilityListener = (e: Event) => { // TODO: is this called? is `value` correct? diff --git a/lib/index.ts b/lib/index.ts index c07bb62..115d080 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,9 +1,13 @@ import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js"; -import { MicrobitWebBluetoothConnection } from "./bluetooth.js"; +import { + MicrobitWebBluetoothConnection, + MicrobitWebBluetoothConnectionOptions, +} from "./bluetooth.js"; import { BoardId } from "./board-id.js"; import { ButtonEvent, ButtonEventType, ButtonState } from "./buttons.js"; import { AfterRequestDevice, + BackgroundErrorEvent, BeforeRequestDevice, BoardVersion, ConnectionStatus, @@ -15,19 +19,30 @@ import { FlashDataError, FlashDataSource, FlashEvent, + FlashOptions, SerialDataEvent, SerialErrorEvent, SerialResetEvent, } from "./device.js"; import { TypedEventTarget } from "./events.js"; import { createUniversalHexFlashDataSource } from "./hex-flash-data-source.js"; +import { LedMatrix } from "./led.js"; +import { Logging, LoggingEvent } from "./logging.js"; import { MagnetometerData, MagnetometerDataEvent } from "./magnetometer.js"; import { ServiceConnectionEventMap } from "./service-events.js"; -import { MicrobitRadioBridgeConnection } from "./usb-radio-bridge.js"; -import { MicrobitWebUSBConnection } from "./usb.js"; +import { UARTDataEvent } from "./uart.js"; +import { + MicrobitRadioBridgeConnection, + MicrobitRadioBridgeConnectionOptions, +} from "./usb-radio-bridge.js"; +import { + MicrobitWebUSBConnection, + MicrobitWebUSBConnectionOptions, +} from "./usb.js"; export { AfterRequestDevice, + BackgroundErrorEvent, BeforeRequestDevice, BoardId, ConnectionStatus, @@ -45,6 +60,7 @@ export { SerialResetEvent, ServiceConnectionEventMap, TypedEventTarget, + UARTDataEvent, }; export type { @@ -57,6 +73,13 @@ export type { DeviceConnection, DeviceErrorCode, FlashDataSource, + FlashOptions, + LedMatrix, + Logging, + LoggingEvent, MagnetometerData, MagnetometerDataEvent, + MicrobitRadioBridgeConnectionOptions, + MicrobitWebBluetoothConnectionOptions, + MicrobitWebUSBConnectionOptions, }; diff --git a/lib/logging.ts b/lib/logging.ts index c81d118..ab0c5f8 100644 --- a/lib/logging.ts +++ b/lib/logging.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -export interface Event { +export interface LoggingEvent { type: string; message?: string; value?: number; @@ -11,13 +11,13 @@ export interface Event { } export interface Logging { - event(event: Event): void; + event(event: LoggingEvent): void; error(message: string, e: unknown): void; log(e: any): void; } export class NullLogging implements Logging { - event(_event: Event): void { + event(_event: LoggingEvent): void { console.log(_event); } error(_m: string, _e: unknown): void { diff --git a/package-lock.json b/package-lock.json index b71307e..4f13900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@types/node": "^20.14.10", "jsdom": "^24.1.0", "prettier": "3.3.2", + "typedoc": "^0.27.6", "typescript": "^5.2.2", "vite": "^5.3.1", "vitest": "^2.0.0" @@ -404,6 +405,17 @@ "node": ">=12" } }, + "node_modules/@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "dev": true, + "dependencies": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -724,6 +736,32 @@ "win32" ] }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.1.tgz", + "integrity": "sha512-gSt2WhLNgEeLstcweQOSp+C+MhOpTsgdNXRqr3zP6M+BUBZ8Md9OU2BYwUYsALBxHza7hwaIWtFHjQ/aOOychw==", + "dev": true, + "dependencies": { + "@shikijs/types": "1.29.1", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.1.tgz", + "integrity": "sha512-aBqAuhYRp5vSir3Pc9+QPu9WESBOjUo03ao0IHLC4TyTioSsp/SkbAZSrIH4ghYYC1T1KTEpRSBa83bas4RnPA==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -736,6 +774,15 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", @@ -752,6 +799,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, "node_modules/@types/usb": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.4.tgz", @@ -862,6 +915,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -877,6 +936,21 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1290,6 +1364,15 @@ } } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loupe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", @@ -1299,6 +1382,12 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/magic-string": { "version": "0.30.10", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", @@ -1308,6 +1397,29 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1347,6 +1459,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1541,6 +1668,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -1756,6 +1892,28 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "peer": true }, + "node_modules/typedoc": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz", + "integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==", + "dev": true, + "dependencies": { + "@gerrit0/mini-shiki": "^1.24.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x" + } + }, "node_modules/typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -1769,6 +1927,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -2058,6 +2222,18 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } } } diff --git a/package.json b/package.json index 7006f69..d182b03 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "build:lib": "npm run build:esm && npm run build:cjs", "build:demo": "vite build --mode=demo", "build": "npm run build:lib && npm run build:demo", + "docs": "typedoc", "ci": "npm run build:lib && npm run test && npx prettier --check lib src", "test": "vitest", "preview": "vite preview" @@ -31,6 +32,7 @@ "@types/node": "^20.14.10", "jsdom": "^24.1.0", "prettier": "3.3.2", + "typedoc": "^0.27.6", "typescript": "^5.2.2", "vite": "^5.3.1", "vitest": "^2.0.0" diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..591265d --- /dev/null +++ b/typedoc.json @@ -0,0 +1,13 @@ +{ + "name": "micro:bit connection library", + "navigationLinks": { + "micro:bit tech site": "https://tech.microbit.org", + "GitHub": "https://github.com/microbit-foundation/microbit-connection" + }, + "entryPoints": ["./lib/index.ts"], + "out": "./docs/build", + "readme": "./README.md", + "headings": { + "readme": false + } +}