Skip to content

[OP-20422] Publish the build to npm (2/2) - #140

Open
myabc wants to merge 5 commits into
masterfrom
code-maintenance/OP-20422-npm-package
Open

myabc wants to merge 5 commits into
masterfrom
code-maintenance/OP-20422-npm-package

Conversation

@myabc

@myabc myabc commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/OP-20422

What are you trying to accomplish?

Second of two PRs. It makes this repository publishable as @openproject/commonmark-ckeditor-build and adds the release flow, so that OpenProject core can replace the build committed under frontend/src/vendor/ckeditor/ with a pinned npm dependency. Stacked on the esbuild PR.

After this, an editor change no longer needs build output copied into a core pull request. Core tests an unreleased change by pinning a canary version.

What approach did you choose and why?

Package

  • The build goes to dist/ and needs no core checkout. The package publishes only dist/.
  • Entry points: the bundle (.), ./ckeditor.css, and ./translations.
  • loadTranslation(locale) replaces core's templated import of a locale file. A bundler can only expand such an import for relative paths, not for a package, so the build ships a static table of per-locale imports that a consumer's bundler splits into lazy chunks.
  • loadTranslation also tries the lower-cased locale and resolves with the name it loaded. Core says zh-CN and pt-BR where CKEditor registers zh-cn and pt-br; today those users get an English editor.
  • The bundle behaves as before and still sets the three window globals. The package deliberately declares no types for them: core declares them itself, and a second declaration of a different type would not compile. Real types across the boundary are a separate piece of work.
  • The patch for the mention plugin moves from postinstall to prepare. As postinstall it would also run, and fail, inside a consumer's install.
  • bin/verify-package.mjs asserts what would be published, and that no install script or runtime dependency reaches the consumer. publint checks the exports map.
  • Version 12.0.0. license and contributors now follow LICENSE.md; the old values, and the bugs and repository fields, came from the CKEditor build template.

Release flow

  • Releases use changesets, as in opf/stimulus-elements: merging to master opens or updates a "Release Tracking" PR, and merging that publishes to npm over OIDC (Trusted Publishing, no token in the repository).
  • The label canary on a PR publishes a snapshot 0.0.0-canary-<timestamp> under the dist-tag canary and comments with the install command. Only PRs from branches of this repository can do this.
  • A check fails PRs without a changeset; the label skip changeset bypasses it.

Local development

npm run watch rebuilds into dist/. With OPENPROJECT_CORE set, each build is also copied over the package core installed, so no symlink is needed and core's next npm ci restores the pinned version.

Open points

  • The release and canary jobs cannot be tested before the package exists on npm and a trusted publisher is configured. The first canary run on this PR is that test.
  • The trusted publisher trusts release.yml by name, so anyone with write access can publish by changing that file in a PR. opf/primer_view_components has the same property.
  • "Release Tracking" PRs are opened with GITHUB_TOKEN and therefore do not trigger CI.
  • The CI job is renamed from build (20.x) to build, since the Node matrix is gone. A branch protection rule naming the old check needs updating.
  • [OP-20399] Upgrade CKEditor to 48.5.2 #132 also changes package.json and the mention patch; whichever merges second needs a rebase.

Verification

CI does not run for this PR while it targets the esbuild branch. Run locally: npm run build, npm run verify:build, npm run verify:package, npm test (256 passed), npm run test:bin (13 passed), npm run lint, npm run typecheck. A clean npm ci still applies the mention patch.

AI involvement

Directed: written by Claude Code from a design agreed with @myabc, then reviewed adversarially by Codex. Codex found that the changesets action did not match the installed CLI version, which is fixed and covered by a test.

@myabc myabc added maintenance skip changeset This change does not affect the published build labels Oct 8, 2026
@myabc
myabc added this pull request to stack #138 October 8, 2026 09:24
export function renderTranslationsIndex(locales) {
const entries = [...locales]
.sort()
.map((locale) => ` [${JSON.stringify(locale)}, () => import(${JSON.stringify(`./${locale}.js`)})],`)
@myabc
myabc requested review from a team and oliverguenther and a balanced review from Copilot October 8, 2026 10:55
@myabc
myabc marked this pull request as ready for review October 8, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The changeset gate, initial publication flow, and canary version uniqueness have unresolved release risks.

4 open findings
What changed in this PR

Makes the CKEditor build publishable as an npm package with automated release and canary workflows.

Changes:

  • Builds and publishes bundle, styles, types, and lazy translations from dist/.
  • Adds Changesets-based releases, canary publishing, and package verification.
  • Updates local OpenProject integration, CI, documentation, and tests.
File Description
README.md Documents build, development, and release workflows.
package.json Defines package exports, scripts, and release tooling.
package-lock.json Locks new publishing dependencies.
jest.config.cjs Configures Jest for the ESM package.
eslint.config.mjs Ignores build output and recognizes CJS configs.
docker-compose.yml Mirrors builds into core’s installed package.
CHANGELOG.md Records the 12.0.0 package changes.
bin/​verify-package.mjs Validates published tarball contents.
bin/​verify-build.mjs Verifies types and translation entry points.
bin/​mirror-to-core.mjs Copies builds into OpenProject core.
bin/​lib/​translations-index.test.mjs Tests translation loading behavior.
bin/​lib/​translations-index.mjs Generates lazy translation imports.
bin/​lib/​release-workflow.test.mjs Checks Changesets action compatibility.
bin/​lib/​paths.test.mjs Tests package mirroring paths.
bin/​lib/​paths.mjs Centralizes build and core paths.
bin/​build.mjs Builds the ESM bundle into dist/.
bin/​build-ckeditor-translations.mjs Generates translation modules and index.
bin/​build-ckeditor-styles.mjs Writes package CSS into dist/.
babel.config.cjs Configures Babel for TypeScript tests.
.gitignore Excludes generated distribution files.
.github/​workflows/​release.yml Adds release and canary publishing.
.github/​workflows/​ci.yml Expands CI package verification.
.github/​workflows/​changeset-check.yml Enforces changesets on pull requests.
.changeset/​README.md Documents changeset usage.
.changeset/​config.json Configures package versioning and changelogs.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +13 to +14
permissions:
contents: read
Comment thread package.json Outdated
{
"name": "@openproject/commonmark-ckeditor-build",
"version": "v11.1.1",
"version": "12.0.0",
Comment thread .github/workflows/changeset-check.yml Outdated
- name: Check for changeset
uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1
with:
file-pattern: '.changeset/*.md'
printf -- '---\n"@openproject/commonmark-ckeditor-build": patch\n---\n\nCanary release\n' > .changeset/force-canary-release.md
jq '.changelog = "@changesets/cli/changelog"' .changeset/config.json > .changeset/config.tmp.json
mv .changeset/config.tmp.json .changeset/config.json
npx changeset version --snapshot canary
@myabc myabc added the canary Publish a snapshot release of this pull request to npm label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Canary published: @openproject/commonmark-ckeditor-build@0.0.0-canary-20261012095012

In frontend/ of OpenProject core:

npm install --save-exact @openproject/commonmark-ckeditor-build@0.0.0-canary-20261012095012

@myabc
myabc force-pushed the code-maintenance/OP-20422-npm-package branch from 44ecd4b to dfc5f86 Compare October 8, 2026 13:26
@myabc myabc removed the skip changeset This change does not affect the published build label Oct 8, 2026
@myabc
myabc force-pushed the code-maintenance/OP-20422-npm-package branch 3 times, most recently from f54c4bd to eefec36 Compare October 11, 2026 16:25
@myabc myabc added the don't merge experiment/spike label Oct 11, 2026
@myabc
myabc force-pushed the code-maintenance/OP-20422-npm-package branch from eefec36 to d190cd8 Compare October 11, 2026 16:57

@oliverguenther oliverguenther left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in person and in element, I'm not yet convinced that we need to publish this to NPM. Yes, it improves consistency, but at the cost of developer efficiency. I'd welcome someone else to try out the DX after this change and see if you can still:

  • Develop locally easily (currently watching/overriding the core build)
  • Build and merge together with a core PR - This will likely be impossible as you first need a build, then a core PR

Especially the second point I'm not really convinced with. With our other dependencies, this makes sense as they are tested individually. For this build, we always depend on the core test suite for correctness.

/cc @opf/developers as other people worked on this more frequently than me in the past weeks.

Base automatically changed from code-maintenance/OP-20422-esbuild to master October 12, 2026 09:49
myabc added 5 commits October 12, 2026 11:49
A build no longer needs a core checkout. With OPENPROJECT_CORE set,
the build is copied over the package core installed from npm, which
keeps the local development loop without symlinks.

https://community.openproject.org/wp/OP-20422
Core loads locales with a templated import of a relative path, which
a bundler cannot expand for a package. The build now ships a static
table of per-locale imports behind one function instead.

The function also tries the lower-cased name and returns the name it
loaded: core says zh-CN where CKEditor registers zh-cn, which left
those users with an English editor.

https://community.openproject.org/wp/OP-20422
Declares the entry points core needs and publishes only dist. The
patch for the mention plugin moves from postinstall to prepare, since
postinstall would also run, and fail, in a consumer's install.

https://community.openproject.org/wp/OP-20422
Merging to master opens or updates a release pull request; merging
that publishes to npm over OIDC. A pull request labelled canary gets
a snapshot release, so core can test a change before it is released.

https://community.openproject.org/wp/OP-20422
Describes building into dist, developing against a core checkout,
and releasing with changesets and canary versions.

https://community.openproject.org/wp/OP-20422
@myabc
myabc force-pushed the code-maintenance/OP-20422-npm-package branch from d190cd8 to 16ac9f3 Compare October 12, 2026 09:49
@as-op

as-op commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

I do agree with @oliverguenther. While I'm not against deploying to npm, I wouldn't be a fan of having a deployment step in between while developing locally. Maybe we can have both somehow? I wouldn't even mind adjusting the package.json in the OP frontend to something like

 "@openproject/commonmark-ckeditor-build": "../../commonmark-ckeditor-build"

or use npm link.
Which would require the layout of transpiled files in the package be more less the same as the local one.

@dfriquet

Copy link
Copy Markdown
Contributor

I experience mixed feelings here. After going through my first PR relying on a change in CKEditor, I must say that it troubles me to commit a full ckeditor.js downstream:

  1. building another branch than main can overwrite someone else’s changes, and the back and forth between branches during development and review is pretty error-prone;
  2. in the end, a reviewer can’t really review the changes, to add some more risk, as it is minified without any kind of traceability.

Like you, I’d prefer to have the ability to:

  1. run a local OP instance built against a specific version of commonmark-ckeditor-build, preferably a local clone watching for changes;
  2. push a PR that either build against commonmark-ckeditor-build#main (lightweight "release" process, just merge the PR) or update the version of the NPM package (full release process).

Sorry for the long comment, but I tried out a few options and… I reached both with this PR (replayed onto master for CKEditor 49, plus #141) and opf/openproject#25897. Both work with one small change on each side:

Develop locally. OPENPROJECT_CORE=<path to core> npm run watch copies each rebuild over the package installed in core. A browser reload shows the change within seconds, without restarting the dev server1.

Build and merge together with a core PR. During review, the core PR pins the editor branch: "@openproject/commonmark-ckeditor-build": "github:opf/commonmark-ckeditor-build#<branch>" (I tried it with a git+file:// URL to a local clone)2. npm clones the branch, runs prepare, and the lockfile pins the resolved commit, so the core test suite runs against exactly that build. We don’t really need NPM if we can build it locally directly from a GitHub commit-ish. Otherwise, we can pin to the canary version.

Once the editor PR is merged, the core PR moves its pin to one of, depending on the editor’s release process:

  • #main: just merge. The commit lives only in the lockfile, and a later npm install or npm update may pull in unreleased work.
  • a git tag, e.g. #v12.1.0 (or #semver:^12.1.0). The version is named in package.json, and no registry, npm account or publishing workflow is needed.
  • a release tarball: on each tag, CI runs npm pack and attaches the result to the GitHub release, and core points at it, e.g. https://github.com/opf/commonmark-ckeditor-build/releases/download/v12.1.0/openproject-commonmark-ckeditor-build-12.1.0.tgz.
  • a published npm version: the full release, prebuilt, and immutable once published.

The release tarball is the one I'd like to try if publishing to NPM is a problem. It's as light as a tag to release: a merge and a tag, no registry, npm account or trusted publisher. And it installs like an npm release: prebuilt, with no clone, devDependencies or build at install time. The lockfile records the tarball's integrity hash, so a replaced file makes installs fail instead of slipping in. That’s what I understand at least.

What a core reviewer gets. Today, a vendored build shows up as ckeditor.js | Bin 1435103 -> 1589049 bytes, with nothing linking it to a commit. With a pin, the core PR's diff names what changed, and the editor-side changes can be read as source:

Pin Diff in the core PR What the reviewer can follow
editor branch, during review #<branch> in package.json; the commit in the lockfile's resolved field the editor PR, and the exact commit the suite ran against
#main only the lockfile's resolved commit changes the compare view between the old and new commits
tag #v12.0.0 → #v12.1.0 in package.json, plus the lockfile's resolved commit compare/v12.0.0...v12.1.0 on this repository, with the merged PRs behind it
release tarball v12.0.0 → v12.1.0 in the tarball URL, plus the lockfile's integrity hash the same compare view, and the GitHub release notes
npm version the version in package.json, plus the lockfile's integrity hash the changeset entries and CHANGELOG.md for that version

tl;dr: I find value here for core reviewers with explicit dependencies, and a good DX without regression — from where I sit. ℹ️ I use the Docker dev environment.

Footnotes

  1. One catch Claude resolved, on the core side ([OP-20422] Install the CKEditor build from npm openproject#25897): the Angular dev server pre-bundles the package and re-transforms the minified code, and the editor never renders. Excluding it fixes that, in frontend/angular.json under serve.options: "prebundle": { "exclude": ["@openproject/commonmark-ckeditor-build"] }. ↩

  2. The branch, #main and tag pins install from git, so the change needed here applies to them: dist/ is gitignored, so prepare has to build, e.g. "prepare": "patch-package && npm run build". Otherwise a git install delivers an empty package. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

canary Publish a snapshot release of this pull request to npm don't merge experiment/spike maintenance

Development

Successfully merging this pull request may close these issues.

6 participants