mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e30c249498 | |||
| 5288f76ded |
@@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Configurations
|
||||
|
||||
### API ###
|
||||
|
||||
## API token to use when looking up the published content
|
||||
# GITBOOK_API_URL=https://api.gitbook.com
|
||||
# GITBOOK_TOKEN=xxx
|
||||
|
||||
## User agent to use when making requests to the API
|
||||
# GITBOOK_USER_AGENT=GitBook/1.0.0
|
||||
# GITBOOK_USER_AGENT_COMMENT=something
|
||||
|
||||
### URL of the application ###
|
||||
# NEXT_PUBLIC_GITBOOK_APP_URL=https://app.gitbook.com
|
||||
|
||||
### Image resizing ###
|
||||
# GITBOOK_IMAGE_RESIZE_SIGNING_KEY=1234567890
|
||||
# GITBOOK_IMAGE_RESIZE_URL=https://mycompany.com/cdn-cgi/image/
|
||||
|
||||
### SEO ###
|
||||
# GITBOOK_BLOCK_SEARCH_INDEXATION=true
|
||||
|
||||
## Caching
|
||||
# GITBOOK_OUTPUT_CACHE=true
|
||||
|
||||
### Sentry ###
|
||||
# SENTRY_DSN=xxx
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"plugins": ["import"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": ["builtin", "external", ["internal", "parent", "sibling", "index"]],
|
||||
"newlines-between": "always",
|
||||
"distinctGroup": true,
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "@gitbook/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@next/next/no-img-element": ["off"]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Changes to the API data cache functions can invalidate all existing data cache
|
||||
# causing a massive amount of revalidation, impacting our API.
|
||||
packages/gitbook/src/lib/data/api.ts @SamyPesse
|
||||
+7
-44
@@ -1,8 +1,6 @@
|
||||
# Welcome to GitBook's contributing guide!
|
||||
|
||||
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
|
||||
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team.In this guide, you'll learn the different ways you can contribute.
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
@@ -47,48 +45,13 @@ Any contribution you make can be made to the code located in this repository. In
|
||||
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
|
||||
##### GitHub Codespaces:
|
||||
|
||||
- [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally.
|
||||
|
||||
#### 2. Create a working branch and start with your changes
|
||||
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
|
||||
|
||||
#### 3. Install dependencies and run the project locally
|
||||
|
||||
##### Prerequisites:
|
||||
- Node.js (Version: >= 22.3)
|
||||
- Use `nvm` for easy Node management
|
||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
|
||||
##### Setup steps:
|
||||
|
||||
1. Ensure you are using the project's version of Node:
|
||||
```bash
|
||||
nvm use
|
||||
```
|
||||
|
||||
2. Install dependencies using Bun:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
Additional development commands:
|
||||
- `bun format`: Format the code using Biome
|
||||
- `bun typecheck`: Run TypeScript type checking
|
||||
- `bun unit`: Run unit tests
|
||||
- `bun e2e`: Run end-to-end tests
|
||||
|
||||
#### 4. Preview your changes
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/gitbook.com/docs` after running the development server.
|
||||
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of. After creating the branch, you can start making changes!
|
||||
|
||||
### Commit your update
|
||||
|
||||
@@ -108,4 +71,4 @@ When you're finished with the changes, [create a pull request](https://docs.gith
|
||||
|
||||
### Your PR is merged
|
||||
|
||||
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
Congratulations 🎉Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
name: Gradual Deploy to Cloudflare
|
||||
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
|
||||
inputs:
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
middlewareVersionId:
|
||||
description: 'Middleware version ID to deploy'
|
||||
required: true
|
||||
serverVersionId:
|
||||
description: 'Server version ID to deploy'
|
||||
required: true
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||
@@ -0,0 +1,35 @@
|
||||
name: 'Setup Playwright'
|
||||
description: 'Install Playwright and dependencies'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
# Run npm ci and get Playwright version
|
||||
- name: 🏗 Prepare Playwright env
|
||||
shell: bash
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Cache browser binaries, cache key is based on Playwright version and OS
|
||||
- name: 🧰 Cache Playwright browser binaries
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: '~/.cache/ms-playwright'
|
||||
key: '${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}'
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
# Install browser binaries & OS dependencies if cache missed
|
||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
bun x playwright install --with-deps chromium
|
||||
|
||||
# Install only the OS dependencies if cache hit
|
||||
- name: 🏗 Install Playwright OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
bun x playwright install-deps
|
||||
@@ -1,138 +0,0 @@
|
||||
name: 'Deploy cloudflare'
|
||||
description: 'Deploy GitBook to Cloudflare'
|
||||
inputs:
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
apiToken:
|
||||
description: 'Cloudflare API token'
|
||||
required: true
|
||||
accountId:
|
||||
description: 'Cloudflare account ID'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||
required: true
|
||||
deploy:
|
||||
description: 'Deploy as main version for all traffic instead of uploading versions'
|
||||
required: true
|
||||
commitTag:
|
||||
description: 'Commit branch to associate with the deployment'
|
||||
required: true
|
||||
commitMessage:
|
||||
description: 'Commit message to associate with the deployment'
|
||||
required: true
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
shell: bash
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v2
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build worker
|
||||
run: bun run turbo build:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
|
||||
|
||||
- id: upload_server
|
||||
name: Upload server to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Extract server version worker ID
|
||||
shell: bash
|
||||
id: extract_server_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run updateWrangler scripts
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ./packages/gitbook/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
|
||||
- id: upload_middleware
|
||||
name: Upload middleware to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Extract middleware version worker ID
|
||||
shell: bash
|
||||
id: extract_middleware_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy server and middleware to Cloudflare
|
||||
if: ${{ inputs.deploy == 'true' }}
|
||||
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||
opItem: ${{ inputs.opItem }}
|
||||
environment: ${{ inputs.environment }}
|
||||
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
@@ -1,80 +0,0 @@
|
||||
name: 'Deploy vercel'
|
||||
description: 'Deploy GitBook to Vercel'
|
||||
inputs:
|
||||
vercelOrg:
|
||||
description: 'Vercel organization'
|
||||
required: true
|
||||
vercelProject:
|
||||
description: 'Vercel project'
|
||||
required: true
|
||||
vercelToken:
|
||||
description: 'Vercel token'
|
||||
required: true
|
||||
opItem:
|
||||
description: '1Password item to load secrets from'
|
||||
required: true
|
||||
opServiceAccount:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
required: true
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
shell: bash
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Pull Vercel Environment Information
|
||||
run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v2
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
|
||||
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
|
||||
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
|
||||
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
|
||||
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
|
||||
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
|
||||
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
|
||||
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
|
||||
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
|
||||
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
|
||||
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build Project Artifacts
|
||||
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
GITBOOK_RUNTIME: vercel
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: deploy
|
||||
shell: bash
|
||||
run: |
|
||||
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
|
||||
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
name: 'Setup Bun'
|
||||
description: 'Install Bun and cache dependencies'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
+139
-33
@@ -4,63 +4,169 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
|
||||
jobs:
|
||||
format:
|
||||
deploy:
|
||||
name: Deploy to Cloudflare Pages
|
||||
runs-on: ubuntu-latest
|
||||
name: Format
|
||||
timeout-minutes: 6
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
outputs:
|
||||
deployment_url: ${{ steps.cloudflare.outputs.url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun format:check
|
||||
test:
|
||||
- name: Cache Next.js build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
|
||||
- name: Sets env vars for production
|
||||
run: |
|
||||
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
|
||||
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
||||
if: startsWith(github.ref, 'refs/heads/main')
|
||||
- name: Sets env vars for preview
|
||||
run: |
|
||||
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
|
||||
if: 1 && !startsWith(github.ref, 'refs/heads/main')
|
||||
- name: Build Next.js with next-on-pages
|
||||
run: bun run build:cloudflare
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
- id: cloudflare
|
||||
name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: ./.vercel/output/static
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
workingDirectory: ./
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "ID: ${{ steps.cloudflare.outputs.id }}"
|
||||
echo "URL: ${{ steps.cloudflare.outputs.url }}"
|
||||
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
|
||||
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
|
||||
- name: Archive build output
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output
|
||||
path: .vercel/
|
||||
visual-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test
|
||||
timeout-minutes: 6
|
||||
name: Visual Testing
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
pagespeed-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run pagespeed tests
|
||||
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
name: Format
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun format --check .
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun lint --no-cache
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun unit
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Open Source)
|
||||
timeout-minutes: 6
|
||||
env:
|
||||
BUN_NPM_TOKEN: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun run build
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
name: Typecheck
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
name: Preview
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (preview)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy to Vercel
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
with:
|
||||
environment: preview
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-preview
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy to Cloudflare
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-cloudflare
|
||||
with:
|
||||
environment: preview
|
||||
deploy: ${{ github.ref == 'refs/heads/main' }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
opItem: op://gitbook-open/2c-preview
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Extract Worker ID
|
||||
id: extract-worker-id
|
||||
if: ${{ !steps.deploy.outputs.deployment-url }}
|
||||
run: |
|
||||
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
|
||||
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
|
||||
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
|
||||
comment-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
name: Comment Deployments (preview)
|
||||
if: always() && !startsWith(github.ref, 'refs/heads/main')
|
||||
needs:
|
||||
- deploy-v2-vercel
|
||||
- deploy-v2-cloudflare
|
||||
steps:
|
||||
- name: Find GitHub Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Summary of the deployments'
|
||||
|
||||
- name: Create or update GitHub comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
Summary of the deployments:
|
||||
|
||||
| Version | URL | Status |
|
||||
| --- | --- | --- |
|
||||
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
||||
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
|
||||
### Test content
|
||||
|
||||
| Site | `2v` | `2c` |
|
||||
| --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||
| E2E | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
edit-mode: replace
|
||||
visual-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'v2-vercel'
|
||||
visual-testing-v2-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'v2-cloudflare'
|
||||
visual-testing-customers-v2:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e-customers
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v2'
|
||||
visual-testing-customers-v2-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run Playwright tests
|
||||
run: bun e2e-customers
|
||||
env:
|
||||
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v2'
|
||||
browserless-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: Browserless Testing v2 (Vercel)
|
||||
needs: deploy-v2-vercel
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run tests
|
||||
run: cd ./packages/gitbook && bun e2e-browserless
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
# browserless-testing-v2-cloudflare:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: Browserless Testing v2 (Cloudflare)
|
||||
# needs: deploy-v2-cloudflare
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Setup Bun
|
||||
# uses: ./.github/composite/setup-bun
|
||||
# - name: Install dependencies
|
||||
# run: bun install --frozen-lockfile
|
||||
# env:
|
||||
# PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
# - name: Run tests
|
||||
# run: cd ./packages/gitbook && bun e2e-browserless
|
||||
# env:
|
||||
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
|
||||
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Production
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (production)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2v-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
with:
|
||||
environment: production
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-production
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (production)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2c-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-cloudflare
|
||||
with:
|
||||
environment: production
|
||||
deploy: true
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
opItem: op://gitbook-open/2c-production
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
commitTag: main
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (staging)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2v-staging
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
with:
|
||||
environment: staging
|
||||
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-staging
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (staging)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: 2c-staging
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-cloudflare
|
||||
with:
|
||||
environment: staging
|
||||
deploy: true
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
opItem: op://gitbook-open/2c-staging
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
commitTag: main
|
||||
commitMessage: ${{ github.sha }}
|
||||
- name: Outputs
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run release
|
||||
version: npm run changeset-version
|
||||
env:
|
||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
+28
-8
@@ -5,6 +5,16 @@ node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -14,15 +24,25 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# Env files
|
||||
.env.local
|
||||
|
||||
# TypeScript
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
.npmrc
|
||||
next-env.d.ts
|
||||
|
||||
# visual tests
|
||||
screenshots/
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.next
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
}
|
||||
Vendored
+1
-11
@@ -7,15 +7,5 @@
|
||||
["style \\=([^;]*);", "\"([^\"]*)\""],
|
||||
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
|
||||
],
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<h1 align="center">GitBook</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
|
||||
<a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
|
||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjUiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCA2NSA2NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI3LjM5NjQgMzMuNjc2NEMzMC41MjU1IDM1LjQ4MjQgMzIuMDkgMzYuMzg1NCAzMy44MDgzIDM2LjM4NjlDMzUuNTI2NSAzNi4zODg0IDM3LjA5MjYgMzUuNDg4MiA0MC4yMjQ5IDMzLjY4NzdMNjAuMTkxNCAyMi4yMTA0QzYxLjA5MjcgMjEuNjkyMiA2MS42NDg0IDIwLjczMTggNjEuNjQ4NCAxOS42OTIxQzYxLjY0ODQgMTguNjUyNCA2MS4wOTI3IDE3LjY5MiA2MC4xOTE0IDE3LjE3MzlMNDAuMjE3NyA1LjY5MjQ1QzM3LjA4ODggMy44OTM4NiAzNS41MjQzIDIuOTk0NTcgMzMuODA3OCAyLjk5NTI0QzMyLjA5MTIgMi45OTU5MSAzMC41Mjc1IDMuODk2NDIgMjcuNCA1LjY5NzQ0TDEwLjIyOTMgMTUuNTg1NUMxMC4xMDIgMTUuNjU4OCAxMC4wMzg0IDE1LjY5NTQgOS45NzkwOCAxNS43MzAxQzQuMTEzNzEgMTkuMTYzNyAwLjQ4OTg5MiAyNS40MzIzIDAuNDQxNDM4IDMyLjIyODZDMC40NDA5NDggMzIuMjk3MyAwLjQ0MDk0OCAzMi4zNzA4IDAuNDQwOTQ4IDMyLjUxNzZDMC40NDA5NDggMzIuNjY0MyAwLjQ0MDk0OCAzMi43Mzc2IDAuNDQxNDM3IDMyLjgwNjNDMC40ODk3ODUgMzkuNTk0OSA0LjEwNTUyIDQ1Ljg1NzcgOS45NjA0NCA0OS4yOTRDMTAuMDE5NiA0OS4zMjg3IDEwLjA4MzIgNDkuMzY1NCAxMC4yMTAyIDQ5LjQzODdMMjAuOTY1OSA1NS42NDg3QzI3LjIzMzIgNTkuMjY3MyAzMC4zNjY4IDYxLjA3NjYgMzMuODA4MSA2MS4wNzc3QzM3LjI0OTMgNjEuMDc4OSA0MC4zODQyIDU5LjI3MTcgNDYuNjUzOSA1NS42NTc0TDU4LjAwOCA0OS4xMTIxQzYxLjE0NzQgNDcuMzAyMyA2Mi43MTcxIDQ2LjM5NzQgNjMuNTc5IDQ0LjkwNTZDNjQuNDQwOSA0My40MTM5IDY0LjQ0MDkgNDEuNjAyIDY0LjQ0MDkgMzcuOTc4NFYzMC45NzgxQzY0LjQ0MDkgMjkuOTcyOCA2My44OTY1IDI5LjA0NjQgNjMuMDE4MiAyOC41NTczQzYyLjE2ODQgMjguMDgzOSA2MS4xMzI1IDI4LjA5MSA2MC4yODkxIDI4LjU3NThMMzcuMDA3NCA0MS45NTg4QzM1LjQ0NTQgNDIuODU2NyAzNC42NjQzIDQzLjMwNTYgMzMuODA3MyA0My4zMDU5QzMyLjk1MDIgNDMuMzA2MiAzMi4xNjg5IDQyLjg1NzcgMzAuNjA2MyA0MS45NjA3TDE0Ljg0ODcgMzIuOTE1NUMxNC4wNTk0IDMyLjQ2MjQgMTMuNjY0NyAzMi4yMzU5IDEzLjM0NzcgMzIuMTk1QzEyLjYyNSAzMi4xMDE3IDExLjkzMDEgMzIuNTA2NiAxMS42NTQ4IDMzLjE4MTNDMTEuNTM0MSAzMy40NzcyIDExLjUzNjUgMzMuOTMyMiAxMS41NDE0IDM0Ljg0MjRDMTEuNTQ1IDM1LjUxMjQgMTEuNTQ2OCAzNS44NDc0IDExLjYwOTQgMzYuMTU1NkMxMS43NDk3IDM2Ljg0NTYgMTIuMTEyNyAzNy40NzA1IDEyLjY0MjggMzcuOTM0MUMxMi44Nzk1IDM4LjE0MTEgMTMuMTY5NiAzOC4zMDg1IDEzLjc1IDM4LjY0MzVMMzAuNTk3NCA0OC4zNjcyQzMyLjE2NDEgNDkuMjcxNCAzMi45NDc0IDQ5LjcyMzUgMzMuODA3NSA0OS43MjM3QzM0LjY2NzcgNDkuNzIzOSAzNS40NTEzIDQ5LjI3MjMgMzcuMDE4NCA0OC4zNjg5TDU3LjY2ODQgMzYuNDY1NEM1OC4yMDM3IDM2LjE1NjkgNTguNDcxNCAzNi4wMDI2IDU4LjY3MjEgMzYuMTE4NUM1OC44NzI3IDM2LjIzNDUgNTguODcyNyAzNi41NDM0IDU4Ljg3MjcgMzcuMTYxM1Y0MC4zMzY1QzU4Ljg3MjcgNDEuMjQyNCA1OC44NzI3IDQxLjY5NTQgNTguNjU3MiA0Mi4wNjgzQzU4LjQ0MTggNDIuNDQxMyA1OC4wNDkzIDQyLjY2NzUgNTcuMjY0NCA0My4xMTk5TDQwLjIzMjIgNTIuOTM4QzM3LjA5NjYgNTQuNzQ1NCAzNS41Mjg4IDU1LjY0OTIgMzMuODA3OSA1NS42NDg0QzMyLjA4NjkgNTUuNjQ3NiAzMC41MTk5IDU0Ljc0MjQgMjcuMzg2IDUyLjkzMjFMMTEuNDUwOSA0My43MjdDMTEuNDAwMyA0My42OTc4IDExLjM3NSA0My42ODMyIDExLjM1MTQgNDMuNjY5NEM4LjAxMDIzIDQxLjcxNyA1Ljk0ODU5IDM4LjE0NTEgNS45MjkyNSAzNC4yNzU0QzUuOTI5MTIgMzQuMjQ4IDUuOTI5MTIgMzQuMjE4OCA1LjkyOTEyIDM0LjE2MDRWMzEuMjQ1OEM1LjkyOTEyIDI5LjEwOTUgNy4wNjY4OSAyNy4xMzQ5IDguOTE1MTMgMjYuMDYzNkMxMC41NDgzIDI1LjExNjkgMTIuNTYyOCAyNS4xMTUxIDE0LjE5NzcgMjYuMDU4N0wyNy4zOTY0IDMzLjY3NjRaIiBmaWxsPSIjRjJGN0Y3Ii8+Cjwvc3ZnPgo=&labelColor=5c5c5c&color=3F89A1&label=%20" alt="Documented on GitBook"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/Open_Source-❤️-FDA599?"/></a>
|
||||
<a href="/LICENSE"><img src="https://img.shields.io/badge/License-GNU_GPLv3-F4E28D"/></a>
|
||||
<a href="/.github/CONTRIBUTING.md"><img src="https://img.shields.io/github/contributors/gitbookIO/gitbook"/></a>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
|
||||
|
||||
<p align="center">This repository contains the open source code used to render GitBook's published content.</p>
|
||||
<p align="center">This repository contains the open-source code used to render GitBook's published content.</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
|
||||
@@ -35,10 +35,8 @@ To run a local version of this project, please follow these simple steps.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (Version: >= 22.3)
|
||||
- Use nvm for easy Node management
|
||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||
- Node.js (Version: >=18.x)
|
||||
- Bun
|
||||
|
||||
### Set up
|
||||
|
||||
@@ -48,56 +46,32 @@ To run a local version of this project, please follow these simple steps.
|
||||
git clone https://github.com/gitbookIO/gitbook.git
|
||||
```
|
||||
|
||||
2. Ensure you are using the project's version of `node`. Running `nvm use` will change your local version to the correct one.
|
||||
|
||||
3. Install the project's dependencies through Bun.
|
||||
2. Install the project's dependencies through Bun.
|
||||
|
||||
```
|
||||
bun install
|
||||
```
|
||||
|
||||
4. Start your local development server.
|
||||
3. Start your local development server.
|
||||
|
||||
```
|
||||
bun dev
|
||||
```
|
||||
|
||||
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
|
||||
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
|
||||
|
||||
examples:
|
||||
### Other development commands
|
||||
|
||||
- http://localhost:3000/url/gitbook.com/docs
|
||||
- http://localhost:3000/url/open-source.gitbook.io/midjourney
|
||||
|
||||
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
|
||||
- `bun format`: format the code
|
||||
- `bun lint`: lint the code
|
||||
|
||||
### CI and testing
|
||||
|
||||
All pull-requests will be tested against both visual and performances testing to prevent regressions.
|
||||
|
||||
## Fonts and Icons
|
||||
|
||||
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
|
||||
|
||||
```
|
||||
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
|
||||
```
|
||||
|
||||
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
|
||||
|
||||
If you are GitBook staff, you'll need our NPM token in your local environment.
|
||||
|
||||
```
|
||||
.env.local
|
||||
|
||||
BUN_NPM_TOKEN=xxx
|
||||
```
|
||||
|
||||
and then reinstall dependencies.
|
||||
|
||||
## Contributing
|
||||
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
GitBook's rendering engine is fully open-source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
|
||||
### Types of contributions
|
||||
|
||||
@@ -105,7 +79,7 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
||||
|
||||
#### Translations
|
||||
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
The GitBook UI is rendered using a set of translation files found in [`src/intl/translations`](/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
|
||||
#### Bugs
|
||||
|
||||
@@ -116,7 +90,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
||||
> [!WARNING]
|
||||
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
|
||||
>
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
@@ -132,30 +106,10 @@ On the con side, you become responsible for the reliability of your published si
|
||||
|
||||
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
||||
|
||||
If you plan to distribute the code, you must make the source code public to comply with the GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
|
||||
See `LICENSE` for more information.
|
||||
|
||||
## Badges
|
||||
|
||||
<p align="left">
|
||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
|
||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=F4E28D"></a>
|
||||
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=FDA599"></a>
|
||||
</p>
|
||||
|
||||
```md
|
||||
[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
|
||||
```
|
||||
|
||||
```html
|
||||
<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
|
||||
<img
|
||||
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
|
||||
/>
|
||||
</a>
|
||||
```
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
GitBook wouldn't be possible without these projects:
|
||||
@@ -165,12 +119,6 @@ GitBook wouldn't be possible without these projects:
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
|
||||
</a>
|
||||
|
||||
## Legacy GitBook (Deprecated)
|
||||
|
||||
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
|
||||
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"**/node_modules/**/*",
|
||||
"**/dist/**/*",
|
||||
"**/build/**/*",
|
||||
"**/public/**/*",
|
||||
"**/.next/**/*",
|
||||
"**/.open-next/**/*",
|
||||
"**/.turbo/**/*",
|
||||
"**/.vercel/**/*",
|
||||
"**/.cache/**/*",
|
||||
"**/.wrangler/**/*",
|
||||
"packages/embed/standalone/**/*",
|
||||
"packages/openapi-parser/src/fixtures/**/*",
|
||||
"packages/emoji-codepoints/index.ts",
|
||||
"packages/icons/src/data/*.json",
|
||||
"packages/gitbook/worker-configuration.d.ts",
|
||||
"**/*.css"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"noDelete": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noUselessFragments": "warn",
|
||||
"noBannedTypes": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedImports": "error",
|
||||
"noVoidElementsWithChildren": "warn",
|
||||
"useJsxKeyInIterable": "warn",
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnknownFunction": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noParameterAssign": "off",
|
||||
"useThrowOnlyError": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"allow": ["assert", "error", "warn"]
|
||||
}
|
||||
},
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "warn",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noPrototypeBuiltins": "warn",
|
||||
"noAssignInExpressions": "warn",
|
||||
"noArrayIndexKey": "warn"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "warn",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"useButtonType": "warn",
|
||||
"useIframeTitle": "warn",
|
||||
"useAltText": "warn",
|
||||
"noPositiveTabindex": "warn",
|
||||
"useFocusableInteractive": "warn",
|
||||
"useAriaPropsForRole": "warn",
|
||||
"useValidAnchor": "warn",
|
||||
"noLabelWithoutControl": "warn",
|
||||
"noNoninteractiveTabindex": "warn"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": ["class", "className", "style"],
|
||||
"functions": ["clsx", "tw"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"packages/gitbook/**/*",
|
||||
"packages/react-openapi/**/*",
|
||||
"packages/react-math/**/*",
|
||||
"packages/react-contentkit/**/*",
|
||||
"packages/icons/**/*"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/gitbook/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["React", "GitBookIntegrationEvent"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.css"],
|
||||
"javascript": {
|
||||
"globals": ["theme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
|
||||
"javascript": {
|
||||
"globals": ["Bun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"packages/cache-do/**/*",
|
||||
"packages/gitbook/cf-env.d.ts",
|
||||
"packages/gitbook/src/cloudflare-entrypoint.ts"
|
||||
],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"DurableObjectLocationHint",
|
||||
"DurableObjectNamespace",
|
||||
"DurableObjectStub",
|
||||
"ContinentCode",
|
||||
"Fetcher",
|
||||
"ExportedHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[install.scopes]
|
||||
"gitbook" = { token = "$BUN_NPM_TOKEN", url = "https://registry.npmjs.org" }
|
||||
@@ -0,0 +1,22 @@
|
||||
# Caching
|
||||
|
||||
## Revalidating the cache
|
||||
|
||||
Invalidate cache can be done at two levels using tags:
|
||||
|
||||
- Data fetching cache
|
||||
- Rendering cache
|
||||
|
||||
To invalidate and refetch the data cache, you can execute a POST request to `/~/gitbook/revalidate`:
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://gitbook/mycompany.com/~gitbook/revalidate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{"tags": ["space.id"]}'
|
||||
```
|
||||
|
||||
To invalidate the rendering cache, the implementation mainly depends on the infrastructure serving the content, GitBook outputs a `Cache-Tag` header on every requests. The value of the header is a comma separated list of tags.
|
||||
|
||||
## Purging the cache
|
||||
|
||||
Purging the cache, without revalidating, is done by passing `"purge": true` in the request body.
|
||||
@@ -0,0 +1,576 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationLocale,
|
||||
CustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import rison from 'rison';
|
||||
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
|
||||
interface Test {
|
||||
name: string;
|
||||
url: string;
|
||||
run?: (page: Page) => Promise<unknown>;
|
||||
fullPage?: boolean;
|
||||
screenshot?: false;
|
||||
}
|
||||
|
||||
interface TestsCase {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
tests: Array<Test>;
|
||||
}
|
||||
|
||||
const allLocales: CustomizationLocale[] = [
|
||||
CustomizationLocale.Fr,
|
||||
CustomizationLocale.Es,
|
||||
CustomizationLocale.Ja,
|
||||
CustomizationLocale.Zh,
|
||||
];
|
||||
|
||||
async function waitForCookiesDialog(page: Page) {
|
||||
const dialog = page.getByRole('dialog', { name: 'Cookies' });
|
||||
const accept = dialog.getByRole('button', { name: 'Accept' });
|
||||
const reject = dialog.getByRole('button', { name: 'Reject' });
|
||||
await expect(accept).toBeVisible();
|
||||
await expect(reject).toBeVisible();
|
||||
}
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'GitBook Site (Single Variant)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'No variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '?q=',
|
||||
},
|
||||
{
|
||||
name: 'Search Results',
|
||||
url: '?q=gitbook',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-results"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AI Search',
|
||||
url: '?q=What+is+GitBook%3F&ask=true',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
||||
},
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook Site (Multi Variants)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Variants dropdown',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
|
||||
await spaceDrowpdown.waitFor();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Default variant',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'RFC variant',
|
||||
url: 'v/rfcs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
baseUrl: 'https://docs.gitbook.com',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '?q=',
|
||||
},
|
||||
{
|
||||
name: 'Search Results',
|
||||
url: '?q=gitbook',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-results"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AI Search',
|
||||
url: '?q=What+is+GitBook%3F&ask=true',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-test="search-ask-answer"]');
|
||||
},
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Versioning',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Revision',
|
||||
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'PDF',
|
||||
url: '~gitbook/pdf?limit=10',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Content tests',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Text',
|
||||
url: 'text-page',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Long text',
|
||||
url: 'text-page/long-text',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Images',
|
||||
url: 'blocks/block-images',
|
||||
run: waitForCookiesDialog,
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Inline Images',
|
||||
url: 'blocks/inline-images',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Tabs',
|
||||
url: 'blocks/tabs',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Hints',
|
||||
url: 'blocks/hints',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Integration Blocks',
|
||||
url: 'blocks/integrations',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Tables',
|
||||
url: 'blocks/tables',
|
||||
run: waitForCookiesDialog,
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Expandables',
|
||||
url: 'blocks/expandables',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'API Blocks',
|
||||
url: 'blocks/api-blocks',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Headings',
|
||||
url: 'blocks/headings',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Marks',
|
||||
url: 'blocks/marks',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Emojis',
|
||||
url: 'blocks/emojis',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Links',
|
||||
url: 'blocks/links',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Lists',
|
||||
url: 'blocks/lists',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
url: 'blocks/code',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Cards',
|
||||
url: 'blocks/cards',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
url: 'blocks/math',
|
||||
},
|
||||
{
|
||||
name: 'Embeds',
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
url: 'blocks/annotations',
|
||||
run: async (page) => {
|
||||
await page.waitForSelector('[data-testid="annotation-button"]');
|
||||
await page.click('[data-testid="annotation-button"]');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Page options',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'With cover',
|
||||
url: 'page-options/page-with-cover',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With hero cover',
|
||||
url: 'page-options/page-with-hero-cover',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With cover and no TOC',
|
||||
url: 'page-options/page-with-cover-and-no-toc',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Customization',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Without header',
|
||||
url: getCustomizationURL({
|
||||
header: {
|
||||
preset: CustomizationHeaderPreset.None,
|
||||
links: [],
|
||||
},
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Share links',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Valid link',
|
||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Invalid link',
|
||||
url: 'invalid/',
|
||||
run: async (page) => {
|
||||
await expect(
|
||||
page.getByText('Authentication missing to access this content'),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Space',
|
||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-space/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'First',
|
||||
url: (() => {
|
||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `first?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Second',
|
||||
url: (() => {
|
||||
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `second?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Collection',
|
||||
baseUrl: `https://gitbook.gitbook.io/gbo-va-collection/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'Root',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Primary (Space A)',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spacea?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Space B',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spaceb?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Space C',
|
||||
url: (() => {
|
||||
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `v/spacec?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visitor Auth - Space (custom domain)',
|
||||
baseUrl: `https://test.gitbook.community/`,
|
||||
tests: [
|
||||
{
|
||||
name: 'Root',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'First',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `first?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Custom page',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `custom-page?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Inner page',
|
||||
url: (() => {
|
||||
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `custom-page/inner-page?jwt_token=${token}`;
|
||||
})(),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Languages',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: allLocales.map((locale) => ({
|
||||
name: locale,
|
||||
url: getCustomizationURL({
|
||||
internationalization: {
|
||||
locale,
|
||||
inherit: false,
|
||||
},
|
||||
}),
|
||||
run: async (page) => {
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
test.describe(testCase.name, () => {
|
||||
for (const testEntry of testCase.tests) {
|
||||
test(testEntry.name, async ({ page, baseURL }) => {
|
||||
const contentUrl = new URL(testEntry.url, testCase.baseUrl);
|
||||
const url = getContentTestURL(contentUrl.toString(), baseURL);
|
||||
await page.goto(url);
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page);
|
||||
}
|
||||
if (testEntry.screenshot !== false) {
|
||||
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
||||
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
fullPage: testEntry.fullPage ?? false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URL with customization settings.
|
||||
*/
|
||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
||||
const encoded = rison.encode_object(partial);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('customization', encoded);
|
||||
|
||||
return `?${searchParams.toString()}`;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
{
|
||||
output: 'standalone',
|
||||
|
||||
env: {
|
||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
},
|
||||
|
||||
webpack(config) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
|
||||
// Required for `swagger2openapi` to work:
|
||||
fs: false,
|
||||
path: false,
|
||||
http: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
// Cache all static assets for 1 year
|
||||
{
|
||||
source: '/~gitbook/static/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'public, max-age=31536000, immutable',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
},
|
||||
{
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
transpileClientSDK: false,
|
||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
tunnelRoute: '/~gitbook/monitoring',
|
||||
// Don't hide source maps from generated client bundles
|
||||
hideSourceMaps: false,
|
||||
disableLogger: true,
|
||||
automaticVercelMonitors: false,
|
||||
},
|
||||
);
|
||||
+87
-45
@@ -1,54 +1,96 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"turbo": "^2.5.8",
|
||||
"vercel": "^39.4.2"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.24.2"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev --concurrency 20",
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "biome check --write ./",
|
||||
"format:check": "biome check --diagnostic-level=error ./",
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && bun run format && bun update",
|
||||
"release": "turbo run release && bun run publish-all-packages",
|
||||
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build:cloudflare": "next-on-pages",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier ./ --ignore-unknown --write",
|
||||
"format:check": "prettier ./ --ignore-unknown --list-different",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test {src,packages}/**/*.test.ts",
|
||||
"e2e": "playwright test",
|
||||
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.143.2",
|
||||
"assert-never": "^1.4.0",
|
||||
"bidc": "^0.0.2",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tsdown": "^0.15.6",
|
||||
"typescript": "^5.5.3",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
}
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.48.0",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.0.7",
|
||||
"classnames": "^2.5.1",
|
||||
"content-security-policy-merger": "^1.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"next": "^14.1.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.15.4",
|
||||
"object-hash": "^3.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.0",
|
||||
"parse-cache-control": "^1.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
"url-join": "^5.0.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@cloudflare/next-on-pages": "^1.11.3",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/katex": "^0.16.5",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/rison": "^0.0.9",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.0.3",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dist/
|
||||
@@ -1,29 +0,0 @@
|
||||
# @gitbook/browser-types
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
- Updated dependencies [6142d6b]
|
||||
- @gitbook/icons@0.3.3
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cbc71a5: First version of the public package for typing script integrations.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 854c448: Custom assistants followup
|
||||
- Updated dependencies [25e2b40]
|
||||
- @gitbook/icons@0.3.0
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@gitbook/browser-types`
|
||||
|
||||
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/browser-types",
|
||||
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
export type GitBookIntegrationEvent = 'load' | 'unload';
|
||||
|
||||
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
|
||||
|
||||
export type GitBookIntegrationTool = AIToolDefinition & {
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing the tool.
|
||||
*/
|
||||
confirmation?: {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when the tool is executed.
|
||||
* The input is provided by the AI assistant following the input schema of the tool.
|
||||
*/
|
||||
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
|
||||
};
|
||||
|
||||
export type GitBookAssistant = {
|
||||
/**
|
||||
* Name of the assistant displayed in the UI.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Icon of the assistant displayed in the UI.
|
||||
* Any FontAwesome icon name is supported.
|
||||
* @example 'sparkle'
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* Callback when the assistant is opened.
|
||||
*/
|
||||
open: (query?: string) => void;
|
||||
|
||||
/**
|
||||
* Whether to display the triggers for this assistant in the UI.
|
||||
* @default true
|
||||
*/
|
||||
ui?: boolean;
|
||||
};
|
||||
|
||||
export type GitBookGlobal = {
|
||||
/**
|
||||
* Register an event listener.
|
||||
*/
|
||||
addEventListener: (
|
||||
type: GitBookIntegrationEvent,
|
||||
func: GitBookIntegrationEventCallback
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Remove an event listener.
|
||||
*/
|
||||
removeEventListener: (
|
||||
type: GitBookIntegrationEvent,
|
||||
func: GitBookIntegrationEventCallback
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Register a custom tool to be exposed to the AI assistant.
|
||||
*/
|
||||
registerTool: (tool: GitBookIntegrationTool) => void;
|
||||
|
||||
/**
|
||||
* Register a custom assistant to be available on the site.
|
||||
*/
|
||||
registerAssistant: (assistant: GitBookAssistant) => () => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Global `window.GitBook` object accessible by integrations.
|
||||
*/
|
||||
GitBook?: GitBookGlobal;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
dist/
|
||||
@@ -1,37 +0,0 @@
|
||||
# @gitbook/cache-tags
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 77397ca: Fix version of @gitbook/api referenced in package.json
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- f32bf1f: Export function `getCacheTagForURL` to easily get the cache tag for a URL.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 05ffd0e: Initial version of the package
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@gitbook/cache-tags`
|
||||
|
||||
Utility to generate cache tags for GitBook Open.
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/cache-tags",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.3.3",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import type { ComputedContentSource } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
/**
|
||||
* Get a stringified cache tag for a given object.
|
||||
*/
|
||||
export function getCacheTag(
|
||||
spec: /**
|
||||
* All data related to a user
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'user';
|
||||
user: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a space
|
||||
*/
|
||||
| {
|
||||
tag: 'space';
|
||||
space: string;
|
||||
}
|
||||
/**
|
||||
* All data related to an integration.
|
||||
*/
|
||||
| {
|
||||
tag: 'integration';
|
||||
integration: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a change request
|
||||
*/
|
||||
| {
|
||||
tag: 'change-request';
|
||||
space: string;
|
||||
changeRequest: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a revision
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'revision';
|
||||
space: string;
|
||||
revision: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a document
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'document';
|
||||
space: string;
|
||||
document: string;
|
||||
}
|
||||
/**
|
||||
* Immutable data related to a computed document
|
||||
* @deprecated - in v2, no tag as this is an immutable data
|
||||
*/
|
||||
| {
|
||||
tag: 'computed-document';
|
||||
space: string;
|
||||
sourceType: string;
|
||||
}
|
||||
/**
|
||||
* All data related to the URL of a content
|
||||
*/
|
||||
| {
|
||||
tag: 'url';
|
||||
hostname: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a site
|
||||
*/
|
||||
| {
|
||||
tag: 'site';
|
||||
site: string;
|
||||
}
|
||||
/**
|
||||
* All data related to an OpenAPI spec
|
||||
*/
|
||||
| {
|
||||
tag: 'openapi';
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a translation
|
||||
*/
|
||||
| {
|
||||
tag: 'translation';
|
||||
organization: string;
|
||||
translation: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
return `user:${spec.user}`;
|
||||
case 'url':
|
||||
return `url:${spec.hostname}`;
|
||||
case 'space':
|
||||
return `space:${spec.space}`;
|
||||
case 'change-request':
|
||||
return `space:${spec.space}:change-request:${spec.changeRequest}`;
|
||||
case 'revision':
|
||||
return `space:${spec.space}:revision:${spec.revision}`;
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
case 'translation':
|
||||
return `organization:${spec.organization}:translation:${spec.translation}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags for a computed content source.
|
||||
*/
|
||||
export function getComputedContentSourceCacheTags(
|
||||
inContext: {
|
||||
spaceId: string;
|
||||
organizationId: string;
|
||||
},
|
||||
source: ComputedContentSource
|
||||
) {
|
||||
const tags: string[] = [];
|
||||
|
||||
if (!('dependencies' in source)) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
// We add the dependencies as tags, to ensure that the computed content is invalidated
|
||||
// when the dependencies are updated.
|
||||
const dependencies = Object.values(source.dependencies ?? {});
|
||||
if (dependencies.length > 0) {
|
||||
dependencies.forEach((dependency) => {
|
||||
switch (dependency.ref.kind) {
|
||||
case 'space':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: dependency.ref.space,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'openapi':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: inContext.organizationId,
|
||||
openAPISpec: dependency.ref.spec,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'translation':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'translation',
|
||||
organization: inContext.organizationId,
|
||||
translation: dependency.ref.translation,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lagging behind the API version
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Push a dummy tag, as the v1 is only using the first tag
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'computed-document',
|
||||
space: inContext.spaceId,
|
||||
sourceType: source.type,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1]!;
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
dist/
|
||||
@@ -1,55 +0,0 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 17dd382: Add `original` background color step
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 193d591: Fix return type for `colorContrast`
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c3f6b8c: Update chroma ratio per step
|
||||
- 5e975ab: Fix code highlighting for HTTP
|
||||
- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Reduce chroma of first color scale step
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4f0a772: Override tint lightness if supplied color is out of bounds
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 445baaa: Initial release
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@gitbook/colors`
|
||||
|
||||
A set of default colors and transformation functions used throughout the GitBook Open and app.
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/colors",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.4.2",
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Default primary color throughout the GitBook ecosystem.
|
||||
*/
|
||||
export const DEFAULT_PRIMARY_COLOR = '#346DDB';
|
||||
|
||||
/**
|
||||
* The darkest color that exists in GitBook, used as the relative minimum of every generated color scale.
|
||||
*/
|
||||
export const DARK_BASE = '#1D1D1D';
|
||||
|
||||
/**
|
||||
* The lightest color that exists in GitBook, used as the relative maximum of every generated color scale.
|
||||
*/
|
||||
export const LIGHT_BASE = '#FFFFFF';
|
||||
|
||||
/**
|
||||
* Used as the basis of all UI elements that are not colored by the primary color. Neutral gray by default, overridden by site customization.
|
||||
*/
|
||||
export const DEFAULT_TINT_COLOR = '#787878';
|
||||
|
||||
/**
|
||||
* Used for informational messages and neutral alerts.
|
||||
*/
|
||||
export const DEFAULT_HINT_INFO_COLOR = '#787878';
|
||||
|
||||
/**
|
||||
* Used for showing important information or non-critical warnings.
|
||||
*/
|
||||
export const DEFAULT_HINT_WARNING_COLOR = '#FE9A00';
|
||||
|
||||
/**
|
||||
* Used for destructive actions or raising attention to critical information.
|
||||
*/
|
||||
export const DEFAULT_HINT_DANGER_COLOR = '#FB2C36';
|
||||
|
||||
/**
|
||||
* Used for showing positive actions or achievements.
|
||||
*/
|
||||
export const DEFAULT_HINT_SUCCESS_COLOR = '#00C950';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './colors';
|
||||
export * from './transformations';
|
||||
@@ -1,445 +0,0 @@
|
||||
import { DARK_BASE, DEFAULT_TINT_COLOR, LIGHT_BASE } from './colors';
|
||||
|
||||
type ColorShades = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
type RGBColor = [number, number, number];
|
||||
type OKLABColor = { L: number; A: number; B: number };
|
||||
type OKLCHColor = { L: number; C: number; H: number };
|
||||
|
||||
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
|
||||
|
||||
export enum ColorCategory {
|
||||
backgrounds = 'backgrounds',
|
||||
components = 'components',
|
||||
borders = 'borders',
|
||||
accents = 'accents',
|
||||
text = 'text',
|
||||
}
|
||||
|
||||
type ColorSubScale = {
|
||||
[key: string]: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main color scale object.
|
||||
*
|
||||
* Each `ColorCategory` can be in/excluded in Tailwind's utility classes generation.
|
||||
* Each subitem maps a semantic name within that category to a step in the scale.
|
||||
*/
|
||||
export const scale: Record<ColorCategory, ColorSubScale> = {
|
||||
[ColorCategory.backgrounds]: {
|
||||
/** Base background */
|
||||
base: 1,
|
||||
/** Accent background */
|
||||
subtle: 2,
|
||||
},
|
||||
[ColorCategory.components]: {
|
||||
/** Component background */
|
||||
DEFAULT: 3,
|
||||
/** Component hover background */
|
||||
hover: 4,
|
||||
/** Component active background */
|
||||
active: 5,
|
||||
},
|
||||
[ColorCategory.borders]: {
|
||||
/** Subtle borders, separators */
|
||||
subtle: 6,
|
||||
/** Element border, focus rings */
|
||||
DEFAULT: 7,
|
||||
/** Element hover border */
|
||||
hover: 8,
|
||||
},
|
||||
[ColorCategory.accents]: {
|
||||
/** Solid backgrounds */
|
||||
solid: 9,
|
||||
/** Hovered solid backgrounds */
|
||||
'solid-hover': 10,
|
||||
/** Original color */
|
||||
original: 'original',
|
||||
},
|
||||
[ColorCategory.text]: {
|
||||
/** Very low-contrast text
|
||||
* Caution: this contrast does not meet accessiblity guidelines.
|
||||
* Always check if you need to include a mitigating contrast-more style for users who need it. */
|
||||
subtle: 9,
|
||||
/** Low-contrast text */
|
||||
DEFAULT: 11,
|
||||
/** High-contrast text */
|
||||
strong: 12,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The mix of foreground and background for every step in a colour scale.
|
||||
* 0: 100% of the background color's luminosity, white in light mode
|
||||
* 1: 100% of the foreground color's luminosity, black in light mode
|
||||
*/
|
||||
export const colorMixMapping = {
|
||||
// bgs |components |borders |solid |text
|
||||
light: [0, 0.02, 0.03, 0.05, 0.07, 0.1, 0.15, 0.2, 0.5, 0.55, 0.6, 1],
|
||||
dark: [0, 0.03, 0.08, 0.1, 0.13, 0.15, 0.2, 0.25, 0.5, 0.55, 0.75, 1],
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a hex color to an RGB color.
|
||||
*/
|
||||
export function hexToRgb(hex: string): string {
|
||||
const [r, g, b] = hexToRgbArray(hex);
|
||||
// Return the RGB values separated by spaces
|
||||
return `${r} ${g} ${b}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hex color to a RGBA color.
|
||||
*/
|
||||
export function hexToRgba(hex: string, alpha: number): string {
|
||||
const [r, g, b] = hexToRgbArray(hex);
|
||||
// Return the RGBA values separated by spaces
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Tailwind-compatible shades from a single color
|
||||
* @param {string} hex The hex code to generate shades from
|
||||
* @param {boolean} halfShades Generate additional shades, e.g. at 150
|
||||
* @returns {{[key: number]: string}}
|
||||
*/
|
||||
export function shadesOfColor(hex: string, halfShades = false) {
|
||||
const baseColor = hex;
|
||||
|
||||
const shades = [
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
400,
|
||||
500,
|
||||
600,
|
||||
700,
|
||||
800,
|
||||
900,
|
||||
...(halfShades ? [150, 250, 350, 450, 550, 650, 750, 850] : []),
|
||||
].sort();
|
||||
|
||||
const result: ColorShades = {};
|
||||
|
||||
for (const shade of shades) {
|
||||
const key = shade.toString();
|
||||
|
||||
if (shade === 500) {
|
||||
result[key] = hex;
|
||||
continue;
|
||||
}
|
||||
|
||||
let shadeIndex = shade;
|
||||
const isDarkShade = shadeIndex > 500;
|
||||
if (isDarkShade) {
|
||||
shadeIndex -= 500;
|
||||
}
|
||||
|
||||
const percentage = shadeIndex / 500;
|
||||
const startColor = isDarkShade ? DARK_BASE : baseColor;
|
||||
const endColor = isDarkShade ? baseColor : LIGHT_BASE;
|
||||
|
||||
result[key] = getColor(percentage, hexToRgbArray(startColor), hexToRgbArray(endColor));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type ColorScaleOptions = {
|
||||
/** If set to `true`, inverts the scale (so 1 is black instead of white) and uses `colorMixMapping.dark` with different mix ratios per step. */
|
||||
darkMode?: boolean;
|
||||
|
||||
/** Define a custom background color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
|
||||
background?: string;
|
||||
|
||||
/** Define a custom foreground color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
|
||||
foreground?: string;
|
||||
|
||||
mix?: {
|
||||
/** If set to a hex code, this color will be additionally mixed into the generated scale according to `mix.ratio`. */
|
||||
color: string;
|
||||
|
||||
/** Define a custom mix ratio to mix the `mix` color with. If left undefined, the default ratio will be used. */
|
||||
ratio: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a [Radix-like](https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale) colour scale based of a hex colour.
|
||||
* @param {string} hex The hex code to generate shades from
|
||||
* @param {object} options
|
||||
*/
|
||||
export function colorScale(
|
||||
hex: string,
|
||||
{
|
||||
darkMode = false,
|
||||
background = darkMode ? DARK_BASE : LIGHT_BASE,
|
||||
foreground = darkMode ? LIGHT_BASE : DARK_BASE,
|
||||
mix,
|
||||
}: ColorScaleOptions = {}
|
||||
) {
|
||||
const baseColor = rgbToOklch(hexToRgbArray(hex));
|
||||
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
||||
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
||||
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
||||
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
||||
|
||||
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
||||
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
||||
baseColor.L = mixColor.L * mix.ratio + baseColor.L * (1 - mix.ratio);
|
||||
baseColor.C = mixColor.C * mix.ratio + baseColor.C * (1 - mix.ratio);
|
||||
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
||||
}
|
||||
|
||||
if (
|
||||
(darkMode && baseColor.L < backgroundColor.L) ||
|
||||
(!darkMode && baseColor.L > backgroundColor.L)
|
||||
) {
|
||||
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
|
||||
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
|
||||
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
|
||||
backgroundColor.L = baseColor.L;
|
||||
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
|
||||
const amplifier = 1;
|
||||
mapping = mapping.map((step, index) =>
|
||||
index < 9 ? step + step * amplifier * difference : step
|
||||
);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < mapping.length; index++) {
|
||||
const step = mapping[index]!;
|
||||
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||
|
||||
if (
|
||||
index === 8 &&
|
||||
!mix &&
|
||||
(darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
|
||||
) {
|
||||
// Original colour is close enough to target, so let's use the original colour as step 9.
|
||||
result.push(hex);
|
||||
continue;
|
||||
}
|
||||
|
||||
const chromaRatio = (() => {
|
||||
switch (index) {
|
||||
// Step 9 and 10 have max chroma, meaning they are fully saturated.
|
||||
case 8:
|
||||
case 9:
|
||||
return 1;
|
||||
// Step 11 and 12 have a reduced chroma
|
||||
case 10:
|
||||
return 0.4;
|
||||
case 11:
|
||||
return 0.1;
|
||||
default:
|
||||
return index * 0.05;
|
||||
}
|
||||
})();
|
||||
|
||||
const shade = {
|
||||
L: targetL, // Blend lightness
|
||||
C: baseColor.C * chromaRatio,
|
||||
H: baseColor.H, // Maintain the hue from the base color
|
||||
};
|
||||
|
||||
const newHex = rgbArrayToHex(oklchToRgb(shade));
|
||||
|
||||
result.push(newHex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hex color to an RGB color set.
|
||||
*/
|
||||
export function hexToRgbArray(hex: string): RGBColor {
|
||||
const originalHex = hex;
|
||||
|
||||
let value = hex.replace('#', '');
|
||||
if (hex.length === 3) value = value + value;
|
||||
|
||||
const r = value.substring(0, 2);
|
||||
const g = value.substring(2, 4);
|
||||
const b = value.substring(4, 6);
|
||||
|
||||
const rgb = [r, g, b].map((channel) => {
|
||||
try {
|
||||
const channelInt = Number.parseInt(channel, 16);
|
||||
if (channelInt < 0 || channelInt > 255) throw new Error();
|
||||
return channelInt;
|
||||
} catch {
|
||||
throw new Error(`Invalid hex color provided: ${originalHex}`);
|
||||
}
|
||||
});
|
||||
|
||||
return rgb as RGBColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a RGB color set to a hex color.
|
||||
*/
|
||||
export function rgbArrayToHex(rgb: RGBColor): string {
|
||||
return `#${rgb
|
||||
.map((channel) => {
|
||||
const component = channel.toString(16);
|
||||
if (component.length === 1) return `0${component}`;
|
||||
return component;
|
||||
})
|
||||
.join('')}`;
|
||||
}
|
||||
|
||||
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||
const rgb = end.map((channel, index) => {
|
||||
return Math.round(channel + percentage * (start[index]! - channel));
|
||||
});
|
||||
|
||||
return rgbArrayToHex(rgb as RGBColor);
|
||||
}
|
||||
|
||||
// Utility constants and helper functions
|
||||
export function rgbToLinear(rgb: RGBColor): [number, number, number] {
|
||||
return rgb.map((v) => {
|
||||
const scaled = v / 255;
|
||||
return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
|
||||
}) as [number, number, number];
|
||||
}
|
||||
|
||||
export function linearToRgb(linear: [number, number, number]): RGBColor {
|
||||
return linear.map((v) => {
|
||||
const scaled = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
||||
return Math.round(Math.max(0, Math.min(1, scaled)) * 255);
|
||||
}) as RGBColor;
|
||||
}
|
||||
|
||||
export function rgbToOklab(rgb: RGBColor): OKLABColor {
|
||||
const [r, g, b] = rgbToLinear(rgb);
|
||||
|
||||
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
||||
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
||||
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
||||
|
||||
const lRoot = Math.cbrt(l);
|
||||
const mRoot = Math.cbrt(m);
|
||||
const sRoot = Math.cbrt(s);
|
||||
|
||||
return {
|
||||
L: 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot,
|
||||
A: 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot,
|
||||
B: 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot,
|
||||
};
|
||||
}
|
||||
|
||||
export function oklabToRgb(oklab: OKLABColor): RGBColor {
|
||||
const { L, A, B } = oklab;
|
||||
|
||||
const lRoot = L + 0.3963377774 * A + 0.2158037573 * B;
|
||||
const mRoot = L - 0.1055613458 * A - 0.0638541728 * B;
|
||||
const sRoot = L - 0.0894841775 * A - 1.291485548 * B;
|
||||
|
||||
const l = lRoot ** 3;
|
||||
const m = mRoot ** 3;
|
||||
const s = sRoot ** 3;
|
||||
|
||||
const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
||||
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
||||
const b = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
||||
|
||||
return linearToRgb([r, g, b]);
|
||||
}
|
||||
|
||||
export function oklabToOklch(oklab: OKLABColor): OKLCHColor {
|
||||
const { L, A, B } = oklab;
|
||||
const C = Math.sqrt(A ** 2 + B ** 2);
|
||||
const H = (Math.atan2(B, A) * 180) / Math.PI;
|
||||
return { L, C, H: H < 0 ? H + 360 : H };
|
||||
}
|
||||
|
||||
export function oklchToOklab(oklch: OKLCHColor): OKLABColor {
|
||||
const { L, C, H } = oklch;
|
||||
const rad = (H * Math.PI) / 180;
|
||||
return {
|
||||
L,
|
||||
A: C * Math.cos(rad),
|
||||
B: C * Math.sin(rad),
|
||||
};
|
||||
}
|
||||
|
||||
export function rgbToOklch(rgb: RGBColor): OKLCHColor {
|
||||
return oklabToOklch(rgbToOklab(rgb));
|
||||
}
|
||||
|
||||
export function oklchToRgb(oklch: OKLCHColor): RGBColor {
|
||||
return oklabToRgb(oklchToOklab(oklch));
|
||||
}
|
||||
|
||||
export function rgbToXyz(rgb: RGBColor): [number, number, number] {
|
||||
const [r, g, b] = rgbToLinear(rgb);
|
||||
return [
|
||||
(r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
|
||||
(r * 0.2126729 + g * 0.7151522 + b * 0.072175) * 100,
|
||||
(r * 0.0193339 + g * 0.119192 + b * 0.9503041) * 100,
|
||||
];
|
||||
}
|
||||
|
||||
export function xyzToLab65(xyz: [number, number, number]): {
|
||||
L: number;
|
||||
A: number;
|
||||
B: number;
|
||||
} {
|
||||
const [x, y, z] = xyz.map((v, i) => {
|
||||
const scaled = v / D65[i]!;
|
||||
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
|
||||
});
|
||||
|
||||
return {
|
||||
L: 116 * y! - 16,
|
||||
A: 500 * (x! - y!),
|
||||
B: 200 * (y! - z!),
|
||||
};
|
||||
}
|
||||
|
||||
export function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
|
||||
return xyzToLab65(rgbToXyz(rgb));
|
||||
}
|
||||
|
||||
/*
|
||||
Delta Phi Star perceptual lightness contrast by Andrew Somers:
|
||||
https://github.com/Myndex/deltaphistar
|
||||
*/
|
||||
export const PHI = 0.5 + Math.sqrt(1.25);
|
||||
|
||||
export function dpsContrast(a: RGBColor, b: RGBColor) {
|
||||
const dps = Math.abs(rgbTolab65(a).L ** PHI - rgbTolab65(b).L ** PHI);
|
||||
const contrast = dps ** (1 / PHI) * Math.SQRT2 - 40;
|
||||
return contrast < 7.5 ? 0 : contrast;
|
||||
}
|
||||
|
||||
export function colorContrast(
|
||||
background: string,
|
||||
foreground: string[] = [LIGHT_BASE, DARK_BASE]
|
||||
): string {
|
||||
const bg = hexToRgbArray(background);
|
||||
|
||||
const best: { color?: RGBColor; contrast: number } = {
|
||||
color: undefined,
|
||||
contrast: 0,
|
||||
};
|
||||
for (const color of foreground) {
|
||||
const c = hexToRgbArray(color);
|
||||
|
||||
const contrast = dpsContrast(c, bg);
|
||||
if (contrast > best.contrast) {
|
||||
best.color = c;
|
||||
best.contrast = contrast;
|
||||
}
|
||||
}
|
||||
|
||||
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
dist/
|
||||
standalone/
|
||||
@@ -1,41 +0,0 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
- Updated dependencies [6142d6b]
|
||||
- @gitbook/icons@0.3.3
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7508674: Fix bundling of packages
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6f368b5: Fix embed assistant window width on small screens
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 81a6bd7: Improve API to control the GitBook embed
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8927e8f: Initial version of the embed SDK.
|
||||
- Updated dependencies [25e2b40]
|
||||
- @gitbook/icons@0.3.0
|
||||
@@ -1,49 +0,0 @@
|
||||
# `@gitbook/embed`
|
||||
|
||||
Embed the GitBook Docs Assistant in your product or website.
|
||||
|
||||
# Usage
|
||||
|
||||
## As a script from your docs site
|
||||
|
||||
All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
|
||||
|
||||
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
|
||||
|
||||
You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname.
|
||||
|
||||
```html
|
||||
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
|
||||
<script>
|
||||
window.GitBook('show');
|
||||
</script>
|
||||
```
|
||||
|
||||
## As a package from NPM
|
||||
|
||||
Install the package: `npm install @gitbook/embed` and import it in your web application:
|
||||
|
||||
```tsx
|
||||
import { createGitBook } from '@gitbook/embed';
|
||||
|
||||
const gitbook = createGitBook({
|
||||
siteURL: 'https://docs.company.com'
|
||||
});
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = gitbook.getFrameURL();
|
||||
|
||||
const frame = gitbook.createFrame(iframe);
|
||||
```
|
||||
|
||||
## As React components
|
||||
|
||||
After installing the NPM package, you can import prebuilt React components:
|
||||
|
||||
```tsx
|
||||
import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
|
||||
|
||||
<GitBookProvider siteURL="https://docs.company.com">
|
||||
<GitBookAssistantFrame />
|
||||
</GitBookProvider>
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/embed",
|
||||
"description": "Embeddable components for GitBook",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"default": "./dist/react/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.1.4",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
"bidc": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"react": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"clean": "rm -rf ./dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { type GitBookFrameClient, createGitBookFrame } from './createGitBookFrame';
|
||||
|
||||
export type CreateGitBookOptions = {
|
||||
/**
|
||||
* URL of the GitBook site to embed.
|
||||
*/
|
||||
siteURL: string;
|
||||
};
|
||||
|
||||
export type GetFrameURLOptions = {
|
||||
/**
|
||||
* Authentication to use for the frame.
|
||||
*/
|
||||
visitor?: {
|
||||
/**
|
||||
* Signed JWT token for Adaptive Content or Visitor Authentication to use.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* Unsigned claims to pass to the frame.
|
||||
* You can use these claims in dynamic expressions using `visitor.claims.unsigned.<claim-name>`.
|
||||
*/
|
||||
unsignedClaims?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GitBookClient = {
|
||||
/**
|
||||
* Get the URL for a GitBook frame.
|
||||
*/
|
||||
getFrameURL: (options: GetFrameURLOptions) => string;
|
||||
/**
|
||||
* Create a new GitBook frame.
|
||||
*/
|
||||
createFrame: (iframe: HTMLIFrameElement) => GitBookFrameClient;
|
||||
};
|
||||
|
||||
export function createGitBook(options: CreateGitBookOptions) {
|
||||
const client: GitBookClient = {
|
||||
getFrameURL: (frameOptions) => {
|
||||
const url = new URL(options.siteURL);
|
||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
|
||||
|
||||
if (frameOptions.visitor?.token) {
|
||||
url.searchParams.set('token', frameOptions.visitor.token);
|
||||
}
|
||||
|
||||
if (frameOptions.visitor?.unsignedClaims) {
|
||||
Object.entries(frameOptions.visitor.unsignedClaims).forEach(([key, value]) => {
|
||||
url.searchParams.set(`visitor.${key}`, String(value));
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
},
|
||||
createFrame: (iframe) => createGitBookFrame(iframe),
|
||||
};
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { createChannel } from 'bidc';
|
||||
import type {
|
||||
FrameToParentMessage,
|
||||
GitBookEmbeddableConfiguration,
|
||||
ParentToFrameMessage,
|
||||
} from './protocol';
|
||||
|
||||
export type GitBookFrameClient = {
|
||||
/**
|
||||
* Navigate to a page by its path.
|
||||
*/
|
||||
navigateToPage: (path: string) => void;
|
||||
|
||||
/**
|
||||
* Navigate to the assistant.
|
||||
*/
|
||||
navigateToAssistant: () => void;
|
||||
|
||||
/**
|
||||
* Post a message to the chat.
|
||||
*/
|
||||
postUserMessage: (message: string) => void;
|
||||
|
||||
/**
|
||||
* Clear the chat.
|
||||
*/
|
||||
clearChat: () => void;
|
||||
|
||||
/**
|
||||
* Set the placeholder settings.
|
||||
*/
|
||||
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
|
||||
|
||||
/**
|
||||
* Register an event listener.
|
||||
*/
|
||||
on: (event: string, listener: (...args: any[]) => void) => () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a client to communicate with the GitBook Assistant frame.
|
||||
*/
|
||||
export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClient {
|
||||
if (!iframe.contentWindow) {
|
||||
throw new Error('Iframe must have a content window');
|
||||
}
|
||||
const channel = createChannel(iframe.contentWindow);
|
||||
|
||||
channel.receive((message: FrameToParentMessage) => {
|
||||
console.log('[gitbook:embed] received message', message);
|
||||
if (message.type === 'close') {
|
||||
const listeners = events.get('close') || [];
|
||||
if (listeners) {
|
||||
listeners.forEach((listener) => listener());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sendToFrame = (message: ParentToFrameMessage) => {
|
||||
console.log('[gitbook:embed] send message', message);
|
||||
channel.send(message);
|
||||
};
|
||||
|
||||
const events = new Map<string, Array<(...args: any[]) => void>>();
|
||||
|
||||
const configuration: GitBookEmbeddableConfiguration = {
|
||||
buttons: [],
|
||||
welcomeMessage: '',
|
||||
suggestions: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
return {
|
||||
navigateToPage: (pagePath) => {
|
||||
sendToFrame({ type: 'navigateToPage', pagePath });
|
||||
},
|
||||
navigateToAssistant: () => {
|
||||
sendToFrame({ type: 'navigateToAssistant' });
|
||||
},
|
||||
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
|
||||
configure: (settings) => {
|
||||
Object.assign(configuration, settings);
|
||||
sendToFrame({ type: 'configure', settings: configuration });
|
||||
},
|
||||
clearChat: () => sendToFrame({ type: 'clearChat' }),
|
||||
on: (event, listener) => {
|
||||
const listeners = events.get(event) || [];
|
||||
listeners.push(listener);
|
||||
events.set(event, listeners);
|
||||
return () => {
|
||||
events.set(
|
||||
event,
|
||||
listeners.filter((l) => l !== listener)
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './createGitBook';
|
||||
export * from './createGitBookFrame';
|
||||
export * from './protocol';
|
||||
@@ -1,90 +0,0 @@
|
||||
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
/**
|
||||
* Custom tool definition to be passed to the AI assistant.
|
||||
*/
|
||||
export type GitBookToolDefinition = AIToolDefinition & {
|
||||
/**
|
||||
* Confirmation action to be displayed to the user before executing the tool.
|
||||
*/
|
||||
confirmation?: {
|
||||
icon?: IconName;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when the tool is executed.
|
||||
* The input is provided by the AI assistant following the input schema of the tool.
|
||||
*/
|
||||
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom button definition to be passed to the embeddable GitBook.
|
||||
*/
|
||||
export type GitBookEmbeddableButtonDefinition = {
|
||||
/**
|
||||
* Icon to be displayed in the button.
|
||||
*/
|
||||
icon: IconName;
|
||||
|
||||
/**
|
||||
* Label to be displayed in the button.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Callback when the button is clicked.
|
||||
*/
|
||||
onClick: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Overall configuration for the layout of the embeddable GitBook.
|
||||
*/
|
||||
export type GitBookEmbeddableConfiguration = {
|
||||
/**
|
||||
* Buttons to be displayed in the header of the embeddable GitBook.
|
||||
*/
|
||||
buttons: GitBookEmbeddableButtonDefinition[];
|
||||
|
||||
/** Message to be displayed in the welcome page. */
|
||||
welcomeMessage: string;
|
||||
|
||||
/** Suggestions of questions to be displayed in the welcome page. */
|
||||
suggestions: string[];
|
||||
|
||||
/** Tools to be provided to the assistant. */
|
||||
tools: GitBookToolDefinition[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Messages sent from the parent to the frame.
|
||||
*/
|
||||
export type ParentToFrameMessage =
|
||||
| {
|
||||
type: 'postUserMessage';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'clearChat';
|
||||
}
|
||||
| {
|
||||
type: 'configure';
|
||||
settings: GitBookEmbeddableConfiguration;
|
||||
}
|
||||
| {
|
||||
type: 'navigateToPage';
|
||||
pagePath: string;
|
||||
}
|
||||
| {
|
||||
type: 'navigateToAssistant';
|
||||
};
|
||||
|
||||
/**
|
||||
* Messages sent from the frame to the parent.
|
||||
*/
|
||||
export type FrameToParentMessage = {
|
||||
type: 'close';
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './client';
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type {
|
||||
GetFrameURLOptions,
|
||||
GitBookEmbeddableConfiguration,
|
||||
GitBookFrameClient,
|
||||
} from '../client';
|
||||
import { useGitBook } from './GitBookProvider';
|
||||
|
||||
export type GitBookFrameProps = {
|
||||
className?: string;
|
||||
} & GetFrameURLOptions &
|
||||
GitBookEmbeddableConfiguration;
|
||||
|
||||
/**
|
||||
* Render a frame with the GitBook Assistant in it.
|
||||
*/
|
||||
export function GitBookFrame(props: GitBookFrameProps) {
|
||||
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
||||
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
const gitbook = useGitBook();
|
||||
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
|
||||
|
||||
const frameURL = useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (frameRef.current) {
|
||||
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
||||
}
|
||||
}, [gitbook]);
|
||||
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
buttons,
|
||||
welcomeMessage,
|
||||
suggestions,
|
||||
tools,
|
||||
});
|
||||
}, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
title="GitBook"
|
||||
ref={frameRef}
|
||||
src={frameURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { type CreateGitBookOptions, createGitBook } from '../client';
|
||||
import { GitBookContext } from './context';
|
||||
|
||||
/**
|
||||
* Provider for the GitBook client.
|
||||
*/
|
||||
export function GitBookProvider(props: React.PropsWithChildren<CreateGitBookOptions>) {
|
||||
const { siteURL, children } = props;
|
||||
|
||||
const options = React.useMemo(
|
||||
() => ({
|
||||
siteURL,
|
||||
}),
|
||||
[siteURL]
|
||||
);
|
||||
|
||||
const client = React.useMemo(() => createGitBook(options), [options]);
|
||||
|
||||
return <GitBookContext.Provider value={client}>{children}</GitBookContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the GitBook client.
|
||||
*/
|
||||
export function useGitBook() {
|
||||
const context = React.useContext(GitBookContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('This component must be used within a <GitBookProvider />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { GitBookClient } from '../client';
|
||||
|
||||
export const GitBookContext = React.createContext<GitBookClient | null>(null);
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './GitBookProvider';
|
||||
export * from './GitBookFrame';
|
||||
@@ -1,178 +0,0 @@
|
||||
import './style.css';
|
||||
|
||||
import {
|
||||
type CreateGitBookOptions,
|
||||
type GetFrameURLOptions,
|
||||
type GitBookClient,
|
||||
type GitBookEmbeddableConfiguration,
|
||||
type GitBookFrameClient,
|
||||
createGitBook,
|
||||
} from '../client';
|
||||
|
||||
export type GitBook = () => void;
|
||||
|
||||
type StandaloneCalls =
|
||||
// Initialize the widget
|
||||
| ['init', CreateGitBookOptions, GetFrameURLOptions]
|
||||
// Unload the widget
|
||||
| ['unload']
|
||||
// Show the widget
|
||||
| ['show']
|
||||
// Hide the widget
|
||||
| ['hide']
|
||||
// Open the window
|
||||
| ['open']
|
||||
// Close the window
|
||||
| ['close']
|
||||
// Toggle the window
|
||||
| ['toggle']
|
||||
// Post a user message
|
||||
| ['postUserMessage', string]
|
||||
// Clear the chat
|
||||
| ['clearChat']
|
||||
// Configure the embed
|
||||
| ['configure', Partial<GitBookEmbeddableConfiguration>]
|
||||
// Navigate to a page
|
||||
| ['navigateToPage', string]
|
||||
// Navigate to the assistant
|
||||
| ['navigateToAssistant'];
|
||||
|
||||
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
|
||||
q?: StandaloneCalls[];
|
||||
};
|
||||
|
||||
const widgetButton = document.createElement('button');
|
||||
widgetButton.id = 'gitbook-widget-button';
|
||||
widgetButton.addEventListener('click', () => {
|
||||
GitBook('toggle');
|
||||
});
|
||||
widgetButton.innerHTML = `
|
||||
<span id="gitbook-widget-button-icon"></span>
|
||||
<span id="gitbook-widget-button-label">Ask</span>
|
||||
`;
|
||||
|
||||
const widgetWindow = document.createElement('div');
|
||||
widgetWindow.id = 'gitbook-widget-window';
|
||||
widgetWindow.classList.add('hidden');
|
||||
|
||||
document.body.appendChild(widgetButton);
|
||||
document.body.appendChild(widgetWindow);
|
||||
|
||||
let widgetIframe: HTMLIFrameElement | undefined;
|
||||
let _client: GitBookClient | undefined;
|
||||
let _frame: GitBookFrameClient | undefined;
|
||||
let frameOptions: GetFrameURLOptions | undefined;
|
||||
let frameConfiguration: GitBookEmbeddableConfiguration = {
|
||||
buttons: [],
|
||||
welcomeMessage: '',
|
||||
suggestions: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
throw new Error(
|
||||
'GitBook client not initialized. Call GitBook("init", { siteURL: "..." }) first.'
|
||||
);
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
function getIframe() {
|
||||
if (!widgetIframe || !_frame) {
|
||||
const client = getClient();
|
||||
|
||||
widgetIframe?.remove();
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
...frameOptions,
|
||||
});
|
||||
widgetWindow.appendChild(widgetIframe);
|
||||
|
||||
_frame = client.createFrame(widgetIframe);
|
||||
}
|
||||
return { iframe: widgetIframe, frame: _frame };
|
||||
}
|
||||
|
||||
const GitBook = (...args: StandaloneCalls) => {
|
||||
switch (args[0]) {
|
||||
case 'init':
|
||||
if (_client) {
|
||||
throw new Error(
|
||||
'GitBook client already initialized. Call GitBook("unload") first.'
|
||||
);
|
||||
}
|
||||
_client = createGitBook(args[1]);
|
||||
frameOptions = args[2];
|
||||
break;
|
||||
case 'unload':
|
||||
_client = undefined;
|
||||
_frame = undefined;
|
||||
widgetIframe?.remove();
|
||||
widgetWindow.classList.add('hidden');
|
||||
break;
|
||||
case 'show':
|
||||
widgetButton.classList.remove('hidden');
|
||||
break;
|
||||
case 'hide':
|
||||
widgetButton.classList.add('hidden');
|
||||
break;
|
||||
case 'open':
|
||||
widgetWindow.classList.remove('hidden');
|
||||
widgetButton.classList.add('open');
|
||||
getIframe();
|
||||
break;
|
||||
case 'toggle':
|
||||
widgetWindow.classList.toggle('hidden');
|
||||
widgetButton.classList.toggle('open');
|
||||
getIframe();
|
||||
break;
|
||||
case 'close':
|
||||
widgetWindow.classList.add('hidden');
|
||||
widgetButton.classList.remove('open');
|
||||
break;
|
||||
case 'postUserMessage':
|
||||
getIframe().frame.postUserMessage(args[1]);
|
||||
break;
|
||||
case 'configure':
|
||||
frameConfiguration = {
|
||||
...frameConfiguration,
|
||||
...args[1],
|
||||
};
|
||||
getIframe().frame.configure({
|
||||
...frameConfiguration,
|
||||
buttons: [
|
||||
...frameConfiguration.buttons,
|
||||
|
||||
// Always include a close button
|
||||
{
|
||||
icon: 'close',
|
||||
label: 'Close',
|
||||
onClick: () => {
|
||||
GitBook('close');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case 'clearChat':
|
||||
getIframe().frame.clearChat();
|
||||
break;
|
||||
case 'navigateToPage':
|
||||
getIframe().frame.navigateToPage(args[1]);
|
||||
break;
|
||||
case 'navigateToAssistant':
|
||||
getIframe().frame.navigateToAssistant();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error - GitBook is not defined in the global scope
|
||||
const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
|
||||
|
||||
// @ts-expect-error - GitBook is not defined in the global scope
|
||||
window.GitBook = GitBook;
|
||||
precalls.forEach((call) => GitBook(...call));
|
||||
|
||||
GitBook('configure', {});
|
||||
@@ -1,171 +0,0 @@
|
||||
:root {
|
||||
--gitbook-widget-top: 1rem;
|
||||
--gitbook-widget-bottom: 1rem;
|
||||
--gitbook-widget-right: 1rem;
|
||||
--gitbook-widget-left: 1rem;
|
||||
|
||||
--gitbook-widget-button-height: 46px;
|
||||
|
||||
--gitbook-widget-radius: .5rem;
|
||||
--gitbook-widget-text-size: 1rem;
|
||||
--gitbook-widget-text-color: #656973;
|
||||
--gitbook-widget-border-color: #e5e5e5;
|
||||
|
||||
--gitbook-widget-background-translucent: rgba(255, 255, 255, 0.9);
|
||||
--gitbook-widget-background-translucent-hover: rgba(250, 250, 250, 0.9);
|
||||
--gitbook-widget-background-solid: #FFFFFF;
|
||||
--gitbook-widget-background-solid-hover: #FBFBFB;
|
||||
|
||||
--gitbook-widget-icon-size: 1.25rem;
|
||||
|
||||
--gitbook-widget-window-width: 28rem; /* 448px */
|
||||
--gitbook-widget-window-height: 40rem; /* 640px */
|
||||
--gitbook-widget-window-spacing: .5rem; /* Spacing between the button and the window */
|
||||
--gitbook-widget-window-bottom: calc(var(--gitbook-widget-bottom) + var(--gitbook-widget-button-height) + var(--gitbook-widget-window-spacing));
|
||||
|
||||
--gitbook-widget-transition-duration-fast: 0.2s;
|
||||
--gitbook-widget-transition-duration-slow: 0.5s;
|
||||
--gitbook-widget-easing: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
#gitbook-widget-button {
|
||||
position: fixed;
|
||||
bottom: var(--gitbook-widget-bottom);
|
||||
right: var(--gitbook-widget-right);
|
||||
height: var(--gitbook-widget-button-height);
|
||||
z-index: 9999;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
gap: .5rem;
|
||||
padding: .75rem 1rem;
|
||||
|
||||
border-radius: 100px;
|
||||
border: 1px solid var(--gitbook-widget-border-color);
|
||||
background-color: var(--gitbook-widget-background-translucent);
|
||||
backdrop-filter: blur(16px);
|
||||
|
||||
font-size: var(--gitbook-widget-text-size);
|
||||
color: var(--gitbook-widget-text-color);
|
||||
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
|
||||
transition: all var(--gitbook-widget-transition-duration-fast) var(--gitbook-widget-easing);
|
||||
|
||||
cursor:pointer;
|
||||
animation: gitbook-widget-present var(--gitbook-widget-transition-duration-slow) var(--gitbook-widget-easing-bounce);
|
||||
}
|
||||
|
||||
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||
background-color: var(--gitbook-widget-background-translucent-hover);
|
||||
}
|
||||
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||
}
|
||||
#gitbook-widget-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
#gitbook-widget-button {
|
||||
background-color: var(--gitbook-widget-background-solid);
|
||||
}
|
||||
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
|
||||
background-color: var(--gitbook-widget-background-solid-hover);
|
||||
}
|
||||
}
|
||||
|
||||
#gitbook-widget-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#gitbook-widget-button.open {
|
||||
padding: .75rem;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Button: Icon */
|
||||
#gitbook-widget-button-icon {
|
||||
width: var(--gitbook-widget-icon-size);
|
||||
height: var(--gitbook-widget-icon-size);
|
||||
mask-image: url("https://static-2v.gitbook.com/~gitbook/static/icons/svgs/custom-icons/gitbook-assistant.svg?v=2");
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
#gitbook-widget-button.open #gitbook-widget-button-icon {
|
||||
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
|
||||
}
|
||||
|
||||
/* Button: Label */
|
||||
#gitbook-widget-button.open #gitbook-widget-button-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Window */
|
||||
#gitbook-widget-window {
|
||||
position: fixed;
|
||||
bottom: var(--gitbook-widget-window-bottom);
|
||||
right: var(--gitbook-widget-right);
|
||||
z-index: 9998;
|
||||
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
|
||||
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
||||
background-color: var(--gitbook-widget-background-solid);
|
||||
border: 1px solid var(--gitbook-widget-border-color);
|
||||
border-radius: var(--gitbook-widget-radius);
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
overflow: hidden;
|
||||
transition-property: transform, opacity, display;
|
||||
transition-duration: var(--gitbook-widget-transition-duration-slow);
|
||||
transition-timing-function: var(--gitbook-widget-easing-bounce);
|
||||
transform-origin: bottom right;
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
#gitbook-widget-window {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
body:has(#gitbook-widget-button.hidden) #gitbook-widget-window {
|
||||
bottom: var(--gitbook-widget-bottom);
|
||||
}
|
||||
|
||||
#gitbook-widget-window.hidden {
|
||||
transition-property: transform, opacity, display;
|
||||
transition-duration: var(--gitbook-widget-transition-duration-fast);
|
||||
transition-timing-function: var(--gitbook-widget-easing);
|
||||
transition-behavior: allow-discrete;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
#gitbook-widget-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@keyframes gitbook-widget-present {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: 'src/index.ts',
|
||||
outDir: 'dist',
|
||||
},
|
||||
{
|
||||
entry: 'src/react/index.ts',
|
||||
outDir: 'dist/react',
|
||||
unbundle: true,
|
||||
},
|
||||
]);
|
||||
@@ -1 +1 @@
|
||||
dist/
|
||||
index.ts
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# @gitbook/emoji-codepoints
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
@@ -1,5 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
|
||||
interface EmojiData {
|
||||
@@ -17,11 +16,11 @@ Object.entries(emojis).forEach(([key, value]) => {
|
||||
if (emoji && key !== emoji) {
|
||||
output[key] = emoji;
|
||||
} else if (!emoji) {
|
||||
console.log('No emoji for', key);
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist/index.ts'),
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`
|
||||
'index.ts',
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"exports": "./dist/index.ts",
|
||||
"sideEffects": false,
|
||||
"exports": "./index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"emoji-assets": "^9.0.0"
|
||||
"emoji-assets": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./build.ts",
|
||||
"clean": "rm -rf ./dist"
|
||||
"postinstall": "bun ./build.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dist
|
||||
@@ -1,47 +0,0 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a629900: Add dev script for @gitbook/expr
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6da3655: Fix exports in gitbook/expr package.json
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3548fa6: Fix eval-estree-expr named import
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e1ff17e: Fix bundling of gitbook/expr package
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ff1e3b: Add support for every/some array methods
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- ada195d: Publish gitbook/expr package to help evaluate user defined expressions.
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@gitbook/expr`
|
||||
|
||||
Safely evaluate & parse user-defined GitBook expressions.
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.2.3",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"assert-never": "catalog:",
|
||||
"escodegen": "^2.1.0",
|
||||
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escodegen": "^0.0.10",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"bun-types": "catalog:",
|
||||
"tsdown": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"clean": "rm -rf ./dist",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -1,889 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { ExpressionRuntime } from '../runtime';
|
||||
import {
|
||||
SymbolArray,
|
||||
SymbolBoolean,
|
||||
SymbolNumber,
|
||||
SymbolObject,
|
||||
SymbolString,
|
||||
SymbolType,
|
||||
SymbolsTable,
|
||||
} from '../symbols';
|
||||
import {
|
||||
type AutocompleteSuggestions,
|
||||
type AutocompleteSymbolSuggestion,
|
||||
SUPPORTED_BINARY_OPERATORS,
|
||||
SUPPORTED_CONDITIONAL_OPERATORS,
|
||||
SUPPORTED_LOGICAL_OPERATORS,
|
||||
} from '../types';
|
||||
|
||||
describe('autocomplete', () => {
|
||||
const runtime = new ExpressionRuntime();
|
||||
const visitorClaimsHelloArraySymbol = SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
});
|
||||
const symbols = {
|
||||
visitor: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: visitorClaimsHelloArraySymbol,
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
};
|
||||
const context = new SymbolsTable(symbols);
|
||||
const SCENARIOS: Array<{
|
||||
expressionWithCursor: string;
|
||||
expectedSuggestions: AutocompleteSuggestions;
|
||||
}> = [
|
||||
{
|
||||
expressionWithCursor: 'visit<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor',
|
||||
parentRef: undefined,
|
||||
childrenRefs: ['visitor.claims'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor.claims',
|
||||
parentRef: 'visitor',
|
||||
childrenRefs: [
|
||||
'visitor.claims.key',
|
||||
'visitor.claims.flags',
|
||||
'visitor.claims.hello',
|
||||
'visitor.claims.role',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolString({ name: 'key' }),
|
||||
ref: 'visitor.claims.key',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor.claims.flags',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1',
|
||||
'visitor.claims.flags.FLAG2',
|
||||
'visitor.claims.flags.FLAG3',
|
||||
'visitor.claims.flags.FLAG4',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
ref: 'visitor.claims.hello',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
ref: 'visitor.claims.role',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.role.length',
|
||||
'visitor.claims.role.at',
|
||||
'visitor.claims.role.endsWith',
|
||||
'visitor.claims.role.includes',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.ke<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolString({ name: 'key' }),
|
||||
ref: 'visitor.claims.key',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.h<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
ref: 'visitor.claims.hello',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.hello.<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolNumber({
|
||||
name: 'length',
|
||||
description: `The length data property of an Array instance represents the number of elements in that array.
|
||||
The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
|
||||
}),
|
||||
ref: 'visitor.claims.hello.length',
|
||||
parentRef: 'visitor.claims.hello',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
...visitorClaimsHelloArraySymbol.methods.map<AutocompleteSymbolSuggestion>(
|
||||
(method) => ({
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: method,
|
||||
ref: `visitor.claims.hello.${method.name}`,
|
||||
parentRef: 'visitor.claims.hello',
|
||||
childrenRefs: [],
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.f<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor.claims.flags',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1',
|
||||
'visitor.claims.flags.FLAG2',
|
||||
'visitor.claims.flags.FLAG3',
|
||||
'visitor.claims.flags.FLAG4',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.fl<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor.claims.flags',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1',
|
||||
'visitor.claims.flags.FLAG2',
|
||||
'visitor.claims.flags.FLAG3',
|
||||
'visitor.claims.flags.FLAG4',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG1' }),
|
||||
ref: 'visitor.claims.flags.FLAG1',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG2' }),
|
||||
ref: 'visitor.claims.flags.FLAG2',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG3' }),
|
||||
ref: 'visitor.claims.flags.FLAG3',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG4' }),
|
||||
ref: 'visitor.claims.flags.FLAG4',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FL<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG1' }),
|
||||
ref: 'visitor.claims.flags.FLAG1',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG2' }),
|
||||
ref: 'visitor.claims.flags.FLAG2',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG3' }),
|
||||
ref: 'visitor.claims.flags.FLAG3',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolBoolean({ name: 'FLAG4' }),
|
||||
ref: 'visitor.claims.flags.FLAG4',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.key <cur>',
|
||||
expectedSuggestions: [...SUPPORTED_BINARY_OPERATORS].map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 <cur>',
|
||||
expectedSuggestions: [
|
||||
...[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS].filter((op) =>
|
||||
['==', '!=', '===', '!==', '&&', '||'].includes(op.operator)
|
||||
),
|
||||
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||
].map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 =<cur>',
|
||||
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
|
||||
['==', '==='].includes(op.operator)
|
||||
).map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 ==<cur>',
|
||||
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
|
||||
(op) => op.operator === '==='
|
||||
).map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 !<cur>',
|
||||
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter((op) =>
|
||||
['!=', '!=='].includes(op.operator)
|
||||
).map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 !=<cur>',
|
||||
expectedSuggestions: SUPPORTED_BINARY_OPERATORS.filter(
|
||||
(op) => op.operator === '!=='
|
||||
).map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == <cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||
},
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == t<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == tr<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == f<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == fa<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == non<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == false<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == <cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||
},
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||
},
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'reader' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == ad<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == admin<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == "ad<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == "admin<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'admin' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == "admin"<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == edit<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == "edit<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == editor<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.String, data: 'editor' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.role == "editor"<cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.hello == <cur>',
|
||||
expectedSuggestions: [],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.hello[1] == <cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: {
|
||||
kind: 'in-array',
|
||||
srcSymbol: visitorClaimsHelloArraySymbol,
|
||||
matchedLiteralString: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.hello[1] == "test<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: {
|
||||
kind: 'in-array',
|
||||
srcSymbol: visitorClaimsHelloArraySymbol,
|
||||
matchedLiteralString: '"test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 == true <cur>',
|
||||
expectedSuggestions: [
|
||||
...SUPPORTED_LOGICAL_OPERATORS.filter((op) => ['&&', '||'].includes(op.operator)),
|
||||
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||
].map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})),
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 !== true && v<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor',
|
||||
parentRef: undefined,
|
||||
childrenRefs: ['visitor.claims'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? <cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor',
|
||||
parentRef: undefined,
|
||||
childrenRefs: ['visitor.claims'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visit<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
role: SymbolString({
|
||||
name: 'role',
|
||||
enum: ['admin', 'editor', 'reader'],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor',
|
||||
parentRef: undefined,
|
||||
childrenRefs: ['visitor.claims'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
expressionWithCursor: 'visitor.claims.flags.FLAG1 ? visitor.claims.fl<cur>',
|
||||
expectedSuggestions: [
|
||||
{
|
||||
type: 'symbol',
|
||||
symbol: {
|
||||
definition: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolBoolean({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolBoolean({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolBoolean({ name: 'FLAG3' }),
|
||||
FLAG4: SymbolBoolean({ name: 'FLAG4' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
ref: 'visitor.claims.flags',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1',
|
||||
'visitor.claims.flags.FLAG2',
|
||||
'visitor.claims.flags.FLAG3',
|
||||
'visitor.claims.flags.FLAG4',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
it.each(SCENARIOS)(
|
||||
'should provide matching suggestion for expression with cursor: $expressionWithCursor',
|
||||
({ expressionWithCursor, expectedSuggestions }) => {
|
||||
const { expression, cursorOffset } = extractCursorPosition(
|
||||
expressionWithCursor,
|
||||
'<cur>'
|
||||
);
|
||||
const { suggestions } = runtime.autocomplete(expression, cursorOffset, context);
|
||||
expect(suggestions).toStrictEqual(expectedSuggestions);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function extractCursorPosition(
|
||||
expressionWithCursor: string,
|
||||
cursorPlaceholder: string
|
||||
): { expression: string; cursorOffset: number } {
|
||||
const cursorOffset = expressionWithCursor.indexOf(cursorPlaceholder);
|
||||
if (cursorOffset === -1) {
|
||||
throw new Error(
|
||||
`Cursor position (${cursorPlaceholder}) not found in the expression string.`
|
||||
);
|
||||
}
|
||||
|
||||
const expression = expressionWithCursor.replace(cursorPlaceholder, '');
|
||||
return { expression, cursorOffset };
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { inferDefaultInputValuesFromObjectJSONSchema } from '../input-values';
|
||||
|
||||
describe('inferDefaultInputValuesFromObjectJSONSchema', () => {
|
||||
it('should infer properly the default input value based on the JSON schema of an object', () => {
|
||||
const defaultInputValues = inferDefaultInputValuesFromObjectJSONSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
FLAG1: { type: 'string' },
|
||||
FLAG2: { type: 'string' },
|
||||
FLAG3: { type: 'string' },
|
||||
},
|
||||
},
|
||||
isAlphaUser: {
|
||||
type: 'boolean',
|
||||
},
|
||||
hello: {
|
||||
type: 'string',
|
||||
enum: ['enumValue1', 'enumValue2', 'enumValue3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(defaultInputValues).toMatchObject({
|
||||
claims: {
|
||||
key: 'default',
|
||||
flags: {
|
||||
FLAG1: 'default',
|
||||
FLAG2: 'default',
|
||||
FLAG3: 'default',
|
||||
},
|
||||
isAlphaUser: true,
|
||||
hello: 'enumValue1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,178 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { ExpressionError } from '../errors';
|
||||
import { ExpressionRuntime } from '../runtime';
|
||||
import type { Logger } from '../types';
|
||||
|
||||
const SILENT_LOGGER: Logger = {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
|
||||
describe('ExpressionRuntime', () => {
|
||||
const runtime = new ExpressionRuntime(SILENT_LOGGER);
|
||||
|
||||
describe('evaluate', () => {
|
||||
it.each([
|
||||
{
|
||||
scenario: 'simple condition',
|
||||
condition: 'isBetaUser === true',
|
||||
inputs: { isBetaUser: false },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
scenario: 'simple condition with multiple inputs variables',
|
||||
condition: 'useProductA && !isBetaUser',
|
||||
inputs: {
|
||||
useProductA: true,
|
||||
isBetaUser: false,
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'condition with objects in inputs variables',
|
||||
condition: 'products.includes("productA") && userSegments.alpha',
|
||||
inputs: {
|
||||
products: ['productA', 'productB'],
|
||||
userSegments: {
|
||||
alpha: true,
|
||||
beta: false,
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array method',
|
||||
condition: 'reviews.every(review => !!review.status)',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array every',
|
||||
condition: 'reviews.every(review => review.status === "approved")',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array map',
|
||||
condition: '[1, 2, 3].map(n => n * x)',
|
||||
inputs: { x: 2 },
|
||||
expectedResult: [2, 4, 6],
|
||||
},
|
||||
])(
|
||||
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||
}
|
||||
);
|
||||
|
||||
const INVALID_EXPRESSSIONS = [
|
||||
{
|
||||
scenario: 'invalid syntax',
|
||||
condition: 't}=d',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
scenario: 'non conditional expression',
|
||||
condition: 'const a = 1;',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
scenario: 'unsafe expression',
|
||||
condition: 'while (1) {}',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
scenario: 'unsafe expression',
|
||||
condition: '[1, 2, 3].map(() => { while (1) {}})',
|
||||
inputs: {},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(INVALID_EXPRESSSIONS)(
|
||||
'should return an object with the error for non conditional expression or syntax errors when using safeEvaluate is on (default): $scenario',
|
||||
({ condition, inputs }) => {
|
||||
const result = runtime.safeEvaluate(condition, inputs);
|
||||
expect(result.value).toBeUndefined();
|
||||
expect(result.error instanceof ExpressionError).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(INVALID_EXPRESSSIONS)(
|
||||
'should throw an error when using evaluate with invalid expressions',
|
||||
({ condition, inputs }) => {
|
||||
expect(() => runtime.evaluate(condition, inputs)).toThrowError(ExpressionError);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('should produce a valid ESTree compatible AST node for conditional expressions', () => {
|
||||
const ast = runtime.parse('isBetaUser === true');
|
||||
|
||||
expect(ast.result).toEqual({
|
||||
type: 'BinaryExpression',
|
||||
start: 0,
|
||||
end: 19,
|
||||
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 19 } },
|
||||
left: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 10,
|
||||
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
||||
name: 'isBetaUser',
|
||||
},
|
||||
operator: '===',
|
||||
right: {
|
||||
type: 'Literal',
|
||||
start: 15,
|
||||
end: 19,
|
||||
loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 19 } },
|
||||
value: true,
|
||||
raw: 'true',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
scenario: 'invalid syntax',
|
||||
condition: 't}=d',
|
||||
},
|
||||
{
|
||||
scenario: 'non conditional expression',
|
||||
condition: 'const a = 1;',
|
||||
},
|
||||
])(
|
||||
'should throw an error for non conditional expressions or syntax errors: $scenario',
|
||||
({ condition }) => {
|
||||
expect(() => runtime.parse(condition)).toThrowError(ExpressionError);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe.skip('generate', () => {
|
||||
it.each([
|
||||
{
|
||||
scenario: 'simple condition',
|
||||
condition: 'isBetaUser === true',
|
||||
},
|
||||
{
|
||||
scenario: 'simple condition with multiple inputs variables',
|
||||
condition: 'useProductA && !isBetaUser',
|
||||
},
|
||||
{
|
||||
scenario: 'condition with objects in inputs variables',
|
||||
condition: 'products.includes("productA") && userSegments.alpha',
|
||||
},
|
||||
])(
|
||||
'should produce the original expression using an AST node produced by parse: $scenario',
|
||||
({ condition }) => {
|
||||
const { result } = runtime.parse(condition);
|
||||
expect(runtime.generate(result)).toStrictEqual(condition);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { ExpressionRuntime, parseTemplate } from '../';
|
||||
|
||||
describe('template expressions', () => {
|
||||
it('should parse template into parts', () => {
|
||||
const parts = parseTemplate('Hello {{ user.name }}!');
|
||||
expect(parts).toEqual([
|
||||
{ type: 'text', value: 'Hello ', start: 0, end: 6 },
|
||||
{ type: 'expression', value: 'user.name', start: 8, end: 19 },
|
||||
{ type: 'text', value: '!', start: 21, end: 22 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse template starting with an expression', () => {
|
||||
const parts = parseTemplate('{{ user.name }} is cool');
|
||||
expect(parts).toEqual([
|
||||
{ type: 'expression', value: 'user.name', start: 2, end: 13 },
|
||||
{ type: 'text', value: ' is cool', start: 15, end: 23 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse template without expressions', () => {
|
||||
const parts = parseTemplate('Hello world');
|
||||
expect(parts).toEqual([{ type: 'text', value: 'Hello world', start: 0, end: 11 }]);
|
||||
});
|
||||
|
||||
it('should evaluate template', () => {
|
||||
const runtime = new ExpressionRuntime();
|
||||
const result = runtime.evaluateTemplate('Hello {{ user.name }}!', {
|
||||
user: { name: 'John' },
|
||||
});
|
||||
expect(result).toBe('Hello John!');
|
||||
});
|
||||
});
|
||||
@@ -1,568 +0,0 @@
|
||||
import type {
|
||||
Node as AcornNode,
|
||||
AnyNode,
|
||||
BinaryExpression,
|
||||
Expression,
|
||||
Identifier,
|
||||
Literal,
|
||||
MemberExpression,
|
||||
PrivateIdentifier,
|
||||
Super,
|
||||
} from 'acorn';
|
||||
import { isDummy } from 'acorn-loose';
|
||||
import * as walk from 'acorn-walk';
|
||||
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
import { type ExtractSymbolDef, SymbolType, SymbolsTable } from './symbols';
|
||||
import {
|
||||
type AutocompleteLiteralValueSuggestion,
|
||||
type AutocompleteOperatorSuggestion,
|
||||
type AutocompleteSuggestions,
|
||||
type AutocompleteSymbolSuggestion,
|
||||
type DirectLiteralValueSuggestion,
|
||||
type ExpressionParserResult,
|
||||
type Logger,
|
||||
SUPPORTED_BINARY_OPERATORS,
|
||||
SUPPORTED_CONDITIONAL_OPERATORS,
|
||||
SUPPORTED_LOGICAL_OPERATORS,
|
||||
} from './types';
|
||||
|
||||
interface ExpressionParser {
|
||||
parse(expr: string, options: { loose?: boolean }): ExpressionParserResult;
|
||||
}
|
||||
|
||||
export class AutoComplete {
|
||||
#parser: ExpressionParser;
|
||||
#logger: Logger;
|
||||
|
||||
constructor(parser: ExpressionParser, logger: Logger = console) {
|
||||
this.#parser = parser;
|
||||
this.#logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates autocomplete suggestions based on the input expression and cursor offset position.
|
||||
*/
|
||||
public getSuggestions(
|
||||
expr: string,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): AutocompleteSuggestions {
|
||||
if (!expr.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const { result, invalidNodes } = this.#parser.parse(expr, { loose: true });
|
||||
|
||||
// Locate the node at the cursor position.
|
||||
const nodeAtCursorFound = walk.findNodeAround(result, cursorOffset, (_type, node) =>
|
||||
isNodeAtCursor(node, cursorOffset)
|
||||
);
|
||||
|
||||
if (!nodeAtCursorFound) {
|
||||
// When we can't find one we might be in the boundary of the program/expression.
|
||||
// We could possibly be in a whitespace at the end of the expression or in a situation where the parsed
|
||||
// tree may contain 2 top level ExpressionStatement (second one returned as invalid node by the parser).
|
||||
//
|
||||
// In this case we want to provide operators as suggestions and refine the search of the "cursor" node to either:
|
||||
// - using the end position of the whole expression AST (e.g white space at the very end of the expression string)
|
||||
// - or using the second top level ExpressionStatement node found by the parser as the cursor is at the end of that node.
|
||||
if (cursorOffset > result.end) {
|
||||
const ast = invalidNodes.length > 0 ? invalidNodes[0]?.expression : result;
|
||||
|
||||
if (!ast) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lastNodeFound = walk.findNodeAround(ast, ast.end, (_type, node) =>
|
||||
isNodeAtCursor(node, ast.end)
|
||||
);
|
||||
if (!lastNodeFound) {
|
||||
return [];
|
||||
}
|
||||
const { node } = lastNodeFound;
|
||||
|
||||
if (!isAnyNode(node)) {
|
||||
throw Error(`Unexpected node type ${node.type}`);
|
||||
}
|
||||
|
||||
return this.getOperatorSuggestionsForNode(ast, node, result.end, context);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const { node } = nodeAtCursorFound;
|
||||
|
||||
if (!isAnyNode(node)) {
|
||||
throw Error(`Unexpected node type ${node.type}`);
|
||||
}
|
||||
|
||||
return this.getSuggestionsForNode(result, node, expr, cursorOffset, context);
|
||||
} catch (error) {
|
||||
this.#logger.error('Error while computing autocomplete suggestions', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides autocomplete suggestions for a specific node in the AST.
|
||||
*/
|
||||
private getSuggestionsForNode(
|
||||
ast: Expression,
|
||||
node: AnyNode,
|
||||
expr: string,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): AutocompleteSuggestions {
|
||||
// When the node is an identifier look up the parent to get more context for the suggestions.
|
||||
let inferNode: AnyNode = node;
|
||||
if (node.type === 'Identifier' || node.type === 'Literal') {
|
||||
const parent = findParentNode(node, ast);
|
||||
inferNode =
|
||||
parent &&
|
||||
!['ExpressionStatement', 'LogicalExpression', 'ConditionalExpression'].includes(
|
||||
parent.type
|
||||
)
|
||||
? parent
|
||||
: node;
|
||||
}
|
||||
|
||||
switch (inferNode.type) {
|
||||
case 'Identifier':
|
||||
case 'MemberExpression': {
|
||||
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
|
||||
inferNode,
|
||||
cursorOffset
|
||||
);
|
||||
|
||||
// Fetch suggestions from the symbol table
|
||||
const candidatesKeys = context.getMatchingSymbolsKeys(pathParts);
|
||||
|
||||
const suggestions: AutocompleteSymbolSuggestion[] = [];
|
||||
for (const candidate of candidatesKeys) {
|
||||
const symbolInfo = context.getSymbolInfo(candidate);
|
||||
if (symbolInfo) {
|
||||
suggestions.push({ type: 'symbol', symbol: symbolInfo });
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.length === 1) {
|
||||
const lastPathSegment = pathParts.at(-1)?.replace(/\*$/, '');
|
||||
|
||||
// Return no suggestion when the only match is an exact match of the
|
||||
// typed token.
|
||||
return lastPathSegment !== suggestions[0]?.symbol.definition.name
|
||||
? suggestions
|
||||
: [];
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
case 'AssignmentExpression':
|
||||
case 'UnaryExpression': {
|
||||
// Provide suggestions for binary or logical operators based on the parsed operator
|
||||
// of the partial expression (e.g suggest "==" when typing "=" (parsed as AssignmentExpression)).
|
||||
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
const { left, right } = inferNode;
|
||||
|
||||
const isOperatorBinaryOp = SUPPORTED_BINARY_OPERATORS.some(
|
||||
(op) => op.operator === inferNode.operator
|
||||
);
|
||||
|
||||
const operatorIndex = expr.indexOf(inferNode.operator, left.end);
|
||||
const operatorOffset = operatorIndex + inferNode.operator.length;
|
||||
const isCursorAfterOperator = cursorOffset > operatorOffset;
|
||||
|
||||
const shouldSuggestValues =
|
||||
isCursorAfterOperator &&
|
||||
isOperatorBinaryOp &&
|
||||
isNodeAtCursor(right, cursorOffset);
|
||||
|
||||
if (shouldSuggestValues) {
|
||||
return this.getLiteralValueSuggestionsForNode(inferNode, cursorOffset, context);
|
||||
}
|
||||
|
||||
// Provide suggestions for binary operators based on the parsed operator
|
||||
return this.getOperatorSuggestionsForNode(ast, inferNode, cursorOffset, context);
|
||||
}
|
||||
case 'ConditionalExpression': {
|
||||
const { consequent, alternate } = inferNode;
|
||||
|
||||
if (isNodeAtCursor(consequent, cursorOffset)) {
|
||||
return this.getSuggestionsForNode(ast, consequent, expr, cursorOffset, context);
|
||||
}
|
||||
|
||||
if (isNodeAtCursor(alternate, cursorOffset)) {
|
||||
return this.getSuggestionsForNode(ast, alternate, expr, cursorOffset, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a path corresponding to the MemberExpression node that can be used to lookup matching symbols in the symbol table.
|
||||
*/
|
||||
private getSymbolsPathPartsForMemberExpressionNode(
|
||||
node: MemberExpression | Identifier,
|
||||
cursorOffset: number,
|
||||
options?: { withWildcardMatches: boolean }
|
||||
): string[] {
|
||||
const withWildcardMatches = options?.withWildcardMatches ?? true;
|
||||
const pathParts: string[] = [];
|
||||
|
||||
switch (node.type) {
|
||||
case 'MemberExpression':
|
||||
{
|
||||
const memberProperty = node.property;
|
||||
|
||||
// Only support identifier or literal expressions as member properties.
|
||||
if (!isSupportedMemberProperty(memberProperty)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Push the path part corresponding to the member property (e.g b in a.b or a['b'])
|
||||
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
|
||||
memberProperty,
|
||||
cursorOffset
|
||||
);
|
||||
|
||||
if (propertyPathPart) {
|
||||
pathParts.push(
|
||||
withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart
|
||||
);
|
||||
}
|
||||
|
||||
// Go through the parent(s) in the chain and add their path parts as well
|
||||
let parent: Expression | Super | undefined = node.object;
|
||||
while (parent) {
|
||||
const parentProperty = 'property' in parent ? parent.property : parent;
|
||||
|
||||
// Only support identifier or literal expressions as parent property.
|
||||
const parentPropertyPathPart = isSupportedMemberProperty(parentProperty)
|
||||
? this.getSymbolsPathPartFromMemberProperty(
|
||||
parentProperty,
|
||||
cursorOffset
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (parentPropertyPathPart) {
|
||||
pathParts.unshift(parentPropertyPathPart);
|
||||
}
|
||||
|
||||
parent = 'object' in parent ? parent.object : undefined;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Identifier': {
|
||||
const propertyPathPart = this.getSymbolsPathPartFromMemberProperty(
|
||||
node,
|
||||
cursorOffset
|
||||
);
|
||||
if (propertyPathPart) {
|
||||
pathParts.push(withWildcardMatches ? `${propertyPathPart}*` : propertyPathPart);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertNever(node);
|
||||
}
|
||||
|
||||
return pathParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a part of a symbol path corresponding to a MemberExpression node property.
|
||||
*/
|
||||
private getSymbolsPathPartFromMemberProperty(
|
||||
node: Identifier | Literal,
|
||||
cursorOffset: number
|
||||
): string {
|
||||
switch (node.type) {
|
||||
case 'Identifier': {
|
||||
if (isDummy(node)) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
return isNodeAtCursor(node, cursorOffset)
|
||||
? node.name.slice(0, cursorOffset)
|
||||
: node.name;
|
||||
}
|
||||
case 'Literal': {
|
||||
if (isDummy(node) || !node.value) {
|
||||
return '*';
|
||||
}
|
||||
const value = String(node.value);
|
||||
return isNodeAtCursor(node, cursorOffset) ? value.slice(0, cursorOffset) : value;
|
||||
}
|
||||
default:
|
||||
assertNever(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides autocomplete literal value suggestions for a specific node in the AST.
|
||||
*/
|
||||
private getLiteralValueSuggestionsForNode(
|
||||
node: BinaryExpression,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteLiteralValueSuggestion> {
|
||||
const { left, right } = node;
|
||||
|
||||
if (left.type !== 'MemberExpression' && left.type !== 'Identifier') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (right.type !== 'Identifier' && right.type !== 'Literal') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const leftSymbolPath = this.getSymbolsPathPartsForMemberExpressionNode(left, cursorOffset, {
|
||||
withWildcardMatches: false,
|
||||
});
|
||||
|
||||
const isLeftComputedMember = left.type === 'MemberExpression' && left.computed;
|
||||
const leftSymbolInfo = context.getSymbolInfo(
|
||||
isLeftComputedMember ? leftSymbolPath.slice(0, -1) : leftSymbolPath
|
||||
);
|
||||
|
||||
if (!leftSymbolInfo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (leftSymbolInfo.definition.type) {
|
||||
case SymbolType.Boolean: {
|
||||
const suggestions: DirectLiteralValueSuggestion[] = [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: true },
|
||||
},
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: { kind: 'direct', type: SymbolType.Boolean, data: false },
|
||||
},
|
||||
];
|
||||
|
||||
return isDummy(right)
|
||||
? suggestions
|
||||
: suggestions.filter((literalValue) => {
|
||||
const literalValueString = String(literalValue.value.data);
|
||||
const rightNodeValue =
|
||||
right.type === 'Identifier' ? right.name : (right.raw ?? '');
|
||||
return (
|
||||
literalValueString !== rightNodeValue &&
|
||||
literalValueString.startsWith(rightNodeValue)
|
||||
);
|
||||
});
|
||||
}
|
||||
case SymbolType.String: {
|
||||
if (!leftSymbolInfo.definition.enum) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rightNodeValue = (() => {
|
||||
if (right.type === 'Identifier') {
|
||||
return right.name;
|
||||
}
|
||||
|
||||
const nodeValue = right.raw ?? '';
|
||||
if (/^(['"])(.*)\1$/.test(nodeValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeValue.replaceAll(/["']/g, '');
|
||||
})();
|
||||
|
||||
if (rightNodeValue === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const suggestions = isDummy(right)
|
||||
? leftSymbolInfo.definition.enum
|
||||
: leftSymbolInfo.definition.enum.filter((enumValue) =>
|
||||
enumValue.startsWith(rightNodeValue)
|
||||
);
|
||||
|
||||
return suggestions.map((value) => ({
|
||||
type: 'literal-value',
|
||||
value: {
|
||||
kind: 'direct',
|
||||
type: SymbolType.String,
|
||||
data: value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
case SymbolType.Array: {
|
||||
// Only return a literal in array value suggestion when the left hand side of the binary expression
|
||||
// is computed, e.g myArray[1]
|
||||
return isLeftComputedMember
|
||||
? [
|
||||
{
|
||||
type: 'literal-value',
|
||||
value: {
|
||||
kind: 'in-array',
|
||||
srcSymbol: leftSymbolInfo.definition,
|
||||
matchedLiteralString: !isDummy(right)
|
||||
? right.type === 'Identifier'
|
||||
? right.name
|
||||
: (right.raw ?? '')
|
||||
: '',
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides autocomplete operator suggestions for a specific node in the AST.
|
||||
*/
|
||||
private getOperatorSuggestionsForNode(
|
||||
ast: Expression,
|
||||
node: AnyNode,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
if (node.type === 'Literal') {
|
||||
const parent = findParentNode(node, ast);
|
||||
|
||||
if (parent?.type === 'BinaryExpression') {
|
||||
return [...SUPPORTED_LOGICAL_OPERATORS, ...SUPPORTED_CONDITIONAL_OPERATORS].map(
|
||||
(op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const literalSymbol = SymbolsTable.inferSymbolFromValue(node.raw);
|
||||
return this.getOperatorSuggestionsForSymbol(literalSymbol);
|
||||
}
|
||||
|
||||
// When the node is an identifier look the parent to get more context for the suggestions
|
||||
let inferNode: AnyNode = node;
|
||||
if (node.type === 'Identifier') {
|
||||
const parent = findParentNode(node, ast);
|
||||
inferNode = parent && parent.type !== 'ExpressionStatement' ? parent : node;
|
||||
}
|
||||
|
||||
switch (inferNode.type) {
|
||||
case 'MemberExpression': {
|
||||
const pathParts = this.getSymbolsPathPartsForMemberExpressionNode(
|
||||
inferNode,
|
||||
cursorOffset,
|
||||
{
|
||||
withWildcardMatches: false,
|
||||
}
|
||||
);
|
||||
const symbolInfo = context.getSymbolInfo(pathParts);
|
||||
return symbolInfo
|
||||
? this.getOperatorSuggestionsForSymbol(symbolInfo.definition)
|
||||
: [];
|
||||
}
|
||||
case 'AssignmentExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'UnaryExpression': {
|
||||
// Starting to write a binary/logical operator so suggest operator matching the already
|
||||
// typed character as operator.
|
||||
const operator = inferNode.operator;
|
||||
return (
|
||||
[...SUPPORTED_BINARY_OPERATORS, ...SUPPORTED_LOGICAL_OPERATORS]
|
||||
.filter((op) => op.operator.startsWith(operator))
|
||||
// No need to include the operator that match exactly
|
||||
.filter((op) => op.operator !== operator)
|
||||
.map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
}))
|
||||
);
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides autocomplete operator suggestions based on the type of a symbol.
|
||||
*/
|
||||
private getOperatorSuggestionsForSymbol(
|
||||
symbol: ExtractSymbolDef<SymbolType>
|
||||
): Array<AutocompleteOperatorSuggestion> {
|
||||
switch (symbol.type) {
|
||||
case SymbolType.Number:
|
||||
case SymbolType.Boolean:
|
||||
case SymbolType.Null:
|
||||
case SymbolType.Undefined:
|
||||
case SymbolType.Object:
|
||||
case SymbolType.Array: {
|
||||
const equalityOps = SUPPORTED_BINARY_OPERATORS.slice(0, 4);
|
||||
const finalSuggestions =
|
||||
symbol.type === SymbolType.Boolean
|
||||
? [
|
||||
...equalityOps,
|
||||
...SUPPORTED_LOGICAL_OPERATORS,
|
||||
...SUPPORTED_CONDITIONAL_OPERATORS,
|
||||
]
|
||||
: equalityOps;
|
||||
return finalSuggestions.map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
}));
|
||||
}
|
||||
case SymbolType.String:
|
||||
return SUPPORTED_BINARY_OPERATORS.map((op) => ({
|
||||
type: 'operator',
|
||||
...op,
|
||||
}));
|
||||
case SymbolType.Function:
|
||||
return this.getOperatorSuggestionsForSymbol(symbol.returns);
|
||||
case SymbolType.Union: {
|
||||
return symbol.members.reduce<Array<AutocompleteOperatorSuggestion>>((prev, cur) => {
|
||||
prev.push(...this.getOperatorSuggestionsForSymbol(cur));
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
default:
|
||||
assertNever(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the parent of child node in the provided AST.
|
||||
*/
|
||||
function findParentNode(child: AnyNode, ast: Expression): AnyNode | undefined {
|
||||
let foundParent: AnyNode | undefined;
|
||||
|
||||
walk.ancestor(ast, {
|
||||
[child.type]: (node: AcornNode, _state: undefined, ancestors: AnyNode[]) => {
|
||||
if (node.start === child.start && node.end === child.end) {
|
||||
// The parent is the second last ancestor in the stack (last one is the actual node)
|
||||
foundParent = ancestors[ancestors.length - 2];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return foundParent;
|
||||
}
|
||||
|
||||
function isSupportedMemberProperty(node: Expression | PrivateIdentifier | Super) {
|
||||
return node.type === 'Identifier' || node.type === 'Literal';
|
||||
}
|
||||
|
||||
function isNodeAtCursor(node: AcornNode, cursorOffset: number) {
|
||||
return cursorOffset >= node.start && cursorOffset <= node.end;
|
||||
}
|
||||
|
||||
function isAnyNode(node: AcornNode): node is AnyNode {
|
||||
return 'type' in node;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Position, Token } from 'acorn';
|
||||
|
||||
export class ExpressionError extends Error {
|
||||
/**
|
||||
* The location of the error in the parsed expression.
|
||||
*/
|
||||
public location?: Position | null;
|
||||
|
||||
/**
|
||||
* The expression token with the error
|
||||
*/
|
||||
public token?: Token;
|
||||
|
||||
constructor(message: string, loc?: Position | null, token?: Token) {
|
||||
super(message);
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, ExpressionError);
|
||||
}
|
||||
this.name = 'ExpressionError';
|
||||
this.location = loc;
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from './errors';
|
||||
export * from './input-values';
|
||||
export * from './runtime';
|
||||
export * from './symbols';
|
||||
export * from './template';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { filterOutNullable } from './utils';
|
||||
|
||||
type InputValuesType =
|
||||
| null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: InputValuesType }
|
||||
| InputValuesType[];
|
||||
|
||||
/**
|
||||
* Infers a default inputValues object based on the JSON schema of an object.
|
||||
*/
|
||||
export function inferDefaultInputValuesFromObjectJSONSchema(
|
||||
schema: JSONSchema7
|
||||
): Record<string, InputValuesType> {
|
||||
if (schema.type !== 'object' || !schema.properties) {
|
||||
throw new Error(`Expected schema of object to be provided: ${schema.type}`);
|
||||
}
|
||||
|
||||
const result: Record<string, InputValuesType> = {};
|
||||
|
||||
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
||||
if (typeof propertySchema === 'boolean') {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[key] = inferDefaultInputValueFromJSONSchema(propertySchema);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function inferDefaultInputValueFromJSONSchema(schema: JSONSchema7): InputValuesType {
|
||||
switch (schema.type) {
|
||||
case 'object':
|
||||
return inferDefaultInputValuesFromObjectJSONSchema(schema);
|
||||
case 'array': {
|
||||
if (schema.items && Array.isArray(schema.items)) {
|
||||
return schema.items.map((itemSchema) => {
|
||||
if (typeof itemSchema === 'boolean') {
|
||||
return false;
|
||||
}
|
||||
return inferDefaultInputValueFromJSONSchema(itemSchema);
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'integer':
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
return inferDefaultInputValueFromPrimitive(schema);
|
||||
default:
|
||||
throw new Error(`Unsupported schema type: ${schema.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function inferDefaultInputValueFromPrimitive(schema: JSONSchema7): InputValuesType {
|
||||
switch (schema.type) {
|
||||
case 'boolean':
|
||||
return true;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 1234;
|
||||
case 'string': {
|
||||
const enumValues = schema.enum?.filter(filterOutNullable);
|
||||
return enumValues?.[0] ?? 'default';
|
||||
}
|
||||
case 'null':
|
||||
return null;
|
||||
default:
|
||||
throw new Error(`Unsupported schema type: ${schema.type}`);
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
import {
|
||||
type Options as AcornOptions,
|
||||
type Expression,
|
||||
type ExpressionStatement,
|
||||
type Position,
|
||||
type Program,
|
||||
type Token,
|
||||
parse,
|
||||
tokenizer,
|
||||
} from 'acorn';
|
||||
import { parse as parseLoose } from 'acorn-loose';
|
||||
import escodegen from 'escodegen';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
const { evaluate } = evalESTreeExpr;
|
||||
|
||||
import { AutoComplete } from './autocomplete';
|
||||
import { ExpressionError } from './errors';
|
||||
import type { SymbolsTable } from './symbols';
|
||||
import type { TemplatePart } from './template';
|
||||
import { parseTemplate as parseTemplateParts } from './template';
|
||||
import type { ExpressionAutocompleteResults, ExpressionParserResult, Logger } from './types';
|
||||
import { formatExpressionResult } from './utils';
|
||||
|
||||
export class ExpressionRuntime {
|
||||
#parserOptions: AcornOptions;
|
||||
#autocompleter: AutoComplete;
|
||||
#logger: Logger;
|
||||
|
||||
constructor(logger: Logger = console) {
|
||||
this.#parserOptions = {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'script',
|
||||
allowHashBang: false,
|
||||
locations: true,
|
||||
};
|
||||
this.#autocompleter = new AutoComplete(this, logger);
|
||||
this.#logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an expression based on the given inputs/context.
|
||||
*/
|
||||
public evaluate(expr: string, inputs: object): unknown {
|
||||
try {
|
||||
const parsed = this.parse(expr);
|
||||
|
||||
if (parsed.invalidNodes.length > 0) {
|
||||
throw new ExpressionError('Invalid nodes found when parsing');
|
||||
}
|
||||
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error instanceof Error
|
||||
? new ExpressionError(error.message)
|
||||
: new ExpressionError('Unexpected error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an expression safely by returning the error instead of throwing when invalid.
|
||||
*/
|
||||
public safeEvaluate(
|
||||
expr: string,
|
||||
inputs: object
|
||||
): { value: unknown; error?: undefined } | { value?: undefined; error: ExpressionError } {
|
||||
try {
|
||||
const value = this.evaluate(expr, inputs);
|
||||
return {
|
||||
value,
|
||||
};
|
||||
} catch (error) {
|
||||
this.#logger.error(`Error while evaluating expression ${expr}`, error);
|
||||
|
||||
if (error instanceof ExpressionError) {
|
||||
return {
|
||||
value: undefined,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: undefined,
|
||||
error:
|
||||
error instanceof Error
|
||||
? new ExpressionError(error.message)
|
||||
: new ExpressionError('Unexpected error'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a condition safely to a boolean.
|
||||
*/
|
||||
public evaluateBoolean(expr: string, inputs: object): boolean {
|
||||
if (expr.trim().length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const evalResult = this.safeEvaluate(expr, inputs);
|
||||
|
||||
if (typeof evalResult.error !== 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(evalResult.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an array of conditions as a single logical expression.
|
||||
* The function treats the conditions as if they were joined by an AND operator,
|
||||
* meaning the evaluation returns `true` only if all conditions are truthy.
|
||||
*/
|
||||
public evaluateBooleanAll(expressions: string[], inputs: object): boolean {
|
||||
if (expressions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return expressions.every((expression) => this.evaluateBoolean(expression, inputs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a template and validate all embedded expressions.
|
||||
*/
|
||||
public parseTemplate(template: string): { parts: TemplatePart[]; errors: ExpressionError[] } {
|
||||
const parts = parseTemplateParts(template);
|
||||
const errors: ExpressionError[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.type === 'expression') {
|
||||
try {
|
||||
const { invalidNodes } = this.parse(part.value);
|
||||
if (invalidNodes.length > 0) {
|
||||
errors.push(new ExpressionError('Invalid expression'));
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as ExpressionError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { parts, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a template string containing `{{ expression }}` placeholders.
|
||||
*/
|
||||
public evaluateTemplate(template: string, inputs: object): string {
|
||||
const { parts } = this.parseTemplate(template);
|
||||
|
||||
return parts
|
||||
.map((part) => {
|
||||
if (part.type === 'text') {
|
||||
return part.value;
|
||||
}
|
||||
const result = this.evaluate(part.value, inputs);
|
||||
return formatExpressionResult(result, '');
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary expression and returns an @ExpressionParserResult.
|
||||
*/
|
||||
public parse(
|
||||
expr: string,
|
||||
options: { loose?: boolean } = {
|
||||
loose: false,
|
||||
}
|
||||
): ExpressionParserResult {
|
||||
try {
|
||||
const ast = options.loose
|
||||
? parseLoose(expr, { ...this.#parserOptions })
|
||||
: parse(expr, { ...this.#parserOptions });
|
||||
|
||||
if (!ast.body || ast.body.length === 0) {
|
||||
throw new ExpressionError('Empty or invalid expression');
|
||||
}
|
||||
|
||||
// Extract the first expression statement that we find
|
||||
const firstExprIndex = ast.body.findIndex((node) => isParsedExpressionStatement(node));
|
||||
const [statement] = ast.body.splice(firstExprIndex, 1);
|
||||
|
||||
if (!statement || !isParsedExpressionStatement(statement)) {
|
||||
throw new ExpressionError('Empty or invalid expression');
|
||||
}
|
||||
|
||||
// Return information on the other nodes as invalid nodes
|
||||
const invalidNodes = ast.body.filter(filterOutModuleDeclarationStatement);
|
||||
|
||||
return {
|
||||
result: statement.expression,
|
||||
invalidNodes,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw createExpressionErrorFromSyntaxError(expr, error);
|
||||
}
|
||||
if (error instanceof ExpressionError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ExpressionError('Unexpected error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides autocomplete suggestions for the given expression at the provided cursor offset.
|
||||
*/
|
||||
public autocomplete(
|
||||
expr: string,
|
||||
cursorOffset: number,
|
||||
context: SymbolsTable
|
||||
): ExpressionAutocompleteResults {
|
||||
const suggestions = this.#autocompleter.getSuggestions(expr, cursorOffset, context);
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
public generate(_node: Expression): string {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
}
|
||||
|
||||
function createExpressionErrorFromSyntaxError(
|
||||
code: string,
|
||||
error: SyntaxError & { loc?: Position }
|
||||
): ExpressionError {
|
||||
const loc = error.loc;
|
||||
|
||||
if (!loc) {
|
||||
return new ExpressionError(error.message);
|
||||
}
|
||||
|
||||
const errorMessage = `${error.message.replace(/\s*\(\d+:\d+\)$/, '')} at ${code.split('\n').length > 1 ? `line ${loc.line}, ` : ''}char ${loc.column}`;
|
||||
const token = getTokenAtLoc(code, loc);
|
||||
|
||||
if (!token) {
|
||||
return new ExpressionError(errorMessage, loc);
|
||||
}
|
||||
|
||||
return new ExpressionError(errorMessage, loc, token);
|
||||
}
|
||||
function getTokenAtLoc(code: string, errorLoc: Position): Token | undefined {
|
||||
const tokens = tokenizer(code, {
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
});
|
||||
|
||||
try {
|
||||
for (const token of tokens) {
|
||||
if (!token.loc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { start, end } = token.loc;
|
||||
|
||||
const onSameLine = errorLoc.line === start.line;
|
||||
const inColumnRange = errorLoc.column >= start.column && errorLoc.column < end.column;
|
||||
|
||||
if (onSameLine && inColumnRange) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isParsedExpressionStatement(
|
||||
statement: Program['body'][number]
|
||||
): statement is ExpressionStatement {
|
||||
return statement.type === 'ExpressionStatement';
|
||||
}
|
||||
|
||||
export function filterOutModuleDeclarationStatement(
|
||||
statement: Program['body'][number]
|
||||
): statement is ExpressionStatement {
|
||||
return ![
|
||||
'ImportDeclaration',
|
||||
'ExportNamedDeclaration',
|
||||
'ExportDefaultDeclaration',
|
||||
'ExportAllDeclaration',
|
||||
].includes(statement.type);
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { SymbolArray, SymbolObject, SymbolString } from '../symbols';
|
||||
import { SymbolsTable } from '../symbols-table';
|
||||
import type { SymbolType } from '../types';
|
||||
|
||||
describe('ExpressionRuntime', () => {
|
||||
const initialSymbols = {
|
||||
visitor: SymbolObject({
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: SymbolObject({
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: SymbolString({ name: 'key' }),
|
||||
flags: SymbolObject({
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: SymbolString({ name: 'FLAG1' }),
|
||||
FLAG2: SymbolString({ name: 'FLAG2' }),
|
||||
FLAG3: SymbolString({ name: 'FLAG3' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
hello: SymbolArray({
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: SymbolString(),
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
};
|
||||
describe('addSymbols', () => {
|
||||
it('should the symbols matching the provided object to the table', () => {
|
||||
const symbolsTable = new SymbolsTable(initialSymbols);
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'object',
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
hello: {
|
||||
type: 'array',
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
ref: 'visitor',
|
||||
childrenRefs: ['visitor.claims'],
|
||||
});
|
||||
|
||||
symbolsTable.addSymbols({
|
||||
space: SymbolObject({
|
||||
name: 'space',
|
||||
properties: {
|
||||
id: SymbolString({ name: 'id' }),
|
||||
title: SymbolString({ name: 'title' }),
|
||||
},
|
||||
methods: [],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['space'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'object',
|
||||
name: 'space',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
name: 'id',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
ref: 'space',
|
||||
childrenRefs: ['space.id', 'space.title'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Symbols standard library', () => {
|
||||
it('should allow to access methods & properties defined as part of the standard library', () => {
|
||||
const symbolsTable = new SymbolsTable({
|
||||
id: SymbolString({ name: 'id' }),
|
||||
title: SymbolString({ name: 'title' }),
|
||||
});
|
||||
|
||||
expect(
|
||||
symbolsTable.getSymbolInfo<SymbolType.String>('id')?.definition.properties.length
|
||||
).toMatchObject({
|
||||
type: 'number',
|
||||
name: 'length',
|
||||
description:
|
||||
'The length data property of a String value contains the length of the string in UTF-16 code units.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSymbolInfo', () => {
|
||||
it('should add the symbols matching the initial symbol definition passed to the constructor', () => {
|
||||
const symbolsTable = new SymbolsTable(initialSymbols);
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'object',
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
hello: {
|
||||
type: 'array',
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
ref: 'visitor',
|
||||
childrenRefs: ['visitor.claims'],
|
||||
});
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor', 'claims'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'object',
|
||||
name: 'claims',
|
||||
description: 'The claims contained in the visitor JWT token',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
hello: {
|
||||
type: 'array',
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
ref: 'visitor.claims',
|
||||
parentRef: 'visitor',
|
||||
childrenRefs: [
|
||||
'visitor.claims.key',
|
||||
'visitor.claims.flags',
|
||||
'visitor.claims.hello',
|
||||
],
|
||||
});
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'key'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
ref: 'visitor.claims.key',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.key.length',
|
||||
'visitor.claims.key.at',
|
||||
'visitor.claims.key.endsWith',
|
||||
'visitor.claims.key.includes',
|
||||
],
|
||||
});
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
ref: 'visitor.claims.flags',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1',
|
||||
'visitor.claims.flags.FLAG2',
|
||||
'visitor.claims.flags.FLAG3',
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags', 'FLAG1'])
|
||||
).toMatchObject({
|
||||
definition: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
ref: 'visitor.claims.flags.FLAG1',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG1.length',
|
||||
'visitor.claims.flags.FLAG1.at',
|
||||
'visitor.claims.flags.FLAG1.endsWith',
|
||||
'visitor.claims.flags.FLAG1.includes',
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
symbolsTable.getSymbolInfo(['visitor', 'claims', 'flags', 'FLAG2'])
|
||||
).toMatchObject({
|
||||
definition: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
ref: 'visitor.claims.flags.FLAG2',
|
||||
parentRef: 'visitor.claims.flags',
|
||||
childrenRefs: [
|
||||
'visitor.claims.flags.FLAG2.length',
|
||||
'visitor.claims.flags.FLAG2.at',
|
||||
'visitor.claims.flags.FLAG2.endsWith',
|
||||
'visitor.claims.flags.FLAG2.includes',
|
||||
],
|
||||
});
|
||||
|
||||
expect(symbolsTable.getSymbolInfo(['visitor', 'claims', 'hello'])).toMatchObject({
|
||||
definition: {
|
||||
type: 'array',
|
||||
name: 'hello',
|
||||
description: 'An array of string',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
ref: 'visitor.claims.hello',
|
||||
parentRef: 'visitor.claims',
|
||||
childrenRefs: [
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferSymbolFromValue', () => {
|
||||
it('should infer properly a symbol based on a value', () => {
|
||||
const symbolDef = SymbolsTable.inferSymbolFromValue(
|
||||
{
|
||||
visitor: {
|
||||
claims: {
|
||||
key: 'test',
|
||||
flags: {
|
||||
FLAG1: 'testflag1',
|
||||
FLAG2: 'testflag2',
|
||||
FLAG3: 'testflag3',
|
||||
},
|
||||
hello: ['test', 'test1', 'test2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'context'
|
||||
);
|
||||
expect(symbolDef).toMatchObject({
|
||||
type: 'object',
|
||||
name: 'context',
|
||||
properties: {
|
||||
visitor: {
|
||||
type: 'object',
|
||||
name: 'visitor',
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
name: 'claims',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
hello: {
|
||||
type: 'array',
|
||||
name: 'hello',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferSymbolFromJSONSchema', () => {
|
||||
it('should infer properly a symbol table based on a JSON schema', () => {
|
||||
const symbolDef = SymbolsTable.inferSymbolFromJSONSchema(
|
||||
{
|
||||
type: 'object',
|
||||
description: `The attributes tied to a site's visitor.`,
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
description: 'The user feature flags',
|
||||
properties: {
|
||||
FLAG1: { type: 'string' },
|
||||
FLAG2: { type: 'string' },
|
||||
FLAG3: { type: 'string' },
|
||||
},
|
||||
},
|
||||
hello: {
|
||||
type: 'string',
|
||||
enum: ['test', 'test1', 'test2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'visitor'
|
||||
);
|
||||
expect(symbolDef).toMatchObject({
|
||||
type: 'object',
|
||||
name: 'visitor',
|
||||
description: `The attributes tied to a site's visitor.`,
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
name: 'claims',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
flags: {
|
||||
type: 'object',
|
||||
name: 'flags',
|
||||
description: 'The user feature flags',
|
||||
properties: {
|
||||
FLAG1: {
|
||||
type: 'string',
|
||||
name: 'FLAG1',
|
||||
},
|
||||
FLAG2: {
|
||||
type: 'string',
|
||||
name: 'FLAG2',
|
||||
},
|
||||
FLAG3: {
|
||||
type: 'string',
|
||||
name: 'FLAG3',
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
hello: {
|
||||
type: 'string',
|
||||
enum: ['test', 'test1', 'test2'],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
},
|
||||
},
|
||||
methods: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './symbols';
|
||||
export * from './symbols-table';
|
||||
export * from './types';
|
||||
@@ -1,350 +0,0 @@
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
|
||||
import { filterOutNullable } from '../utils';
|
||||
import {
|
||||
SymbolArray,
|
||||
SymbolBoolean,
|
||||
SymbolNull,
|
||||
SymbolNumber,
|
||||
SymbolObject,
|
||||
SymbolString,
|
||||
SymbolUndefined,
|
||||
} from './symbols';
|
||||
import {
|
||||
type ExtractSymbolDef,
|
||||
type GenericSymbolDef,
|
||||
type ObjectSymbolDef,
|
||||
SymbolType,
|
||||
type SymbolWithMethods,
|
||||
type SymbolWithProperties,
|
||||
resolveSymbolDef,
|
||||
} from './types';
|
||||
|
||||
export interface SymbolInfo<T extends SymbolType = SymbolType> {
|
||||
/**
|
||||
* Definition of the symbol.
|
||||
*/
|
||||
definition: ExtractSymbolDef<T>;
|
||||
|
||||
/**
|
||||
* Reference of the symbol in the table of symbols.
|
||||
*/
|
||||
ref: string;
|
||||
|
||||
/**
|
||||
* Stores the reference to the parent symbol.
|
||||
*/
|
||||
parentRef?: string;
|
||||
|
||||
/**
|
||||
* Stores the reference to the children symbols.
|
||||
*/
|
||||
childrenRefs?: string[];
|
||||
}
|
||||
|
||||
export class SymbolError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, SymbolError);
|
||||
}
|
||||
this.name = 'SymbolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class SymbolsTable {
|
||||
/**
|
||||
* Internal table that keeps track of all symbols reference.
|
||||
*/
|
||||
#table: Record<string, SymbolInfo>;
|
||||
|
||||
/**
|
||||
* Internal table that keep track of the raw symbols definitions.
|
||||
*/
|
||||
#rawSymbols: Record<string, GenericSymbolDef>;
|
||||
|
||||
constructor(initialContext: Record<string, GenericSymbolDef> = {}) {
|
||||
this.#table = {};
|
||||
this.#rawSymbols = {};
|
||||
this.addSymbols(initialContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new symbols table by merging the current one with the provided one.
|
||||
*/
|
||||
merge(other: SymbolsTable): SymbolsTable {
|
||||
return new SymbolsTable({
|
||||
...this.#rawSymbols,
|
||||
...other.#rawSymbols,
|
||||
});
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.#table, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the symbol of a value and generate the appropriate symbol definition.
|
||||
*/
|
||||
static inferSymbolFromValue(value: unknown, name?: string): ExtractSymbolDef<SymbolType> {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return SymbolArray({ items: SymbolUndefined() });
|
||||
}
|
||||
|
||||
const firstItemSymbol = SymbolsTable.inferSymbolFromValue(value.at(0));
|
||||
// Check that the array is not a mixin of different items types.
|
||||
if (value.length > 1) {
|
||||
const secondItemSymbol = SymbolsTable.inferSymbolFromValue(value.at(1));
|
||||
if (firstItemSymbol.type !== secondItemSymbol.type) {
|
||||
throw new SymbolError('Array with mixin items types are not supported');
|
||||
}
|
||||
}
|
||||
return SymbolArray({ name, items: firstItemSymbol });
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return SymbolUndefined({ name });
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return SymbolNull({ name });
|
||||
}
|
||||
|
||||
const valueType = typeof value;
|
||||
switch (valueType) {
|
||||
case 'string':
|
||||
return SymbolString({ name });
|
||||
case 'number':
|
||||
return SymbolNumber({ name });
|
||||
case 'boolean':
|
||||
return SymbolBoolean({ name });
|
||||
case 'object': {
|
||||
const properties = Object.entries(value).reduce<Record<string, GenericSymbolDef>>(
|
||||
(prev, [name, val]) => {
|
||||
prev[name] = SymbolsTable.inferSymbolFromValue(val, name);
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return SymbolObject({ name, properties, methods: [] });
|
||||
}
|
||||
default:
|
||||
throw new SymbolError(`Unsupported symbol type ${valueType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer a table of symbol based on a JSON schema object describing it.
|
||||
*/
|
||||
static inferSymbolFromJSONSchema(
|
||||
schema: JSONSchema7,
|
||||
name?: string
|
||||
): ExtractSymbolDef<SymbolType> {
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
return SymbolString({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
...(schema.enum
|
||||
? {
|
||||
enum: schema.enum
|
||||
.filter(filterOutNullable)
|
||||
.map((enumValue) => enumValue.toString()),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return SymbolNumber({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
});
|
||||
case 'boolean':
|
||||
return SymbolBoolean({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
});
|
||||
case 'null':
|
||||
return SymbolNull({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
});
|
||||
case 'object':
|
||||
return SymbolsTable.#buildObjectSymbolFromJSONSchemaObject(schema, name);
|
||||
case 'array':
|
||||
return SymbolsTable.#buildArraySymbolFromJSONSchemaArray(schema, name);
|
||||
default:
|
||||
throw new Error(`Unsupported schema type: ${schema.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
static #buildObjectSymbolFromJSONSchemaObject(
|
||||
schema: JSONSchema7,
|
||||
name?: string
|
||||
): ObjectSymbolDef {
|
||||
const properties: Record<string, GenericSymbolDef> = {};
|
||||
|
||||
if (schema.properties) {
|
||||
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
|
||||
properties[propertyName] = SymbolsTable.inferSymbolFromJSONSchema(
|
||||
propertySchema as JSONSchema7,
|
||||
propertyName
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return SymbolObject({
|
||||
name,
|
||||
properties,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
methods: [],
|
||||
});
|
||||
}
|
||||
|
||||
static #buildArraySymbolFromJSONSchemaArray(
|
||||
schema: JSONSchema7,
|
||||
name?: string
|
||||
): ExtractSymbolDef<SymbolType.Array> {
|
||||
if (schema.items) {
|
||||
const itemSymbol = SymbolsTable.inferSymbolFromJSONSchema(
|
||||
schema.items as JSONSchema7,
|
||||
`${name || ''}_item`
|
||||
);
|
||||
return SymbolArray({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
items: itemSymbol,
|
||||
});
|
||||
}
|
||||
|
||||
return SymbolArray({
|
||||
name,
|
||||
...(schema.description ? { description: schema.description } : {}),
|
||||
items: SymbolUndefined(),
|
||||
});
|
||||
}
|
||||
|
||||
private generateSymbolRefPath(path: string[]): string {
|
||||
return path.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single symbol to the table at the provided path.
|
||||
*/
|
||||
private addSymbol(path: string[], definition: GenericSymbolDef, raw = false): void {
|
||||
const fullPath = this.generateSymbolRefPath(path);
|
||||
const parentPath = path.slice(0, -1).join('.');
|
||||
|
||||
if (this.#table[fullPath]) {
|
||||
throw new SymbolError(`Symbol "${fullPath}" already exists.`);
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
this.#rawSymbols[fullPath] = definition;
|
||||
}
|
||||
|
||||
// Add the new symbol linking it to its parent
|
||||
this.#table[fullPath] = {
|
||||
definition: resolveSymbolDef(definition),
|
||||
ref: fullPath,
|
||||
parentRef: parentPath || undefined,
|
||||
childrenRefs: [],
|
||||
};
|
||||
|
||||
if (parentPath && this.#table[parentPath]) {
|
||||
if (this.#table[parentPath].childrenRefs) {
|
||||
this.#table[parentPath].childrenRefs.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any nested symbols if the value is a symbol with properties...
|
||||
if (isSymbolWithProperties(definition)) {
|
||||
Object.entries(definition.properties).forEach(([propKey, propSymbol]) => {
|
||||
if (isObjectSymbol(propSymbol)) {
|
||||
this.addSymbols({ [propKey]: propSymbol }, path, false);
|
||||
} else {
|
||||
this.addSymbol([...path, propKey], propSymbol, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ...or a symbol with methods
|
||||
if (isSymbolWithMethods(definition)) {
|
||||
definition.methods.forEach((methodSymbol) => {
|
||||
this.addSymbol([...path, methodSymbol.name], methodSymbol, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provided object of symbols definitions to the symbol table.
|
||||
*/
|
||||
public addSymbols(
|
||||
symbols: Record<string, GenericSymbolDef>,
|
||||
prefix: string[] = [],
|
||||
raw = true
|
||||
): void {
|
||||
for (const [key, symbolDef] of Object.entries(symbols)) {
|
||||
const path = [...prefix, key];
|
||||
|
||||
// Add the current symbol to the table.
|
||||
this.addSymbol(path, symbolDef, raw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a symbol's information using its path in the table.
|
||||
*/
|
||||
public getSymbolInfo<T extends SymbolType>(path: string | string[]): SymbolInfo<T> | undefined {
|
||||
const key = Array.isArray(path) ? path.join('.') : path;
|
||||
const info = this.#table[key];
|
||||
return info ? typedSymbolInfo(info) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all symbol keys matching the pattern defined by the provided path.
|
||||
*/
|
||||
public getMatchingSymbolsKeys(path: string[]): string[] {
|
||||
const wildcardRegex = new RegExp(
|
||||
`^${path
|
||||
.map((segment) => {
|
||||
if (segment.includes('*')) {
|
||||
return `${segment.split('*')[0]}([^.]+)?`;
|
||||
}
|
||||
return segment;
|
||||
})
|
||||
.join('\\.')}$`
|
||||
);
|
||||
|
||||
return Object.keys(this.#table)
|
||||
.filter((key) => wildcardRegex.test(key))
|
||||
.filter(filterOutNullable);
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectSymbol(symbol: GenericSymbolDef): symbol is ObjectSymbolDef {
|
||||
return symbol.type === SymbolType.Object;
|
||||
}
|
||||
|
||||
function isSymbolWithProperties(symbol: GenericSymbolDef): symbol is SymbolWithProperties {
|
||||
return (
|
||||
symbol.type === SymbolType.Object ||
|
||||
symbol.type === SymbolType.Array ||
|
||||
symbol.type === SymbolType.String
|
||||
);
|
||||
}
|
||||
|
||||
function isSymbolWithMethods(symbol: GenericSymbolDef): symbol is SymbolWithMethods {
|
||||
return (
|
||||
symbol.type === SymbolType.Object ||
|
||||
symbol.type === SymbolType.Array ||
|
||||
symbol.type === SymbolType.String
|
||||
);
|
||||
}
|
||||
|
||||
function typedSymbolInfo<T extends SymbolType>(info: SymbolInfo): SymbolInfo<T> {
|
||||
const definition = resolveSymbolDef(info.definition);
|
||||
return { ...info, definition } as SymbolInfo<T>;
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
import {
|
||||
type ArraySymbolDef,
|
||||
type BooleanSymbolDef,
|
||||
type ExtractSymbolDef,
|
||||
type FunctionSymbolDef,
|
||||
type GenericSymbolDef,
|
||||
type NullSymbolDef,
|
||||
type NumberSymbolDef,
|
||||
type ObjectSymbolDef,
|
||||
type StringSymbolDef,
|
||||
SymbolType,
|
||||
type SymbolsWithPropertiesAndMethods,
|
||||
type UndefinedSymbolDef,
|
||||
type UnionSymbolDef,
|
||||
} from './types';
|
||||
|
||||
export function SymbolBoolean(args: Omit<BooleanSymbolDef, 'type'> = {}): BooleanSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Boolean,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolNumber(args: Omit<NumberSymbolDef, 'type'> = {}): NumberSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Number,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolString(
|
||||
args: Omit<StringSymbolDef, 'type' | 'methods' | 'properties'> = {}
|
||||
): StringSymbolDef {
|
||||
return createSymbolWithPropertiesAndMethods<StringSymbolDef>(SymbolType.String, args);
|
||||
}
|
||||
|
||||
export function SymbolObject(args: Omit<ObjectSymbolDef, 'type'>): ObjectSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Object,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolArray(
|
||||
args: Omit<ArraySymbolDef, 'type' | 'methods' | 'properties'>
|
||||
): ArraySymbolDef {
|
||||
return createSymbolWithPropertiesAndMethods(SymbolType.Array, args);
|
||||
}
|
||||
|
||||
export function SymbolFunction(args: Omit<FunctionSymbolDef, 'type'>): FunctionSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Function,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function OptionalFunctionArg(
|
||||
optionalArg: ExtractSymbolDef<SymbolType>
|
||||
): ExtractSymbolDef<SymbolType> & { optional: true } {
|
||||
return {
|
||||
...optionalArg,
|
||||
optional: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolUnion(args: Omit<UnionSymbolDef, 'type'>): UnionSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Union,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolUndefined(args: Omit<UndefinedSymbolDef, 'type'> = {}): UndefinedSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Undefined,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
export function SymbolNull(args: Omit<NullSymbolDef, 'type'> = {}): NullSymbolDef {
|
||||
return {
|
||||
type: SymbolType.Null,
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
function createSymbolWithPropertiesAndMethods<
|
||||
T extends SymbolsWithPropertiesAndMethods & { type: SymbolType },
|
||||
>(type: T['type'], args: Omit<T, 'type' | 'methods' | 'properties'>): T {
|
||||
const symbol = { type, ...args } as T;
|
||||
|
||||
Object.defineProperty(symbol, 'properties', {
|
||||
get() {
|
||||
if (symbol.type === SymbolType.Array) {
|
||||
return isArraySymbol(symbol)
|
||||
? StandardLibrary[SymbolType.Array]?.(symbol).properties
|
||||
: {};
|
||||
}
|
||||
return StandardLibrary[symbol.type]?.properties || {};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(symbol, 'methods', {
|
||||
get() {
|
||||
if (symbol.type === SymbolType.Array) {
|
||||
return isArraySymbol(symbol)
|
||||
? StandardLibrary[SymbolType.Array]?.(symbol).methods
|
||||
: [];
|
||||
}
|
||||
return StandardLibrary[symbol.type]?.methods || [];
|
||||
},
|
||||
});
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
// TODO-ADAPTIVE-CONTENT: extend the definition of the supported standard library methods and properties.
|
||||
|
||||
const StandardLibrary: Partial<
|
||||
{
|
||||
[key in Exclude<SymbolType, SymbolType.Array>]: {
|
||||
properties: Record<string, GenericSymbolDef>;
|
||||
methods: FunctionSymbolDef[];
|
||||
};
|
||||
} & {
|
||||
[SymbolType.Array]: (symbol: ArraySymbolDef) => {
|
||||
properties: Record<string, GenericSymbolDef>;
|
||||
methods: FunctionSymbolDef[];
|
||||
};
|
||||
}
|
||||
> = {
|
||||
[SymbolType.String]: {
|
||||
properties: {
|
||||
length: SymbolNumber({
|
||||
name: 'length',
|
||||
description:
|
||||
'The length data property of a String value contains the length of the string in UTF-16 code units.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
|
||||
}),
|
||||
},
|
||||
methods: [
|
||||
SymbolFunction({
|
||||
name: 'at',
|
||||
description: `Takes an integer value and returns the item at that index, allowing for positive and negative integers.
|
||||
Negative integers count back from the last item in the string.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at',
|
||||
args: [
|
||||
SymbolNumber({
|
||||
name: 'index',
|
||||
description: 'The index (position) of the string character to be returned',
|
||||
}),
|
||||
],
|
||||
returns: SymbolUnion({
|
||||
description: `A String consisting of the single UTF-16 code unit located at the specified position.
|
||||
Returns undefined if the given index can not be found.`,
|
||||
members: [SymbolString(), SymbolUndefined()],
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'endsWith',
|
||||
description: `Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding
|
||||
elements of this object (converted to a String) starting at endPosition – length(this). Otherwise returns false.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith',
|
||||
args: [
|
||||
SymbolString({
|
||||
name: 'searchString',
|
||||
description: `The characters to be searched for at the end of str. Cannot be a regex.
|
||||
All values that are not regexes are coerced to strings, so omitting it or passing undefined causes endsWith() to search for
|
||||
the string "undefined", which is rarely what you want.`,
|
||||
}),
|
||||
OptionalFunctionArg(
|
||||
SymbolNumber({
|
||||
name: 'endPosition',
|
||||
description: `The end position at which searchString is expected to be found
|
||||
(the index of searchString's last character plus 1). Defaults to str.length.`,
|
||||
})
|
||||
),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description: `true if the given characters are found at the end of the string, including when searchString is an empty string;
|
||||
otherwise, false.`,
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'includes',
|
||||
description: `Returns true if searchString appears as a substring of the result of converting this object to a String, at one or more positions
|
||||
that are greater than or equal to position; otherwise, returns false.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes',
|
||||
args: [
|
||||
SymbolString({
|
||||
name: 'searchString',
|
||||
description: `A string to be searched for within str. Cannot be a regex. All values that are not regexes are coerced to strings, so omitting it
|
||||
or passing undefined causes includes() to search for the string "undefined", which is rarely what you want.`,
|
||||
}),
|
||||
OptionalFunctionArg(
|
||||
SymbolNumber({
|
||||
name: 'position',
|
||||
description:
|
||||
'The position within the string at which to begin searching for searchString. (Defaults to 0.)',
|
||||
})
|
||||
),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description: `true if the search string is found anywhere within the given string, including when searchString is an empty string;
|
||||
otherwise, false.`,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
[SymbolType.Array]: (arraySymbolDef: ArraySymbolDef) => ({
|
||||
properties: {
|
||||
length: SymbolNumber({
|
||||
name: 'length',
|
||||
description: `The length data property of an Array instance represents the number of elements in that array.
|
||||
The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
|
||||
}),
|
||||
},
|
||||
methods: [
|
||||
SymbolFunction({
|
||||
name: 'at',
|
||||
description: `Takes an integer value and returns the item at that index, allowing for positive and negative integers.
|
||||
Negative integers count back from the last item in the array.`,
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at',
|
||||
args: [
|
||||
SymbolNumber({
|
||||
name: 'index',
|
||||
description: `Zero-based index of the array element to be returned, converted to an integer.
|
||||
Negative index counts back from the end of the array — if index < 0, index + array.length is accessed.`,
|
||||
}),
|
||||
],
|
||||
returns: SymbolUnion({
|
||||
description: `The element in the array matching the given index. Always returns undefined if index < -array.length or index >= array.length
|
||||
without attempting to access the corresponding property.`,
|
||||
members: [arraySymbolDef.items, SymbolUndefined()],
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'includes',
|
||||
description:
|
||||
'Determines whether an array includes a certain value among its entries, returning true or false as appropriate.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'searchElement',
|
||||
description: 'The value to be searched for within the array.',
|
||||
},
|
||||
OptionalFunctionArg(
|
||||
SymbolNumber({
|
||||
name: 'fromIndex',
|
||||
description:
|
||||
'The position within the string at which to begin searching for searchString. (Defaults to 0.)',
|
||||
})
|
||||
),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'some',
|
||||
description:
|
||||
'Tests whether at least one element in the array passes the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'every',
|
||||
description:
|
||||
'Tests whether all elements in the array pass the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
function isArraySymbol(symbol: GenericSymbolDef): symbol is ArraySymbolDef {
|
||||
return symbol.type === SymbolType.Array;
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import type { MandateProps } from '../utils';
|
||||
|
||||
export enum SymbolType {
|
||||
Boolean = 'boolean',
|
||||
Number = 'number',
|
||||
String = 'string',
|
||||
Object = 'object',
|
||||
Array = 'array',
|
||||
Function = 'function',
|
||||
Union = 'union',
|
||||
Undefined = 'undefined',
|
||||
Null = 'null',
|
||||
}
|
||||
|
||||
export interface SymbolMetadata {
|
||||
/**
|
||||
* Long description of the symbol.
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Link to a documentation/manual page.
|
||||
*/
|
||||
link?: string;
|
||||
}
|
||||
|
||||
export interface GenericSymbolDef extends SymbolMetadata {
|
||||
/**
|
||||
* Type of the symbol.
|
||||
*/
|
||||
type: SymbolType;
|
||||
|
||||
/**
|
||||
* Name of the symbol.
|
||||
*/
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SymbolWithProperties extends GenericSymbolDef {
|
||||
/**
|
||||
* Properties on the symbol type.
|
||||
*/
|
||||
properties: Record<string, GenericSymbolDef>;
|
||||
}
|
||||
|
||||
export interface SymbolWithMethods extends GenericSymbolDef {
|
||||
/**
|
||||
* Methods that can be called on the symbol type.
|
||||
*/
|
||||
methods: FunctionSymbolDef[];
|
||||
}
|
||||
|
||||
export interface BooleanSymbolDef extends GenericSymbolDef {
|
||||
type: SymbolType.Boolean;
|
||||
}
|
||||
|
||||
export interface NumberSymbolDef extends GenericSymbolDef {
|
||||
type: SymbolType.Number;
|
||||
}
|
||||
|
||||
export interface StringSymbolDef extends SymbolWithProperties, SymbolWithMethods {
|
||||
type: SymbolType.String;
|
||||
|
||||
/**
|
||||
* Set of enumerated values that the string symbol is retristred to.
|
||||
*/
|
||||
enum?: string[];
|
||||
|
||||
/**
|
||||
* Properties on strings.
|
||||
*/
|
||||
properties: {
|
||||
length: NumberSymbolDef;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ObjectSymbolDef extends SymbolWithProperties, SymbolWithMethods {
|
||||
type: SymbolType.Object;
|
||||
}
|
||||
|
||||
export interface ArraySymbolDef extends SymbolWithProperties, SymbolWithMethods {
|
||||
type: SymbolType.Array;
|
||||
|
||||
/**
|
||||
* Symbol representing the type of the items of the array
|
||||
*/
|
||||
items: ExtractSymbolDef<SymbolType>;
|
||||
|
||||
/**
|
||||
* Properties on arrays.
|
||||
*/
|
||||
properties: {
|
||||
length: NumberSymbolDef;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FunctionSymbolDef extends MandateProps<GenericSymbolDef, 'name'> {
|
||||
type: SymbolType.Function;
|
||||
|
||||
/**
|
||||
* Symbols describing the arguments of the function.
|
||||
*/
|
||||
args: (ExtractSymbolDef<SymbolType> & { optional?: boolean })[];
|
||||
|
||||
/**
|
||||
* Symbol describing the returned value of the function.
|
||||
*/
|
||||
returns: ExtractSymbolDef<SymbolType>;
|
||||
}
|
||||
|
||||
export interface UnionSymbolDef extends GenericSymbolDef {
|
||||
type: SymbolType.Union;
|
||||
|
||||
/**
|
||||
* Symbols composing the union.
|
||||
*/
|
||||
members: ExtractSymbolDef<SymbolType>[];
|
||||
}
|
||||
|
||||
export interface UndefinedSymbolDef extends GenericSymbolDef {
|
||||
type: SymbolType.Undefined;
|
||||
}
|
||||
|
||||
export interface NullSymbolDef extends GenericSymbolDef {
|
||||
type: SymbolType.Null;
|
||||
}
|
||||
|
||||
export type SymbolsWithPropertiesAndMethods = ArraySymbolDef | ObjectSymbolDef | StringSymbolDef;
|
||||
|
||||
export type ExtractSymbolDef<T extends SymbolType> = T extends SymbolType.String
|
||||
? StringSymbolDef
|
||||
: T extends SymbolType.Number
|
||||
? NumberSymbolDef
|
||||
: T extends SymbolType.Boolean
|
||||
? BooleanSymbolDef
|
||||
: T extends SymbolType.Array
|
||||
? ArraySymbolDef
|
||||
: T extends SymbolType.Object
|
||||
? ObjectSymbolDef
|
||||
: T extends SymbolType.Function
|
||||
? FunctionSymbolDef
|
||||
: T extends SymbolType.Union
|
||||
? UnionSymbolDef
|
||||
: T extends SymbolType.Undefined
|
||||
? UndefinedSymbolDef
|
||||
: T extends SymbolType.Null
|
||||
? NullSymbolDef
|
||||
: never;
|
||||
|
||||
export function resolveSymbolDef(
|
||||
symbol: GenericSymbolDef
|
||||
):
|
||||
| StringSymbolDef
|
||||
| NumberSymbolDef
|
||||
| BooleanSymbolDef
|
||||
| ArraySymbolDef
|
||||
| ObjectSymbolDef
|
||||
| FunctionSymbolDef
|
||||
| UnionSymbolDef
|
||||
| UndefinedSymbolDef
|
||||
| NullSymbolDef {
|
||||
switch (symbol.type) {
|
||||
case SymbolType.String:
|
||||
return symbol as StringSymbolDef;
|
||||
|
||||
case SymbolType.Number:
|
||||
return symbol as NumberSymbolDef;
|
||||
|
||||
case SymbolType.Boolean:
|
||||
return symbol as BooleanSymbolDef;
|
||||
|
||||
case SymbolType.Array:
|
||||
return symbol as ArraySymbolDef;
|
||||
|
||||
case SymbolType.Object:
|
||||
return symbol as ObjectSymbolDef;
|
||||
|
||||
case SymbolType.Function:
|
||||
return symbol as FunctionSymbolDef;
|
||||
|
||||
case SymbolType.Union:
|
||||
return symbol as UnionSymbolDef;
|
||||
|
||||
case SymbolType.Undefined:
|
||||
return symbol as UndefinedSymbolDef;
|
||||
|
||||
case SymbolType.Null:
|
||||
return symbol as NullSymbolDef;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown symbol type: ${symbol.type}`);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
export type TemplateText = {
|
||||
type: 'text';
|
||||
value: string;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
export type TemplateExpression = {
|
||||
type: 'expression';
|
||||
value: string;
|
||||
start: number; // Start index of the expression content (after `{{`)
|
||||
end: number; // End index of the expression content (before `}}`)
|
||||
};
|
||||
|
||||
export type TemplatePart = TemplateText | TemplateExpression;
|
||||
|
||||
/**
|
||||
* Parse a template string containing `{{ expression }}` placeholders.
|
||||
*/
|
||||
export function parseTemplate(template: string): TemplatePart[] {
|
||||
const parts: TemplatePart[] = [];
|
||||
const regex = /\{\{(.*?)\}\}/gs;
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of template.matchAll(regex)) {
|
||||
const matchStart = match.index ?? 0;
|
||||
const matchEnd = matchStart + match[0].length;
|
||||
|
||||
if (matchStart > lastIndex) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
value: template.slice(lastIndex, matchStart),
|
||||
start: lastIndex,
|
||||
end: matchStart,
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({
|
||||
type: 'expression',
|
||||
value: (match[1] ?? '').trim(),
|
||||
start: matchStart + 2,
|
||||
end: matchEnd - 2,
|
||||
});
|
||||
lastIndex = matchEnd;
|
||||
}
|
||||
|
||||
if (lastIndex < template.length) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
value: template.slice(lastIndex),
|
||||
start: lastIndex,
|
||||
end: template.length,
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import type { BinaryOperator, Expression, ExpressionStatement, LogicalOperator } from 'acorn';
|
||||
|
||||
import type { ArraySymbolDef, SymbolInfo, SymbolType } from './symbols';
|
||||
|
||||
export interface ExpressionGenerator {
|
||||
/**
|
||||
* Converts an ESTree compatible AST node into a string representing the corresponding expression.
|
||||
*/
|
||||
generate(node: Expression): string;
|
||||
}
|
||||
|
||||
export interface ExpressionParserResult {
|
||||
/**
|
||||
* The expression statement from the valid portion of the parsed expression.
|
||||
*
|
||||
* It is undefined when no valid expression statements could be found.
|
||||
*/
|
||||
result: Expression;
|
||||
|
||||
/**
|
||||
* The information of the invalid (non-expression) nodes found from the other portions of the parsed expression.
|
||||
*/
|
||||
invalidNodes: Array<ExpressionStatement>;
|
||||
}
|
||||
|
||||
export interface ExpressionAutocompleteResults {
|
||||
suggestions: AutocompleteSuggestions;
|
||||
}
|
||||
|
||||
type ConditionalOperator = '?';
|
||||
|
||||
export const SUPPORTED_BINARY_OPERATORS = [
|
||||
{
|
||||
operator: '==',
|
||||
description: 'Checks whether two values are equal.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality',
|
||||
},
|
||||
{
|
||||
operator: '!=',
|
||||
description: 'Checks whether two values are unequal.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality',
|
||||
},
|
||||
{
|
||||
operator: '===',
|
||||
description: 'Checks whether two values are equal (strict comparison).',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality',
|
||||
},
|
||||
{
|
||||
operator: '!==',
|
||||
description: 'Checks whether two values are unequal (strict comparison).',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_inequality',
|
||||
},
|
||||
{
|
||||
operator: '<',
|
||||
description: 'Checks if the left value is less than the right value.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than',
|
||||
},
|
||||
{
|
||||
operator: '<=',
|
||||
description: 'Checks if the left value is less than or equal to the right value.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than_or_equal',
|
||||
},
|
||||
{
|
||||
operator: '>',
|
||||
description: 'Checks if the left value is greater than the right value.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than',
|
||||
},
|
||||
{
|
||||
operator: '>=',
|
||||
description: 'Checks if the left value is greater than or equal to the right value.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than_or_equal',
|
||||
},
|
||||
{
|
||||
operator: 'in',
|
||||
description: 'Checks if a property exists in an object or if a value is in an array.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/in',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const SUPPORTED_LOGICAL_OPERATORS = [
|
||||
{
|
||||
operator: '&&',
|
||||
description: 'Logical AND operator; returns true if both operands are true.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND',
|
||||
},
|
||||
{
|
||||
operator: '||',
|
||||
description: 'Logical OR operator; returns true if at least one operand is true.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const SUPPORTED_CONDITIONAL_OPERATORS = [
|
||||
{
|
||||
operator: '?',
|
||||
description:
|
||||
'Conditional (ternary) operator; returns one of two values based on a condition.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator',
|
||||
},
|
||||
] as const;
|
||||
|
||||
type DirectLiteralValue = {
|
||||
kind: 'direct';
|
||||
} & (
|
||||
| {
|
||||
type: SymbolType.Boolean;
|
||||
data: boolean;
|
||||
}
|
||||
| {
|
||||
type: SymbolType.Number;
|
||||
data: number;
|
||||
}
|
||||
| {
|
||||
type: SymbolType.String;
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
type: SymbolType.Null;
|
||||
data: null;
|
||||
}
|
||||
);
|
||||
|
||||
type InArrayLiteralValue = {
|
||||
kind: 'in-array';
|
||||
srcSymbol: ArraySymbolDef;
|
||||
matchedLiteralString: string;
|
||||
};
|
||||
|
||||
export type DirectLiteralValueSuggestion = {
|
||||
type: 'literal-value';
|
||||
value: DirectLiteralValue;
|
||||
};
|
||||
|
||||
export type InArrayLiteralValueSuggestion = {
|
||||
type: 'literal-value';
|
||||
value: InArrayLiteralValue;
|
||||
};
|
||||
|
||||
export type AutocompleteLiteralValueSuggestion =
|
||||
| DirectLiteralValueSuggestion
|
||||
| InArrayLiteralValueSuggestion;
|
||||
|
||||
export interface AutocompleteOperatorSuggestion {
|
||||
type: 'operator';
|
||||
operator:
|
||||
| Extract<BinaryOperator, (typeof SUPPORTED_BINARY_OPERATORS)[number]['operator']>
|
||||
| Extract<LogicalOperator, (typeof SUPPORTED_LOGICAL_OPERATORS)[number]['operator']>
|
||||
| Extract<
|
||||
ConditionalOperator,
|
||||
(typeof SUPPORTED_CONDITIONAL_OPERATORS)[number]['operator']
|
||||
>;
|
||||
description: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface AutocompleteSymbolSuggestion {
|
||||
type: 'symbol';
|
||||
symbol: SymbolInfo;
|
||||
}
|
||||
|
||||
export type AutocompleteSuggestions = Array<
|
||||
| AutocompleteSymbolSuggestion
|
||||
| AutocompleteLiteralValueSuggestion
|
||||
| AutocompleteOperatorSuggestion
|
||||
>;
|
||||
|
||||
type LoggerFn = (message: string, ...args: any[]) => void;
|
||||
|
||||
/**
|
||||
* A logger that can be passed to the runtime.
|
||||
*/
|
||||
export interface Logger {
|
||||
debug: LoggerFn;
|
||||
info: LoggerFn;
|
||||
error: LoggerFn;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Format the result value of an expression for display as a string.
|
||||
*/
|
||||
export function formatExpressionResult(value: any, defaultValue = ''): string {
|
||||
if (value === undefined || value === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter function to exclude `null` values
|
||||
*/
|
||||
export function filterOutNullable<T>(value: T): value is NonNullable<T> {
|
||||
return !!value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type to make optional properties on a object mandatory.
|
||||
*
|
||||
* interface SomeObject {
|
||||
* uid: string;
|
||||
* price: number | null;
|
||||
* location?: string;
|
||||
* }
|
||||
*
|
||||
* type ValuableObject = MandateProps<SomeObject, 'price' | 'location'>;
|
||||
*/
|
||||
export type MandateProps<T extends {}, K extends keyof T> = T & {
|
||||
[MK in K]-?: NonNullable<T[MK]>;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node20/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["types/**/*.d.ts", "src/**/*.ts"]
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
declare module 'eval-estree-expression' {
|
||||
/**
|
||||
* Options for evaluation and compilation.
|
||||
*/
|
||||
export interface EvalESTreeExpressionOptions {
|
||||
/**
|
||||
* Force logical operators to return a boolean result. Default: undefined
|
||||
*/
|
||||
booleanLogicalOperators?: boolean;
|
||||
/**
|
||||
* Allow function calls to be evaluated. This is unsafe, please enable this option at your own risk. Default: false
|
||||
*/
|
||||
functions?: boolean;
|
||||
/**
|
||||
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
|
||||
*/
|
||||
generate?: boolean | ((node: any) => string);
|
||||
/**
|
||||
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
||||
*/
|
||||
regexOperator?: boolean;
|
||||
/**
|
||||
* Throw an error when variables are undefined. Default: false
|
||||
*/
|
||||
strict?: boolean;
|
||||
/**
|
||||
* Used with the variables method to return nested variables (e.g., variables with dot notation, like foo.bar.baz). Default: undefined
|
||||
*/
|
||||
withMembers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an ESTree expression asynchronously against a given context.
|
||||
* @param expression - An object representing an ESTree-compliant AST node.
|
||||
* @param context - An object containing variables and values to be used during evaluation.
|
||||
* @returns A promise resolving to the result of the evaluation.
|
||||
*/
|
||||
export function evaluate<ASTNode>(
|
||||
ast: ASTNode,
|
||||
context: object,
|
||||
options?: EvalESTreeExpressionOptions
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* Evaluates an ESTree expression synchronously against a given context.
|
||||
* @param expression - An object representing an ESTree-compliant AST node.
|
||||
* @param context - An object containing variables and values to be used during evaluation.
|
||||
* @returns The result of the evaluation.
|
||||
*/
|
||||
export namespace evaluate {
|
||||
function sync<ASTNode>(
|
||||
expression: ASTNode,
|
||||
context: object,
|
||||
options?: EvalESTreeExpressionOptions
|
||||
): any;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
dist/
|
||||
src/data/*.json
|
||||
@@ -1,19 +0,0 @@
|
||||
# @gitbook/fonts
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- fbfcca5: Initial version of the package
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@gitbook/fonts`
|
||||
|
||||
Utilities to lookup default fonts supported by GitBook.
|
||||
@@ -1,91 +0,0 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import type { FontDefinitions } from '../src/types';
|
||||
|
||||
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
|
||||
[CustomizationDefaultFont.Inter]: 'inter',
|
||||
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
|
||||
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
|
||||
[CustomizationDefaultFont.Lato]: 'lato',
|
||||
[CustomizationDefaultFont.Merriweather]: 'merriweather',
|
||||
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
|
||||
[CustomizationDefaultFont.OpenSans]: 'open-sans',
|
||||
[CustomizationDefaultFont.Overpass]: 'overpass',
|
||||
[CustomizationDefaultFont.Poppins]: 'poppins',
|
||||
[CustomizationDefaultFont.Raleway]: 'raleway',
|
||||
[CustomizationDefaultFont.Roboto]: 'roboto',
|
||||
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
|
||||
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
|
||||
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
|
||||
[CustomizationDefaultFont.ABCFavorit]: 'inter',
|
||||
};
|
||||
|
||||
/**
|
||||
* Scripts to generate the list of all icons.
|
||||
*/
|
||||
async function main() {
|
||||
// @ts-expect-error - we build the object
|
||||
const output: FontDefinitions = {};
|
||||
|
||||
for (const font of Object.values(CustomizationDefaultFont)) {
|
||||
const googleFontName = googleFontsMap[font];
|
||||
const fontMetadata = APIv2[googleFontName.toLowerCase()];
|
||||
if (!fontMetadata) {
|
||||
throw new Error(`Font ${googleFontName} not found`);
|
||||
}
|
||||
|
||||
output[font] = {
|
||||
font: googleFontName,
|
||||
unicodeRange: fontMetadata.unicodeRange,
|
||||
variants: {
|
||||
'400': {},
|
||||
'700': {},
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(output[font].variants).forEach((weight) => {
|
||||
const variants = fontMetadata.variants[weight];
|
||||
const normalVariant = variants.normal;
|
||||
if (!normalVariant) {
|
||||
throw new Error(`Font ${googleFontName} has no normal variant`);
|
||||
}
|
||||
|
||||
output[font].variants[weight] = {};
|
||||
Object.entries(normalVariant).forEach(([script, url]) => {
|
||||
output[font].variants[weight][script] = url.url.woff;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await writeDataFile('fonts', JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* We write both in dist and src as the build process might have happen already
|
||||
* and tsc doesn't copy the files.
|
||||
*/
|
||||
async function writeDataFile(name, content) {
|
||||
const srcData = path.resolve(__dirname, '../src/data');
|
||||
const distData = path.resolve(__dirname, '../dist/data');
|
||||
|
||||
// Ensure the directories exists
|
||||
await Promise.all([
|
||||
fs.mkdir(srcData, { recursive: true }),
|
||||
fs.mkdir(distData, { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
|
||||
fs.writeFile(path.resolve(distData, `${name}.json`), content),
|
||||
]);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`Error generating icons list: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "@gitbook/fonts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"google-font-metadata": "^6.0.7",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./bin/generate.js",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
||||
"unit": "bun test"
|
||||
},
|
||||
"files": ["dist", "bin", "README.md", "CHANGELOG.md"],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user