This monorepo contains packages for various Scripture editors.
Each Scripture application's editor will have many behaviors in common with other Scripture applications. Each will need some features that are unique. We are developing all of these parts in a compatible way and in one place in order to maximize collaboration and sharing.
In this monorepo:
- Each application produces their own editor package - the application uses the package produced from this repo because the source is there.
- Common nodes, plugins, and formatters for the toolbar plugin.
- Specific sets of nodes, plugins, and formatters for each data type extending from the common items where applicable.
Sharing in this monorepo is a commitment to maintain and organize it. Each application package is free to move in its own direction but keeping in mind items that can be pushed up outside the specific editor package to be used in common.
This repository is maintained by the Paratext 10 Studio team at
paranext/scripture-editors, and it is the source of truth for the editor packages. Day-to-day
development happens here.
It began as a copy of eten-tech-foundation/scripture-editors, carrying that repo's
full history, its MIT license, and its attribution. It was seeded with a curated set of refs —
main, platform-yalc, release-prep, and the release tags — so if you need a branch that was left
behind, see Transferring work from the eten-tech-foundation
repository. It is not a GitHub fork: while a repository
is a fork, GitHub always defaults new pull requests to the upstream repository and offers no setting
to change that, which makes it far too easy to open a PR against the wrong repo. Everything a fork
would give us still works through an ordinary git remote — see
Working with the eten-tech-foundation repository.
Platform.Bible consumes two of this repo's packages —
@eten-tech-foundation/platform-editor and @eten-tech-foundation/scripture-utilities — but not
from the npm registry. Its preinstall step clones this repo (or finds an existing checkout),
builds those two packages, and stages a copy of each into paranext-core/dev-packages/staging/,
which its package.json files then reference with file: specifiers.
Because the build is committed, that copy step needs nothing from this repo's toolchain.
The practical consequence for anyone working here: this repo's package.json files are
authoritative for its own dependencies. Adding, bumping, or removing a dependency here flows into
paranext-core on its next npm install, with nothing to restate on the consuming side. See
paranext-core's README for the consuming half, including how to get
your changes here into a running Platform.Bible.
Which revision paranext-core builds is pinned in its dev-packages.json, normally the
platform-yalc branch.
- Install Volta.
- Clone the monorepo:
git clone https://github.com/paranext/scripture-editors.git cd scripture-editors pnpm install - Install Nx globally (note we intentionally use
npmrather thanpnpmfor global installs, see JavaScript Tool Manager):npm i -g nx
- Run one of the top level developer environments (see the Nx Graph below), e.g.:
nx dev perf-react
You can use Volta with this repo to use the right version of tools such as Node.js and PNPM.
If you don't use Volta just look at the volta property in package.json to see the right tool versions to install in your preferred way.
NOTE: there is a known limitation using PNPM with Volta. So set your environment variable VOLTA_FEATURE_PNPM to 1. Also to install packages globally, use NPM instead of PNPM (only for global installs). For an example, see step 2 of Developer Quick Start.
| Source | Demo App |
|---|---|
| perf-vanilla | nx dev perf-vanilla |
| perf-react | nx dev perf-react |
| platform | nx dev platform |
| scribe-editor* | nx dev scribe-editor |
* This item appears in 2 lists as it has both the Demo App and Package in the same source folder.
| Source | Package |
|---|---|
| platform-editor | |
| scribe-editor | |
| utilities |
* This package has not yet been published to the new NPM organization. The previous version of scribe-editor is here .
| Source | Library |
|---|---|
| shared-react | internal non-published |
| shared | internal non-published |
| test-data | internal non-published |
---
title: Nx Graph
---
graph TB
V(perf-vanilla) --> T(test-data)
V --> S(shared)
S --> T
S --> U(utilities)
R(perf-react) --> T
R --> S
R --> SR(shared-react)
SR --> S
SR --> U
SB(scribe-editor) --> SR
SB --> S
SB --> U
P(platform) --> PE(platform-editor)
P --> T
P --> U
PE --> SR
PE --> S
PE --> U
✨ This workspace has been generated by Nx, a Smart, fast and extensible build system. ✨
If you haven't installed Nx globally (as recommended in step 2 of Developer Quick Start), then just prefix each call to nx with pnpm, e.g. pnpm nx build perf-react.
To execute tasks with Nx use the following syntax:
nx <target> <project> <...options>
# e.g.
nx build perf-reactYou can also run multiple targets:
nx run-many -t <target1> <target2>..or add -p to filter specific projects
nx run-many -t <target1> <target2> -p <proj1> <proj2>Targets can be defined in the package.json or projects.json. Learn more in the docs.
If you happen to use Nx plugins, you can leverage code generators that might come with it.
Run nx list to get a list of available plugins and whether they have generators. Then run nx list <plugin-name> to see what generators are available.
Learn more about Nx generators on the docs.
Have a look at the Nx Console extensions. It provides autocomplete support, a UI for exploring and running tasks & generators, and more! Available for VSCode, IntelliJ and comes with a LSP for Vim users.
Just run nx build perf-react to build that application. The build artifacts will be stored in the dist/ directory, ready to be deployed.
Nx comes with local caching already built-in (check your nx.json). On CI you might want to go a step further.
The unit tests run automatically on each GitHub PR (see test.yml).
To run all TS unit tests:
nx run-many -t testTo run all TS unit tests for a single package (in this example the shared package):
nx test sharedTo run all TS unit tests watching for file changes:
- On Windows:
nx watch --all -- nx test %NX_PROJECT_NAME% - On Linux or macOS:
nx watch --all -- nx test \$NX_PROJECT_NAME
You can also use the recommended VS Code extensions to run tests there. This is particularly useful for running individual tests and debugging.
If you change the public API of a package, run nx extract-api to update its API report:
nx extract-api <package-name> # e.g., nx extract-api platform-editorOr update all packages at once:
nx run-many -t extract-apiThe generated API report files should be committed alongside your changes.
Formatting happens automatically when you commit — the pre-commit hook runs Prettier over your staged files, and that is the gate. If you use VS Code with this repo's recommended extensions, files will also be formatted when you save. You shouldn't normally need to format anything by hand.
To check TypeScript for readability, maintainability, and functionality errors, run the following from the repo root (or just use VS Code with this repo's recommended extensions).
nx run-many -t lint # to check linting
nx run-many -t typecheck # to check types
npx prettier --write <files> # only if you need to format something by handUse Prettier directly rather than nx format:write. CI runs nx format:check purely as a backstop for commits that bypassed the hook; it exists because nx format once resolved a Prettier configuration that didn't quite match invoking Prettier directly. Several Nx majors have passed and that may well be fixed, but it has never been reconfirmed — so prefer the tool the hook uses.
Note .prettierignore excludes some files that still get staged, notably pnpm-lock.yaml and **/tsconfig*.json. Prettier leaves those alone by design; that isn't the hook failing.
Dependabot is configured to open security PRs only, so any dependabot PR on this repo is an advisory rather than a routine bump. We supersede those PRs with a single hand-made batch commit, because dependabot resolves pnpm monorepo trees poorly and because a version bump often fails to clear the advisory on its own.
See docs/dependency-updates.md for the procedure, how to write the
pnpm-workspace.yaml overrides, and why a green CI run is not evidence that a vulnerability is
fixed. Claude Code users can invoke the
update-dependencies skill, which follows that doc.
This repo gives Claude Code a TypeScript language server (jump-to-definition, find-references, and
type-error diagnostics across the monorepo). It works on Windows, macOS, and Linux with no manual
setup beyond pnpm install.
typescript-language-server is a repo devDependency, so pnpm install provides it (and the
typescript it needs is already in the workspace). A committed Claude Code plugin
(.claude/skills/typescript-lsp-volta) launches it and
loads automatically when you start Claude Code from the repo root — no global install, no /plugin
install step.
It uses a custom plugin rather than the official typescript-lsp, which fails under Volta on Windows;
see the plugin README for why and how.
The custom plugin works on Windows and macOS but currently has a loading issue on Linux / WSL where
Claude Code recognises the plugin yet fails to start its LSP server. On Linux the official plugin
works fine (Volta shims are real executables, not .cmd files), so add this override to
.claude/settings.local.json (gitignored):
Then run /reload-plugins inside Claude Code.
Notes:
- Start Claude Code from the repo root; project-scope plugins don't load from a subdirectory. After
changing directories, run
/reload-plugins. - The
typescript-language-serverdev dependency is installed for everyone viapnpm installbut is only used by Claude Code. If you don't use Claude Code, you can ignore this feature.
Thanks to CodeSandbox for the instant dev environment: https://codesandbox.io/p/github/eten-tech-foundation/scripture-editors/main
Lexical works with plain-vanilla JS/TS as well as with React. To that end, the editor packages in this repo perf-react and perf-vanilla are 2 editor components that behave the same to edit the PERF data format.
If you are using a framework other than React and need to wrap a plain-vanilla JS editor for your framework, you could add your own vanilla TS editor package to this repo. By comparing perf-vanilla and perf-react you can see how to take any existing React plugins you might want and convert them to vanilla TS.
paranext-core and paratext-10-studio pin the platform-yalc branch rather than main. It
exists so that a change here can be coordinated with the change that consumes it: a breaking edit
lands on main first, and platform-yalc is moved forward only once the consuming side is ready,
so build servers never pick up a half-finished handoff.
To move it forward, use the script rather than doing it by hand:
# with platform-yalc checked out and a clean tree
npm run move-platform-yalcIt resets your local branch to origin's state (this branch is force-pushed, so your copy is stale
by design), rebases onto origin/main, runs the consumer-lockfile check below, and force-pushes
only if that passes. It refuses to run from another branch, on a dirty tree, or with commits that
exist only on your machine. See Dependency changes need a paranext-core lockfile
PR for the check itself, its flags, and why
to give it a GITHUB_TOKEN locally.
Doing it by hand skips the lockfile check, which is the failure that reaches every paranext-core build rather than only you.
The name is historical — it refers to yalc, which paranext-core no longer uses. The branch's coordination role is still real, so it stays.
paranext-core's package-lock.json records these packages' dependency closure. If a dependency was
added, removed, or its version range changed here since platform-yalc last moved, every
paranext-core build breaks when it moves again — its npm ci refuses to run until its lockfile is
refreshed.
This is about the dependencies these packages declare, not about their own version numbers.
Raising platform-editor from 0.8.16 to 0.8.17 needs nothing on core's side; changing what
platform-editor depends on — in any way, including moving a dependency's version range — needs
the lockfile PR below.
So when moving platform-yalc past a dependency change:
- In a paranext-core checkout (with this repo's checkout beside it or in its
dev-packages/), runnpm install, commit thepackage-lock.jsondiff, and open a PR. - Merge that PR together with the
platform-yalcpush.
Two things check this for you:
-
npm run move-platform-yalc(run from aplatform-yalccheckout with a clean tree) is the recommended way to move the branch at all: it resets your local branch to origin's state, rebases ontoorigin/main, runs this check, and force-pushes only if it passes — so the problem surfaces before the push, not after, and your checkout ends up at exactly the pushed state. (-- --dry-runstops short of pushing;-- --skip-verifyis the emergency hatch.) -
Give it a token when you run it locally. The check reads paranext-core's
dev-packages.jsonandpackage-lock.jsonand scans core's recently-updated open PRs, which is tens of API requests against the 60-an-hour budget GitHub gives unauthenticated callers. Inside ActionsGITHUB_TOKENis set for you; locally it is not, so a run can stop partway with a rate-limit error. Authenticated requests get 5,000 an hour:export GITHUB_TOKEN=$(gh auth token)
The check says this itself when it recognises the failure, but exporting it first avoids the round trip.
-
The Verify platform-yalc consumer sync workflow runs the same check on every push to
platform-yalc, however the push was made. It passes when paranext-core'smain— or an open paranext-core PR touchingpackage-lock.json— matches this branch's dependencies, and fails with these same instructions otherwise. Re-run it after opening the core PR.
This repo carries the full history of eten-tech-foundation/scripture-editors, so the
two share a common ancestry and git can move commits between them. Set it up as a second remote:
git remote add eten-tech-foundation https://github.com/eten-tech-foundation/scripture-editors.git
git fetch eten-tech-foundationorigin stays this repository. Nothing about your existing checkout changes.
git fetch eten-tech-foundation
git checkout -b merge-eten main
git merge eten-tech-foundation/mainResolve any conflicts, then open a PR into main here as usual.
We are not committed to upstreaming routinely, but the option is deliberately kept open. Push a branch to their repo and open the PR there:
git fetch eten-tech-foundation
git checkout -b my-contribution eten-tech-foundation/main
git cherry-pick <commits from this repo>
git push eten-tech-foundation my-contributionThen open a pull request on eten-tech-foundation/scripture-editors with my-contribution as the
compare branch. Because this repository is not a fork, GitHub will not silently offer their repo as
the base for PRs opened from origin — you have to target it explicitly, which is the intent.
The dist/ and etc/ folders of packages/platform and packages/utilities are committed,
unlike most build output.
paranext-core consumes these packages by copying them out of a checkout rather than installing them from a registry (see Relationship to paranext-core). Committing the build means it needs nothing from this repo's toolchain — no pnpm, no nx, no build step — just to run Platform.Bible. Only people changing the editor build it.
etc/<package>.api.md is API Extractor's report on the public type surface. Nothing consumes it at
runtime; it is committed so that a change to the public API — an added export, a changed signature,
a removed type — arrives as a readable diff in the pull request that makes it, rather than buried in
a rolled-up declaration bundle.
The obligation that comes with both: rebuild and commit them in the same PR as the src/
change. Stale output is invisible in review — the source diff looks right while consumers get old
code and the report claims the API did not move.
pnpm rebuild-committed-output # builds both packages, rolls up their types, writes the reports
git add packages/platform/dist packages/platform/etc packages/utilities/dist packages/utilities/etcUse that script rather than pnpm nx run-many -t extract-api directly. The two run the same work,
but the order the packages are extracted in decides whether each one's rolled-up declarations survive
— a sibling's build can overwrite them — and the script fixes the order. scripts/published-packages.mjs
explains the mechanism; issue #5 is the fix that will make the order stop
mattering.
CI enforces this: it rebuilds and fails if the committed output differs from what the source
produces. Every published artifact is byte-deterministic, so a passing check means they genuinely
match. *.tsbuildinfo is TypeScript's incremental cache rather than a shipped artifact, so it is
neither committed nor packed.
Consumers pin a revision of this repository — a branch or tag in dev-packages.json and
productInfo.json — rather than a published package version. A release is therefore a repository
tag, and one tag pins both packages at once. Tags are named v<platform-editor version>, e.g.
v0.8.17, since platform-editor is the package this repo exists to ship.
These packages are not published to npm. paranext-core stages them from a checkout of the
pinned revision, copying the dist/ this repo commits; see
Relationship to paranext-core.
The flow mirrors paranext-core's:
- Decide which branch you are releasing from. For a stable release, rebase
release-prepontomainif it has not already been rebased this cycle. For a pre-release,mainis normally fine. - Make sure the package versions are what you want to release. If not, dispatch the Bump
Versions workflow against that branch, then open a PR from the
bump-versions-<version>branch it creates and merge that. The workflow pushes the branch; it does not open the PR, and the target branches are protected. - Dispatch the Publish workflow against the branch you are releasing from. It tags the commit,
creates a GitHub release, and — if you give it
newVersionAfterPublishing— opens a follow-upbump-versions-<version>branch so later work applies to a new in-progress version rather than to the one just released. - Open a PR from that
bump-versions-<version>branch and merge it. - Point consumers at the new tag by setting
revisionin paranext-core'sdev-packages.json. That is the only place the editor revision is named: paratext-10-studio'sproductInfo.jsonleavesbranchout fordevPackageReposand follows whatever core pins, so pinning a paranext-core release there already pins the editor that release was built with. - If this release changed either package's dependency lists — added, removed, or re-ranged a
dependencies,peerDependencies,peerDependenciesMetaoroptionalDependenciesentry — refresh paranext-core'spackage-lock.jsonin the same commit that moves the revision (npm installthere, commit the diff). Core stages these packages inside its own tree, so itsnpm civalidates the staged manifests against its lockfile and aborts on any difference; until the lockfile is refreshed, every core build fails. A release that only moves the packages' own version numbers needs nothing here —npm cidoes not check afile:dependency's version.verify-consumer-lockfile-sync.mjsmakes exactly this comparison, and runs on every push toplatform-yalc.
MIT © ETEN Tech Foundation, SIL Global and United Bible Societies
This repository stays MIT-licensed. Platform.Bible itself is moving to AGPL-3.0-or-later; MIT is
compatible in that direction, and keeping this repo MIT is also what keeps contributing changes back
to eten-tech-foundation/scripture-editors possible.
{ // ... keep any existing keys ... "enabledPlugins": { "typescript-lsp@claude-plugins-official": true, "typescript-lsp-volta@skills-dir": false, }, }