snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

5
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,5 @@
# Repository-wide ownership
* @ueberdosis/editor-maintainers
# LICENSE
LICENSE.md @philipisik @ueberdosis/editor-maintainers

View File

@@ -0,0 +1,51 @@
title: 'Community Extension: '
body:
- type: markdown
attributes:
value: |
Hey! Thanks for your time and effort to create a new community extension! Please make sure to fill out the form below.
- type: textarea
id: description
attributes:
label: Description
description: Please describe how your extension works and what it does.
placeholder: 'My extension does …'
validations:
required: true
- type: textarea
id: installation
attributes:
label: Installation
description: Please describe how users can install your extension.
placeholder: 'npm install …'
validations:
required: true
- type: textarea
id: usage
attributes:
label: Usage
description: Please describe how users can use your extension in their editor.
placeholder: 'To use my extension you have to …'
validations:
required: true
- type: dropdown
id: type
attributes:
label: Type
description: Please select the type of this extension.
options:
- 'Node'
- 'Mark'
- 'Prosemirror plugin'
- 'Package or Kit'
- 'Other'
validations:
required: true
- type: textarea
id: other
attributes:
label: Other
description: Feel free to add any other information about your extension.
placeholder: 'I hope you like …'
validations:
required: false

View File

@@ -0,0 +1,36 @@
title: 'Feature Request: '
labels:
- 'Type: Feature Request'
body:
- type: markdown
attributes:
value: |
Thanks for your time to create a new feature request! Please make sure to fill out the form below.
- type: textarea
id: description
attributes:
label: Description
description: Please describe the feature you would like to see in Tiptap.
placeholder: 'I wish there was an extension for …'
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use Case
description: Please describe the use case for this feature.
placeholder: 'I want to use this feature for …'
validations:
required: true
- type: dropdown
id: type
attributes:
label: Type
description: Please select the type of this feature.
options:
- 'New extension'
- 'New feature'
- 'New Tiptap API'
- 'Other'
validations:
required: true

View File

@@ -0,0 +1,52 @@
title: 'Community Extension: '
body:
- type: markdown
attributes:
value: |
Hey! Thanks for using Tiptap in your project. We hope you had a great experience. Please take a moment to share your project with us. We would love to see what you built with Tiptap.
- type: textarea
id: description
attributes:
label: Description
description: Please describe what your project is about
placeholder: 'My project is about …'
validations:
required: true
- type: input
id: url
attributes:
label: URL
description: If possible share the URL of your project.
placeholder: 'https://example.com'
validations:
required: false
- type: textarea
id: about
attributes:
label: About
description: Feel free to talk about how you used Tiptap in your project, what you liked about it, what you didn't like about it, and what you would like to see in the future.
placeholder: 'If used Tiptap to …'
validations:
required: true
- type: dropdown
id: type
attributes:
label: Type
description: Please select the type of your project.
options:
- 'Chat Application'
- 'Commenting Application'
- 'Content Management System'
- 'Document Editor'
- 'Document Editor with Collaboration'
- 'Other'
validations:
required: true
- type: textarea
id: other
attributes:
label: Other
description: Feel free to add any other information about your project.
placeholder: 'I hope you like …'
validations:
required: false

81
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Bug Report
description: Found a bug in the editor core or one of the extensions? Report it here to help us improve.
labels: ['status: triage']
type: 'Bug'
body:
- type: markdown
attributes:
value: |
Please use simple, human-written English. Keep your answers short and clear.
Small code examples that show the problem are welcome. Do not include a code fix or a complete implementation in an issue. If you already have a fix, please open a pull request instead.
- type: input
id: packages
attributes:
label: Affected Packages
description: List the Tiptap packages involved in the problem.
placeholder: core, extension-mention, react
validations:
required: true
- type: input
id: version
attributes:
label: Tiptap Version
description: Which Tiptap version or package versions are affected?
placeholder: 3.0.0
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser Used
description: Select the browser where you saw the problem.
options:
- Chrome
- Firefox
- Safari
- Edge
- Other
- Not browser-specific
validations:
required: true
- type: textarea
id: body
attributes:
label: What happened?
description: Describe what happens, when it happens, and why it is a problem. Use short sentences and simple English. Do not write a wall of text, use complicated language, or include a complete code fix.
placeholder: The issue happens when...
validations:
required: true
- type: textarea
id: expectation
attributes:
label: Expected Behavior
description: Describe what you expected to happen.
placeholder: I expected...
validations:
required: true
- type: markdown
attributes:
value: |
A small reproducible example is the best way to help us investigate the issue. Please add one if you can.
CodeSandbox templates:
* JavaScript: [JS Template](https://codesandbox.io/s/tiptap-js-fv1lyo)
* React: [React Template](https://codesandbox.io/s/tiptap-react-qidlsv)
* Vue 3: [Vue 3 Template](https://codesandbox.io/p/sandbox/tiptap-vue-3-ci7q9h)
- type: input
id: sandbox
attributes:
label: Reproducible Example URL (Optional)
description: Link a CodeSandbox, StackBlitz, GitHub repository, or similar example if you have one.
placeholder: https://codesandbox.io/s/example
- type: textarea
id: context
attributes:
label: Additional Context (Optional)
description: Add useful screenshots, videos, or other short details. Avoid long explanations.
- type: markdown
attributes:
value: 'Thank you for helping us improve our open-source projects by reporting this issue!'

View File

@@ -0,0 +1,81 @@
name: Bug Report (Tiptap Pro)
description: If you've encountered a bug with Tiptap Pro features, please report it here.
labels: ['tier: pro', 'status: triage']
type: 'Bug'
body:
- type: markdown
attributes:
value: |
Please use simple, human-written English. Keep your answers short and clear.
Small code examples that show the problem are welcome. Do not include a code fix or a complete implementation in an issue. If you already have a fix, please open a pull request instead.
- type: input
id: packages
attributes:
label: Affected Packages
description: List the Tiptap Pro packages involved in the problem.
placeholder: pro-extension, pro-collaboration
validations:
required: true
- type: input
id: version
attributes:
label: Tiptap Version
description: Which Tiptap or Tiptap Pro version or package versions are affected?
placeholder: 3.0.0
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser Used
description: Select the browser where you saw the problem.
options:
- Chrome
- Firefox
- Safari
- Edge
- Other
- Not browser-specific
validations:
required: true
- type: textarea
id: body
attributes:
label: What happened?
description: Describe what happens, when it happens, and why it is a problem. Use short sentences and simple English. Do not write a wall of text, use complicated language, or include a complete code fix.
placeholder: The issue happens when...
validations:
required: true
- type: textarea
id: expectation
attributes:
label: Expected Behavior
description: Describe what you expected to happen.
placeholder: I expected...
validations:
required: true
- type: markdown
attributes:
value: |
A small reproducible example is the best way to help us investigate the issue. Please add one if you can.
CodeSandbox templates:
- JavaScript: [Template](https://codesandbox.io/s/tiptap-js-fv1lyo)
- React: [Template](https://codesandbox.io/s/tiptap-react-qidlsv)
- Vue 3: [Template](https://codesandbox.io/p/sandbox/tiptap-vue-3-ci7q9h)
- type: input
id: sandbox
attributes:
label: Reproducible Example URL (Optional)
description: Link a CodeSandbox, StackBlitz, GitHub repository, or similar example if you have one.
placeholder: https://codesandbox.io/s/example
- type: textarea
id: context
attributes:
label: Additional Context (Optional)
description: Add useful screenshots, videos, or other short details. Avoid long explanations.
- type: markdown
attributes:
value: 'Thank you for contributing to Tiptap Pro by reporting this issue!'

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: New Feature Request
url: https://github.com/ueberdosis/tiptap/discussions/new?category=feature-requests
about: Interested in proposing a new feature for Tiptap? Submit your feature request here.
- name: Help & Support
url: https://github.com/ueberdosis/tiptap/discussions/new?category=questions-help
about: Require assistance or have inquiries about using Tiptap? Ask your questions here.
- name: Join our Discord
url: https://discord.gg/WtJ49jGshW
about: Interested in engaging with the Tiptap community? Join our Discord server.
- name: Present your project
url: https://github.com/ueberdosis/tiptap/discussions/new?category=showcase
about: Developed something impressive using Tiptap? Share your project with the community here.
- name: Present your Tiptap extensions
url: https://github.com/ueberdosis/tiptap/discussions/new?category=community-extensions
about: Created a Tiptap extension? Showcase your work to the community here.

View File

@@ -0,0 +1,45 @@
name: Documentation feedback
description: Share what we need to explain better.
title: '[Documentation]: '
labels:
- 'Type: Documentation'
- 'Category: Open Source'
- 'Status: New'
body:
- type: input
id: url
attributes:
label: What’s the URL to the page you’re sending feedback for?
placeholder: https://tiptap.dev/example
validations:
required: true
- type: textarea
id: part-of-the-documentation
attributes:
label: What part of the documentation needs improvement?
placeholder: 'I’ve read the following page of the documentation …'
validations:
required: true
- type: textarea
id: good-parts
attributes:
label: What is helpful about that part?
placeholder: 'I think this part is really good: …'
validations:
required: true
- type: textarea
id: bad-parts
attributes:
label: What is hard to understand, missing or misleading?
placeholder: 'But you really need to improve …'
validations:
required: true
- type: textarea
id: context
attributes:
label: Anything to add? (optional)
description: 'Add any other context or screenshots here.'
- type: markdown
attributes:
value: |
Thanks for taking the time to send us feedback!

View File

@@ -0,0 +1,9 @@
name: Restore dependencies
description: Install dependencies from the cached package manager data
runs:
using: composite
steps:
- name: Restore dependencies
shell: bash
run: vp install --offline --frozen-lockfile -- --strict-peer-dependencies

20
.github/actions/setup-env/action.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Setup Vite+ environment
description: Install Vite+ and set up Node.js and dependency caching
inputs:
node-version:
description: Node.js version
required: true
cache:
description: Whether to cache package manager data
default: 'false'
required: false
runs:
using: composite
steps:
- uses: voidzero-dev/setup-vp@v1.18.0
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.cache == 'true' && 'true' || 'false' }}
run-install: false

View File

@@ -0,0 +1,27 @@
name: Setup Playwright
description: Cache and install Playwright browsers with system dependencies
runs:
using: composite
steps:
- name: Get Playwright version
id: playwright-version
shell: bash
run: echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
shell: bash
run: vp exec playwright install --with-deps chromium
- name: Install Playwright system deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
shell: bash
run: vp exec playwright install-deps chromium

BIN
.github/assets/cover.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# Dependabot creates pull requests to keep your dependencies secure and up-to-date.
# Documentation: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
open-pull-requests-limit: 10
schedule:
interval: 'weekly'
day: 'monday'
reviewers:
- 'bdbch'

43
.github/instructions/PR.instructions.md vendored Normal file
View File

@@ -0,0 +1,43 @@
---
applyTo: '**'
---
When asked to write a pull request description, use the following template:
```
## Changes Overview
<!-- Briefly describe your changes. -->
## Implementation Approach
<!-- Describe your approach to implementing these changes. Keep it concise. -->
## Testing Done
<!-- Explain how you tested these changes. Link to test scenarios or specs if relevant. -->
## Verification Steps
<!-- Describe steps reviewers can take to verify the functionality of your changes. -->
## Additional Notes
<!-- Add any other notes or screenshots about the PR here. -->
## Checklist
- [ ] I have created a [changeset](https://github.com/changesets/changesets) for this PR if necessary.
- [ ] My changes do not break the library.
- [ ] I have added tests where applicable.
- [ ] I have followed the project guidelines.
- [ ] I have fixed any lint issues.
## Related Issues
<!-- Link any related issues here -->
```
When generating the pull request description, ensure it is clear, concise, and follows the provided template. Focus on the key aspects of the changes made, how they were implemented, and how they can be verified.
Make it as minimal as possible to convey the necessary information effectively without causing to much noise and making it hard to read.

View File

@@ -0,0 +1,9 @@
---
applyTo: '**'
---
When a user asks for a changeset to be generated, follow the following rules:
Create a good changeset file for the changes in the diff. Don't include non-frontfacing changes, as the changeset file will be used for the changelog. Our users don't care about deep logic that they'll not interact with so we only want to generate changelog entries for front-facing/user-facing changes and API changes our users will need to know about.
Make sure that the changeset file stays minimal and short but includes important information that may be important for our users to understand what actually changed.

View File

@@ -0,0 +1,261 @@
---
applyTo: '**'
---
# Tiptap
This document explains how to work on the Tiptap monorepo in VS Code. It covers repo layout, local dev, linting and formatting, tests, docs, and release workflow. It is written to be friendly for both humans and AI coding assistants.
---
## What is Tiptap
Tiptap is a headless rich text editor toolkit built on ProseMirror. It ships a small Core and many opt-in Extensions so you can compose exactly the editor you need for React, Vue, or vanilla apps. The project is optimized for user experience and developer experience. APIs are predictable, behavior is testable, and everything should be documented with JSDoc and runnable examples so we can generate API docs automatically.
Key points for AI assistants:
- Treat Tiptap as a collection of focused packages that together form an editor system.
- Do not assume a single framework. Many packages are framework agnostic, with separate bindings for React and Vue.
- Favor small pure utilities and deterministic code. Side effects should be explicit.
---
## Repository layout
```
.
├─ packages/ # Core and all first-party extensions
│ ├─ core/ # Editor core (@tiptap/core)
│ ├─ extension-*/ # Individual extensions
│ ├─ pm/ # ProseMirror related internals and helpers
│ └─ ... # Shared utilities, framework bindings, etc.
├─ demos/ # Vite app for live examples and colocated e2e specs
│ ├─ src/
│ │ ├─ react/ # React demos
│ │ └─ vue/ # Vue demos
│ └─ test/ # Playwright helpers (getEditor, setEditorContent, ...)
├─ .changeset/ # Changesets for versioning and changelogs
└─ .github/ # Workflows and docs like this file
```
Notes:
- All packages we publish or use live under `packages/*`.
- The `demos/` folder contains a Vite app. It automatically discovers and parses React and Vue demos so they appear in the UI without manual wiring.
- Playwright e2e specs live alongside their demos as `demos/src/**/index.spec.ts`. `playwright.config.ts` auto-starts the Vite dev server on `http://127.0.0.1:4080` — no need to launch it manually.
## NPM scripts
Scripts defined at the repo root. Run them with `vp run <script>` (or `pnpm run <script>`):
- `vp run dev` - start the demos on port 3000
- `vp run build` - build all packages via Vite+ (`vp run -r build`)
- `vp run check` - run format check + lint
- `vp run check:fix` - run format:fix + lint:fix
- `vp run format` - run oxfmt formatter check
- `vp run format:fix` - run oxfmt formatter
- `vp run lint` - run oxlint checks
- `vp run lint:fix` - run oxlint with auto-fix
- `vp run lint:staged` - run Vite+ staged checks on staged files
- `vp run test:e2e` - run Playwright e2e tests headlessly in Chromium
- `vp run test:e2e:firefox` - same, in Firefox
- `vp run test:e2e:all` - same, in both browsers
- `vp run test:e2e:open` - run Playwright in UI mode (Chromium tests)
- `vp run test:e2e:open:firefox` - UI mode, Firefox tests
- `vp run test:e2e:open:all` - UI mode, both browsers selectable
- `vp run test:e2e:report` - open the HTML report from the last run
- `vp run test:unit` - run Vitest unit tests
- `vp run test` - build then run all tests
- `vp run serve` - build and serve the demos on port 3000
- `vp run publish` - build and publish with Changesets
- `vp run reset` - remove caches, build artifacts, and reinstall deps
Scripts are cached by Vite Task (`run.cache.scripts` is enabled in `vite.config.mts`). Re-running an unchanged script replays its output instead of executing. Persistent scripts like `dev` never exit, so they are never cached. Clear the cache with `vp cache clean`.
---
## Linting & formatting
- Lint and format settings are in `vite.config.mts`.
- Vite+ hooks (`vp staged`) run automatically on commits.
Use `vp run lint` / `vp run format`, or the equivalent `pnpm run` commands.
Run manually:
```bash
vp run lint
vp run lint:fix
vp run format
vp run format:fix
vp run check
vp run check:fix
```
---
## Demos
- Demos are a Vite app in `demos/`.
- React and Vue examples live in `demos/react` and `demos/vue`. They are automatically parsed into the app.
- Start in dev mode:
```bash
vp run dev
```
- Build static output and serve locally:
```bash
vp run serve
```
When adding a demo, keep it small and self-contained, with imports from published package names (`@tiptap/...`).
---
## Testing
Two layers:
- **Unit tests** with Vitest (happy-dom). Place file-specific tests next to the file they cover and name them `<basename>.spec.ts` (for example, `getAttributes.ts` uses `getAttributes.spec.ts`). Each package keeps a `tests/` folder next to `src/` for package-wide test support. Put reusable package test utilities in `tests/utils/`, scenario or reproduction data in `tests/fixtures/`, and file-based fixtures in `tests/fixtures/files/`. Add shared package setup in `tests/setup/` only when multiple specs need it. Keep one-off mocks and helpers next to the spec that uses them.
- **E2E tests** with Playwright, colocated next to their demos as `demos/src/**/index.spec.ts`. They drive the real Vite-served demo pages in Chromium.
Run them:
```bash
vp run test:unit # Vitest
vp run test:e2e # Playwright headless (Chromium)
vp run test:e2e:firefox # Playwright headless (Firefox)
vp run test:e2e:all # both browsers — every test twice
vp run test:e2e:open # UI mode (Chromium tests)
vp run test:e2e:open:firefox # UI mode (Firefox tests)
vp run test:e2e:open:all # UI mode, switch between browsers in the project picker
vp run test:e2e:report # open the HTML report from the last run
```
Playwright auto-starts the demo dev server (`pnpm -C demos run start:e2e` on port 4080) via `playwright.config.ts` — no separate terminal needed. Shared helpers live in `demos/test/helpers.ts`: `getEditor`, `setEditorContent`, `clickButton`. Use `demos/src/Commands/Cut/index.spec.ts` as a canonical template when adding new specs.
Browser setup:
- CI installs Chromium only (cached between runs) and only runs the Chromium project.
- For local Firefox testing, install it once with `vp exec playwright install firefox` (~80MB).
- UI mode (`--ui`) always opens its host window in Chromium — that's the Playwright UI app itself, not the browser running your tests. Tests still execute in the project you selected (check the trace metadata or `browserName` fixture if you need to confirm).
---
## Documentation style
We focus heavily on **User Experience** and **Developer Experience**. Every public API must be documented with JSDoc, including:
- `@param` and `@returns` annotations
- Argument descriptions
- At least one runnable example
This ensures our automated API docs are complete and examples are usable without extra context.
Example:
````ts
/**
* Toggle bold mark on the current selection.
*
* Example
* ```ts
* editor.chain().focus().toggleBold().run()
* ```
*
* @param editor - The editor instance
* @returns true if the command was applied
*/
export function toggleBold(editor: Editor): boolean {
// ...
}
````
---
## Versioning and releases with Changesets
- Run `pnpm changeset` to create a new changeset (choose packages + bump type).
- Run `vp run version` to update versions and changelogs.
- Publishing happens automatically via the Publish CI workflow on configured branches. See `agents/VERSIONING.md` for details.
Changelogs must describe **user-facing changes**. Avoid internal noise.
---
## Cleaning and resetting
- `vp run clean:packages` - remove build artifacts
- `vp run clean:packs` - remove generated tarballs
- `vp run reset` - full reset of caches, node_modules, and lockfiles
---
## Principles
- Keep packages modular and framework-agnostic where possible.
- Breaking changes require a major bump and a clear migration path.
- Always add or update demos and tests when introducing a feature.
- Code should be deterministic, documented, and tested.
---
## Extra guidance (short additions)
To make these instructions easier for automated agents and new contributors, the sections below add a few operational details and guardrails that speed up safe, repeatable changes.
### Environment
- Recommended Node version: >=18.x. Use a node version manager (nvm, fnm) or Corepack to pin a runtime.
- Recommended package manager: pnpm (use the repo's lockfile). If you see unexpected errors, run `vp run reset`.
### Where to edit packages
Packages live under `packages/*`. Public entry points are typically `packages/<name>/src/index.ts` and are referenced by the package's `package.json` (`main`/`module`/`exports`). Prefer editing `src/` files and keep package diffs focused. For framework bindings check `packages/react/` or `packages/vue/`.
### Demos auto-discovery rules
The demos app discovers examples automatically. When adding a demo:
- Keep demo files small and self-contained. Import from published package names (for example `@tiptap/extension-foo`).
- Name demo files clearly; follow existing naming conventions in `demos/`.
### Validation checklist (run locally before opening a PR)
Run the following to validate changes quickly:
```bash
vp run lint
vp run build
vp run test:unit # Vitest
vp run test:e2e # Playwright (auto-starts the demo server)
vp run dev # optionally run the demos and open http://localhost:3000
```
If a single package is failing types, run a targeted build for that package (e.g. `vp run -F @tiptap/core build`), or run `vp run build` at the repo root.
### PR checklist
- All checks pass (lint/build/tests).
- Changeset added for user-facing changes (`pnpm changeset`).
- Demo added/updated for UI-visible changes.
- Short, clear PR description and changelog entry that explains why the change is needed.
### Guidance for automated agents and AI assistants
- Keep PR descriptions short, clear, and easy to read. Use simple English and explain why the change is needed. Avoid large text blocks and complicated language.
- Make single-purpose, small diffs. Avoid sweeping changes in one PR.
- Always run the validation checklist above after edits.
- Add or update a demo and tests for user-visible behavior. For deterministic behaviour, favour unit tests over fragile e2e tests where possible.
- Add a Changeset for any user-facing change. Do not change public APIs without a major bump; document migration steps in the PR description.
### Troubleshooting notes
- If CI fails with dependency or lockfile errors, run `vp run reset` locally and re-run the build.
- For flaky Playwright tests, reproduce locally with `vp run test:e2e:open` (UI mode) or rerun with `--trace on` and inspect via `vp run test:e2e:report`.
---
These additions are intentionally short so they are easy to follow and scriptable by tools and agents. If you'd like, I can apply a slightly different tone or expand any section into more detail (for example, exact node/pnpm version pinning or demo naming patterns).

10
.github/publish-config.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"branches": {
"main": {
"distTag": "next",
"label": "next",
"title": "Release new pre release",
"commit": "chore(release): release new pre release"
}
}
}

21
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,21 @@
## Fixes
<!-- Use GitHub keywords to link the issue or issues fixed by this PR, for example: Fixes #123 or Closes #456. -->
## Changes and Review
<!-- Explain what changed, why it changed, and how reviewers can verify it. -->
<!-- Keep this section short: use 2–4 short sentences or a few bullets. Do not write a technical deep dive, a full implementation explanation, or a large wall of text. -->
## Checklist
- [ ] I have added a [changeset](https://github.com/changesets/changesets) if necessary.
- [ ] I have added tests if possible.
- [ ] I have made sure to test my changes myself.
- [ ] This is a critical bug or security fix that also needs to land on the current stable release (see [Branching](../CONTRIBUTING.md#branching) in CONTRIBUTING.md).
### Responsibility
<!-- This checkbox is checked by default on purpose. The PR author is responsible for reviewing and understanding the changes, even when an AI agent created them. -->
- [x] I have reviewed and understand these changes, and I take responsibility for this PR, even if an AI agent created it.

View File

@@ -0,0 +1,128 @@
import { describe, it, before, after } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { resolveConfig } from '../resolve-publish-config.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ACTUAL_CONFIG_PATH = join(__dirname, '..', '..', 'publish-config.json')
let tmpDir
let configPath
before(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'publish-config-test-'))
configPath = join(tmpDir, 'publish-config.json')
})
after(() => {
rmSync(tmpDir, { recursive: true, force: true })
})
function writeConfig(data) {
writeFileSync(configPath, JSON.stringify(data, null, 2))
}
describe('real publish-config.json', () => {
let actualConfig
before(() => {
const raw = readFileSync(ACTUAL_CONFIG_PATH, 'utf8')
actualConfig = JSON.parse(raw)
})
it('is valid JSON and contains a branches object', () => {
assert.ok(actualConfig.branches, 'missing branches object')
assert.ok(typeof actualConfig.branches === 'object', 'branches must be an object')
})
it('has at least one configured branch', () => {
const names = Object.keys(actualConfig.branches)
assert.ok(names.length > 0, 'publish-config.json has no branches configured')
})
it('every branch entry has all required string fields', () => {
const required = ['distTag', 'label', 'title', 'commit']
for (const [name, entry] of Object.entries(actualConfig.branches)) {
for (const field of required) {
assert.ok(
entry[field] && typeof entry[field] === 'string',
`Branch "${name}" is missing required string field "${field}"`,
)
}
}
})
it('resolves each configured branch through the resolver', () => {
for (const name of Object.keys(actualConfig.branches)) {
const result = resolveConfig(name, ACTUAL_CONFIG_PATH)
assert.equal(result.configured, true, `Branch "${name}" should resolve as configured`)
assert.equal(result.distTag, actualConfig.branches[name].distTag)
assert.equal(result.label, actualConfig.branches[name].label)
assert.equal(result.title, actualConfig.branches[name].title)
assert.equal(result.commit, actualConfig.branches[name].commit)
}
})
})
describe('unknown branches', () => {
it('returns configured=false for a branch with no entry', () => {
writeConfig({
branches: { known: { distTag: 'x', label: 'stable', title: 'T', commit: 'c' } },
})
const result = resolveConfig('unknown', configPath)
assert.equal(result.configured, false)
})
it('does not match partial branch names', () => {
writeConfig({
branches: { main: { distTag: 'x', label: 'stable', title: 'T', commit: 'c' } },
})
const result = resolveConfig('main-foo', configPath)
assert.equal(result.configured, false)
})
it('returns only configured=false for an unknown branch', () => {
writeConfig({ branches: {} })
const result = resolveConfig('anything', configPath)
assert.deepEqual(result, { configured: false })
})
})
describe('validation errors', () => {
it('fails when a required field is missing', () => {
writeConfig({ branches: { main: { distTag: 'latest', label: 'stable' } } })
assert.throws(() => resolveConfig('main', configPath), /missing required/i)
})
it('fails when a required field is not a string', () => {
writeConfig({
branches: { main: { distTag: 'latest', label: 123, title: null, commit: 'ok' } },
})
assert.throws(() => resolveConfig('main', configPath), /missing required/i)
})
it('fails when config has no branches object', () => {
writeConfig({})
assert.throws(() => resolveConfig('main', configPath), /branches/)
})
it('fails when branches is an array instead of an object', () => {
writeConfig({ branches: [] })
assert.throws(() => resolveConfig('any', configPath), /branches/)
})
it('fails when config file is malformed JSON', () => {
writeFileSync(configPath, 'not-json')
assert.throws(() => resolveConfig('main', configPath), /Failed to read or parse/)
})
it('fails when config file does not exist', () => {
assert.throws(
() => resolveConfig('main', join(tmpDir, 'nonexistent.json')),
/Failed to read or parse/,
)
})
})

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs'
import { join, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const DEFAULT_CONFIG_PATH = join(__dirname, '..', 'publish-config.json')
const REQUIRED_FIELDS = ['distTag', 'label', 'title', 'commit']
/** @typedef {{ distTag: string, label: string, title: string, commit: string }} BranchEntry */
/**
* Reads and parses the publish config JSON file.
* @param {string} configPath
* @returns {object}
*/
function readConfig(configPath) {
try {
return JSON.parse(readFileSync(configPath, 'utf8'))
} catch (err) {
throw new Error(`Failed to read or parse ${configPath}: ${err.message}`)
}
}
/**
* Validates that the config has a "branches" object.
* @param {object} config
*/
function requireBranchesObject(config) {
if (!config.branches || typeof config.branches !== 'object' || Array.isArray(config.branches)) {
throw new Error('publish-config.json must contain a "branches" object')
}
}
/**
* Validates that a branch entry has all required string fields.
* @param {string} branch
* @param {BranchEntry} entry
*/
function requireValidEntry(branch, entry) {
for (const field of REQUIRED_FIELDS) {
if (!entry[field] || typeof entry[field] !== 'string') {
throw new Error(
`Branch "${branch}" is missing required string field "${field}" in publish-config.json`,
)
}
}
}
/**
* Looks up a branch in the publish config and returns its release settings.
*
* @param {string} branch - Branch name to look up (exact match).
* @param {string} [configPath] - Path to publish-config.json. Defaults to the
* canonical location next to the scripts directory.
* @returns {{ configured: false } | { configured: true, distTag: string, label: string, title: string, commit: string }}
* @throws {Error} When the config cannot be read/parsed or a required field is missing.
*/
export function resolveConfig(branch, configPath = DEFAULT_CONFIG_PATH) {
const config = readConfig(configPath)
requireBranchesObject(config)
const entry = config.branches[branch]
if (!entry) {
return { configured: false }
}
requireValidEntry(branch, entry)
return {
configured: true,
distTag: entry.distTag,
label: entry.label,
title: entry.title,
commit: entry.commit,
}
}
// ── CLI entry point ───────────────────────────────────────────
// When run as `node resolve-publish-config.mjs <branch>`, emits
// key=value lines suitable for GitHub Actions step outputs.
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const branch = process.argv[2]
if (!branch) {
console.error('Usage: node resolve-publish-config.mjs <branch-name> [config-path]')
process.exit(1)
}
const configPath = process.argv[3] || DEFAULT_CONFIG_PATH
try {
const result = resolveConfig(branch, configPath)
for (const [key, value] of Object.entries(result)) {
// Convert camelCase to snake_case for GitHub Actions output keys
const actionsKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
console.log(`${actionsKey}=${value}`)
}
} catch (err) {
console.error(err.message)
process.exit(1)
}
}

167
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,167 @@
name: Verify
env:
NODE_VERSION: 24
VP_GIT_HOOKS: 0
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
- next
- release/*
pull_request:
branches:
- main
- next
- release/*
jobs:
install-node-dependencies:
name: Install node dependencies
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- name: Install dependencies
run: vp install --frozen-lockfile -- --strict-peer-dependencies
build-packages:
name: Build packages
runs-on: ubuntu-latest
needs: install-node-dependencies
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- uses: ./.github/actions/restore-dependencies
- name: Build packages
run: vp run build && vp run build:demos
- name: Check package exports
run: vp run check:package-exports
check-linting-formatting:
name: Check linting & formatting
runs-on: ubuntu-latest
needs: install-node-dependencies
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- uses: ./.github/actions/restore-dependencies
- name: Run linting and formatting checks
run: vp check
run-unit-tests:
name: Run unit tests
runs-on: ubuntu-latest
needs: install-node-dependencies
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- uses: ./.github/actions/restore-dependencies
- name: Run unit tests
run: vp test
run-e2e-tests:
name: Run e2e tests (shard ${{ matrix.shard }}/${{ matrix.total }})
runs-on: ubuntu-latest
needs: build-packages
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
total: [4]
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- uses: ./.github/actions/restore-dependencies
- uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: vp exec playwright test --project=chromium --shard=${{ matrix.shard }}/${{ matrix.total }}
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v7
with:
name: e2e-blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 7
merge-e2e-reports:
name: Merge e2e reports
runs-on: ubuntu-latest
needs: [install-node-dependencies, run-e2e-tests]
if: always() && needs.install-node-dependencies.result == 'success'
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- uses: ./.github/actions/restore-dependencies
- name: Download blob reports
uses: actions/download-artifact@v8
with:
path: all-blob-reports
pattern: e2e-blob-report-*
merge-multiple: true
- name: Merge into HTML report
if: ${{ hashFiles('all-blob-reports/**') != '' }}
run: vp exec playwright merge-reports --reporter html ./all-blob-reports
- name: Upload merged HTML report
uses: actions/upload-artifact@v7
with:
name: playwright-html-report
path: playwright-report
retention-days: 14

40
.github/workflows/notify.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Notify
on:
release:
types: [published]
jobs:
discord_notify:
name: Send release to Discord
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Post release to Discord (tsickert/discord-webhook)
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.DISCORD_RELEASES_STABLE_WEBHOOK }}
embed-title: ${{ github.event.release.name || github.event.release.tag_name }}
embed-description: ${{ github.event.release.body }}
embed-url: ${{ github.event.release.html_url }}
wait: true
slack_release_notify:
name: Send release to Slack
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Send Slack release notification
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
{
"message": ${{ toJSON(format('[Tiptap Editor {0}]: {1}\nRelease title: {2}\n{3}', github.event.release.prerelease && 'Pre-release' || 'Release', github.event.release.name || github.event.release.tag_name, github.event.release.html_url, github.event.release.body || 'No release notes provided.')) }}
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

165
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,165 @@
name: Publish
env:
NODE_VERSION: 24
VP_GIT_HOOKS: 0
# this branch only releases from main
# other versions are released from their respective release/* branches
on:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs:
resolve-config:
name: Resolve publish config
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
configured: ${{ steps.resolve.outputs.configured }}
dist_tag: ${{ steps.resolve.outputs.dist_tag }}
label: ${{ steps.resolve.outputs.label }}
title: ${{ steps.resolve.outputs.title }}
commit: ${{ steps.resolve.outputs.commit }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
- name: Validate publish config
run: node --test .github/scripts/__tests__/*.test.mjs
- name: Look up branch in publish config
id: resolve
run: node .github/scripts/resolve-publish-config.mjs "${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 30
needs: resolve-config
if: needs.resolve-config.outputs.configured == 'true'
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- name: Install dependencies
run: vp install --frozen-lockfile -- --strict-peer-dependencies
- name: Run build
run: vp run build && vp run build:demos
- name: Check package exports
run: vp run check:package-exports
- name: Archive package dist artifacts
run: tar -cf package-dist-artifacts.tar packages/*/dist
- name: Upload package dist artifacts
uses: actions/upload-artifact@v7
with:
name: package-dist-artifacts
if-no-files-found: error
path: package-dist-artifacts.tar
release:
name: Bump Versions OR Release to Registry
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [resolve-config, build]
permissions:
id-token: write
contents: write
pull-requests: write
outputs:
dist_tag: ${{ needs.resolve-config.outputs.dist_tag }}
label: ${{ needs.resolve-config.outputs.label }}
published: ${{ steps.changesets.outputs.published }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-env
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'true'
- name: Update npm for trusted publishing
run: npm install -g npm@11.6.2
- name: Install dependencies
run: vp install --frozen-lockfile -- --strict-peer-dependencies
- name: Download package dist artifacts
uses: actions/download-artifact@v8
with:
name: package-dist-artifacts
path: .
- name: Restore package dist artifacts
run: tar -xf package-dist-artifacts.tar
- name: Verify package dist artifacts
run: bash ./scripts/check-package-dists.sh && vp run check:package-exports
- name: Run changesets release flow
id: changesets
uses: changesets/action@v1
with:
createGithubReleases: false
version: vp run version
publish: vp exec changeset publish
title: ${{ needs.resolve-config.outputs.title }}
commit: ${{ needs.resolve-config.outputs.commit }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
NPM_CONFIG_ACCESS: public
notify-slack:
name: Send notification to Slack
runs-on: ubuntu-latest
needs: release
if: needs.release.outputs.published == 'true'
timeout-minutes: 5
permissions: {}
steps:
- name: Send Slack notification
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
{
"message": "[Tiptap Editor ${{ needs.release.outputs.label }}]: Published packages from branch ${{ github.ref_name }} with npm tag ${{ needs.release.outputs.dist_tag }}."
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
notify-slack-failure:
name: Send failure notification to Slack
runs-on: ubuntu-latest
needs: [resolve-config, release]
if: always() && (needs.resolve-config.result == 'failure' || needs.release.result == 'failure')
timeout-minutes: 5
permissions: {}
steps:
- name: Send Slack failure notification
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
{
"message": "[Tiptap Editor Release]: There was an issue publishing packages from branch ${{ github.ref_name }}. Logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}