Merged TryGhost/Koenig into the Ghost monorepo - #29171
Conversation
no ref The typescript migration requires this change to not throw errors on lint locally while building.
- @tryghost/html-to-mobiledoc@3.2.22 - @tryghost/kg-card-factory@5.1.13 - @tryghost/kg-clean-basic-html@4.2.21 - @tryghost/kg-converters@1.1.20 - @tryghost/kg-default-atoms@5.1.8 - @tryghost/kg-default-cards@10.2.11 - @tryghost/kg-default-nodes@2.0.16 - @tryghost/kg-default-transforms@1.2.39 - @tryghost/kg-html-to-lexical@1.2.39 - @tryghost/kg-lexical-html-renderer@1.3.39 - @tryghost/kg-markdown-html-renderer@7.1.16 - @tryghost/kg-mobiledoc-html-renderer@7.1.16 - @tryghost/kg-parser-plugins@4.2.21 - @tryghost/kg-unsplash-selector@0.3.22 - @tryghost/kg-utils@1.0.43 - @tryghost/koenig-lexical@1.7.23
…ESM output
The Vite 8/Rolldown migration externalized react subpaths (react/jsx-runtime,
react-dom/server) which caused two issues:
- UMD: subpaths mapped to React/ReactDOM globals that don't have the required
exports (jsx, jsxs, renderToString)
- ESM: bundled CJS modules produced require() shims that break in browsers
Removed react-dom/server dependency entirely by replacing renderToString with
direct DOM APIs for the drop indicator and flushSync+createRoot for the drag
ghost icon. Added esmExternalRequirePlugin to convert remaining CJS
require("react") calls from bundled dependencies into ESM import references.
Updated UMD globals to map react-dom/client instead of react-dom/server.
Added --src lib flag to scope coverage to source files only, matching the pattern used by other TypeScript-migrated packages.
- @tryghost/html-to-mobiledoc@3.2.23 - @tryghost/kg-default-nodes@2.0.17 - @tryghost/kg-default-transforms@1.2.40 - @tryghost/kg-html-to-lexical@1.2.40 - @tryghost/kg-lexical-html-renderer@1.3.40 - @tryghost/kg-parser-plugins@4.2.22 - @tryghost/kg-unsplash-selector@0.3.23 - @tryghost/koenig-lexical@1.7.24
…lector These types are needed by consumers to properly type their onImageInsert callbacks but were not exported from the package entry point.
- @tryghost/kg-html-to-lexical@1.2.41 - @tryghost/kg-unsplash-selector@0.3.24 - @tryghost/koenig-lexical@1.7.25
- @tryghost/html-to-mobiledoc@3.2.24 - @tryghost/kg-clean-basic-html@4.2.22 - @tryghost/kg-default-nodes@2.0.18 - @tryghost/kg-default-transforms@1.2.41 - @tryghost/kg-html-to-lexical@1.2.42 - @tryghost/kg-lexical-html-renderer@1.3.41 - @tryghost/kg-parser-plugins@4.2.23 - @tryghost/kg-unsplash-selector@0.3.25 - @tryghost/koenig-lexical@1.7.26
- @tryghost/koenig-lexical@1.7.27
no ref - this was not observed in the demo app because of the way the editor type was previously handled, and it wasn't clear it wasn't wired up outside of the app
- @tryghost/kg-default-nodes@2.0.19 - @tryghost/kg-default-transforms@1.2.42 - @tryghost/kg-html-to-lexical@1.2.43 - @tryghost/kg-lexical-html-renderer@1.3.42 - @tryghost/koenig-lexical@1.7.28
Ghost no longer uses mobiledoc-based editing internals as of ba438f2, so the following packages have no remaining consumers in Ghost, Koenig, or anywhere else in the TryGhost org: - @tryghost/kg-default-atoms - @tryghost/kg-mobiledoc-html-renderer - @tryghost/html-to-mobiledoc - @tryghost/kg-parser-plugins (last consumer was html-to-mobiledoc) Removing these also drops @tryghost/mobiledoc-kit from the dependency tree entirely.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
no issue `yarn build-storybook` and `yarn storybook` failed with `ReferenceError: require is not defined in ES module scope` while loading .storybook/main.ts, and the manager build failed with an exports mismatch from leftover Storybook 8 addons. - replaced the `getAbsolutePath()` resolution in .storybook/main.ts with plain package names — the helper relied on CJS `require` which is unavailable in ESM scope, and absolute directory paths can't be imported by Node's ESM loader anyway; plain names are Storybook's recommended default for yarn-hoisted setups - removed the `core.builder` override — the @storybook/react-vite framework already implies the vite builder in Storybook 10 - removed leftover v8/v9 addon packages (addon-essentials, addon-interactions, addon-actions, addon-mdx-gfm) whose features moved into Storybook core in v9 and which broke the manager build - added @storybook/addon-docs, required in Storybook 10 for the `autodocs` tag used in preview - removed the explicit @storybook/builder-vite dependency, now only used as a transitive dependency of @storybook/react-vite
no issue CodeQL flagged two issues on the TypeScript rename PR (TryGhost/Koenig#2046); the code is long-standing but the rename surfaced it as new code. Fixing separately so the rename PR stays purely mechanical. - `sanitizeHtml()`'s script/iframe placeholder regexes only matched strict `</script>`/`</iframe>` end tags, but per the HTML spec browsers also close on lax forms like `</script >` and `</script\t\n bar>`, so such markup skipped placeholder replacement. DOMPurify still stripped the elements afterwards so this wasn't exploitable, but the end-tag matching now mirrors browser parsing - `dataSrcToFile()` fell back to `Math.random()` for filename generation when `crypto.randomUUID()` is unavailable (non-secure contexts); it now uses `crypto.getRandomValues()`
- Add permissive tsconfig.json (strict mode arrives with the type migration) - Add src/vite-env.d.ts with Vite/SVG ambient declarations - Point vite lib entry, index.html demo entry, and vitest setupFiles at renamed .ts/.tsx files - Add __dirname ESM shim to vite configs (loaded as native ESM once .ts) - Update playwright firefox testMatch/testIgnore regexes for .ts test files - Parse .ts/.tsx in ESLint via typescript-eslint parser, keeping existing rules - Ignore .d.ts files in ESLint (base no-unused-vars misfires on declarations) - Reference vite.config.demo.ts in package.json scripts
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
- imports all 13 Koenig packages under koenig/* with full git history rewritten from packages/* via git filter-repo - issue/PR refs in imported commit messages are qualified as TryGhost/Koenig#N to avoid mislinking against Ghost issues - workspace integration, dependency conversion, and CI follow in subsequent commits (see docs/koenig-monorepo-merge-plan.md)
|
Important Review skippedToo many files! This PR contains 898 files, which is 598 over the limit of 300. To get a review, narrow the scope: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (131)
📒 Files selected for processing (898)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run @tryghost/admin-x-settings:test:acceptance |
✅ Succeeded | 10m 25s | View ↗ |
nx run ghost:test:ci:integration |
✅ Succeeded | 4s | View ↗ |
nx run ghost:test:integration |
✅ Succeeded | 1m 37s | View ↗ |
nx run ghost:test:legacy |
✅ Succeeded | 2m 56s | View ↗ |
nx run @tryghost/koenig-lexical:test:acceptance |
✅ Succeeded | 2m 34s | View ↗ |
nx run ghost:test:e2e |
✅ Succeeded | 1m 35s | View ↗ |
nx run-many --target=build --projects=tag:publi... |
✅ Succeeded | <1s | View ↗ |
nx run @tryghost/activitypub:test:acceptance |
✅ Succeeded | 50s | View ↗ |
Additional runs (9) |
✅ Succeeded | ... | View ↗ |
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗
☁️ Nx Cloud last updated this comment at 2026-07-08 11:59:51 UTC
| end = end.replace(/(\*\*|__)/, ""); | ||
| } else if(type == "italic") { | ||
| start = start.replace(/(\*|_)(?![\s\S]*(\*|_))/, ""); | ||
| end = end.replace(/(\*|_)/, ""); |
| /\s*editor-preview-active\s*/g, "" | ||
| ); | ||
| if(toolbar) { | ||
| toolbar.className = toolbar.className.replace(/\s*active\s*/g, ""); |
| ); | ||
| var toolbar = editor.toolbarElements.preview; | ||
| var toolbar_div = wrapper.previousSibling; | ||
| toolbar.className = toolbar.className.replace(/\s*active\s*/g, ""); |
| preview.className = preview.className.replace( | ||
| /\s*editor-preview-active-side\s*/g, "" | ||
| ); | ||
| toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ""); |
| if(!/active/.test(toolbarButton.className)) { | ||
| toolbarButton.className += " active"; | ||
| } else { | ||
| toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ""); |
| let type = 'text'; | ||
| let data = content.slice(entity.start, entity.end + 1).join('').replace(/\n/g, '<br>'); | ||
| if (entity.url) { | ||
| if (!entity.display_url || entity.display_url.startsWith('pic.twitter.com')) { |
| let type = 'text'; | ||
| let data = content.slice(entity.start, entity.end + 1).join('').replace(/\n/g, '<br>'); | ||
| if (entity.url) { | ||
| if (!entity.display_url || entity.display_url.startsWith('pic.twitter.com')) { |
| } | ||
|
|
||
| function removeCodeWrappers(html: string): string { | ||
| return html.replace(/<code\b[^>]*>((.*?){.*?}(.*?))<\/code>/gi, '$1'); |
| }; | ||
|
|
||
| export function render(markdown: string, options: RenderOptions = {}): string { | ||
| return selectRenderer(options).render(markdown); |
| ) { | ||
| target[property] = _mergeProperties(target[property] || {}, source[property]); | ||
| } else { | ||
| target[property] = source[property]; |
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 28927440502 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e949df782
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "@tryghost/string": "0.3.5", | ||
| "@tryghost/url-utils": "5.2.7", | ||
| "handlebars": "4.7.9", | ||
| "juice": "12.1.1", |
There was a problem hiding this comment.
Publish the same Juice dependency Ghost ships
@tryghost/kg-default-cards now declares juice@12.1.1, but the root workspace overrides juice to 9.1.0 and the Ghost archive copies that override, so CI/Ghost build and ship this card package against 9.1.0 while the standalone npm package will install 12.1.1 for external consumers. That breaks the stated “content that shipped inside a released Ghost” guarantee for the Koenig npm packages; align this dependency with the override or remove the override for this package before publishing it separately.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed real, but pre-existing rather than introduced here: Ghost's root workspace has overridden juice to 9.1.0 since before this merge, so the npm-installed kg-default-cards@12.1.1-juice was already being forced to 9.1.0 inside Ghost while external consumers got 12.1.1. The merge preserves that status quo (and the shipped archive matches what CI tests). Needs a team decision: either align kg-default-cards to juice 9.1.0 (changes the external npm package) or drop/scoped-narrow the root override. Leaving this thread open.
10a6308 to
e6ebed0
Compare
ref #29171 The "Unit tests" job wedged until cancelled: every suite passed but the job never finished. nx prints a task's output only on completion, and the one task that never completed (twice in a row) was kg-default-nodes:test:unit. Reproduced in a Linux container with the exact PR tree: direct `pnpm test:unit` exits cleanly, but `nx run` of the same target prints "708 passing" and then hangs forever. nx runs tasks with socketpair stdio, and in that environment the @prettier/sync worker channel (a MessagePort from make-synchronized) stays referenced after the tests finish - mocha waits for the event loop to drain, so the process never exits. kg-default-nodes is the only mocha-based koenig package whose tests use @prettier/sync, which is why its nine sibling packages with the identical c8+mocha+tsx setup were unaffected. - added mocha --exit so the process exits once the run completes; coverage collection is unaffected (verified 91% lines in-container) - bounded each unit-test retry attempt with `timeout 25m` so a future exit-hang becomes a retriable failure (nx skips already-passed projects on retry) instead of wedging the job for 6 hours - gave the legacy tests step timeout-minutes: 30 - the sqlite leg hit a similar one-off vitest teardown hang that held the job for 90 minutes
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #29171 +/- ##
==========================================
+ Coverage 74.28% 74.29% +0.01%
==========================================
Files 1566 1566
Lines 135733 135781 +48
Branches 16443 16463 +20
==========================================
+ Hits 100828 100884 +56
+ Misses 33873 33865 -8
Partials 1032 1032
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ref #29171 Converted the imported koenig/* packages to pnpm workspace members: consumers reference them as workspace:*, koenig inter-package deps use workspace:~ (the publish model computes patch versions from npm, so package.json digits go stale), scripts moved from yarn to pnpm, and shared dev deps route through the catalog. Also wired the nx task graph so test targets work in the monorepo: - ghost/core's plain test:e2e/test:integration/test:legacy targets get ^build like their test:ci:* variants, and the build targetDefaults outputs include {projectRoot}/build - the kg packages emit build/ (dual esm/cjs tsc), so without it nx cache restores produced empty packages and ghost/core tests failed with MODULE_NOT_FOUND on @tryghost/kg-converters - ghost/admin's dev/build/test targets depend on ^build so its workspace deps (koenig-lexical, kg-converters, ...) are built first - kg-default-nodes' mocha runs with --exit: under nx's task runner (socketpair stdio) the @prettier/sync worker channel stays referenced after the suites finish, so mocha - which waits for the event loop to drain - never exited and wedged CI. It is the only mocha-based koenig package using @prettier/sync; direct pnpm runs were unaffected.
ref #29171 Wired the imported koenig/* packages into the Ghost CI matrix and the release lane: - unit/lint/acceptance coverage for the koenig packages in ci.yml; the Playwright acceptance matrix is selected via an nx "playwright" tag on the packages instead of a hardcoded list - the koenig-lexical acceptance leg installs MS core fonts (caret position assertions depend on Arial line-wrap metrics, not just screenshots) and firefox media codecs via playwright install-deps; scoped to that one matrix leg because repo-wide apt-get has a history of hangs - npm publishing runs only in the release tag lane (publish_koenig_packages, after publish_ghost) and skips prerelease tags since the publish script computes stable versions; failures feed notify_release_failure. publish-koenig-package.yml remains as a workflow_dispatch escape hatch for out-of-band single-package publishes, with dispatch inputs passed via env rather than interpolation - each unit-test retry attempt is bounded by `timeout 25m` so a test process that finishes but never exits becomes a retriable failure (nx skips already-passed projects on retry) instead of wedging the job for 6 hours; the legacy step gets timeout-minutes for the same reason
- replaces the pages.yml workflow from the archived Koenig repo: builds the koenig-lexical demo and deploys it to GitHub Pages with the koenig.ghost.org CNAME on main pushes touching koenig/ - Pages needs enabling on this repo (gh-pages branch) and the TENOR_API_KEY / KLIPY_API_KEY secrets copying over before the old repo is archived
- component discovery now walks workspace: deps transitively, since the kg-* packages depend on each other (previously only direct file: deps of ghost/core were packed) - components are packed from the source workspace so pnpm rewrites workspace: specs itself; packing the deploy-dir copies fails on the workspace protocol - every component gets a pnpm override pinning it to its bundled tarball, so nested specs like kg-html-to-lexical's ~2.1.3 on kg-default-nodes resolve to the tarball that shipped in the archive instead of the npm registry — production installs of Ghost never depend on Koenig packages being published
ref #29171 Updated the koenig/* READMEs and agent docs for their new home in the Ghost monorepo, and brought the imported code up to Ghost's lint conventions: - koenig-lexical's five warn-level eslint rules are now errors per the repo-wide "error or off, never warn" rule; pre-existing react-hooks/exhaustive-deps violations carry inline disables with rationale comments (GifSelector re-subscribes its keydown handler every render; EmbedCard's observer effect intentionally runs once) - removed an unused import left behind by a commented-out test - kg-default-nodes lint fixes that Koenig CI never caught because its lint was not wired into a posttest there
ref #29171 The kg server packages are now built before the dev containers start (docker:up depends on their build targets, so a fresh checkout works without a manual build step), and pnpm dev:lexical runs the in-repo Koenig editor instead of expecting a dev server in an external Koenig checkout: - a dev:integrated script in koenig-lexical rebuilds the editor on change and serves its dist via vite preview, alongside watchers for kg-default-nodes and kg-default-transforms - the docker:dev:lexical nx target starts that watcher with the rest of the dev stack; the Caddy dev gateway proxies /ghost/assets/koenig-lexical/* to it so Admin loads the local build - dev-gateway docs updated to match (note: the Caddyfile comment deliberately avoids backticks - Caddy's lexer treats them as quote delimiters even inside comments)
407ae32 to
df3249f
Compare

Implements the plan in #29156: moves all 13 Koenig packages from TryGhost/Koenig into this monorepo under
koenig/*, with full git history.This branch must land as a true merge commit (or admin push). Squashing would flatten the ~5,200 imported commits into one and destroy
git log/git blame/git bisectfor all Koenig code. The branch is structured as:fb6b427a6— the history import: a single--allow-unrelated-historiesmerge of the rewritten Koenig history (packages/*→koenig/*viagit filter-repo, issue refs qualified asTryGhost/Koenig#N)Koenig is frozen at
84bff4510, which is the imported tip — no divergence.What changes
Workspace integration
koenig/*added to the pnpm workspace; all Ghost consumers (ghost/core,ghost/admin,apps/*) now useworkspace:*for kg packages and koenig-lexical — nothing in dev or CI installs Koenig from npmworkspace:~so publish rewrites them to~x.y.z(npm stays the patch-version source of truth)test:e2erenamedtest:acceptance; phantom deps exposed by pnpm strict linking added explicitlyPublishing (release-tag lane only)
publish_koenig_packagesjob runs afterpublish_ghostonv*tags: per-package git-diff vs the previous release tag, next-patch version computed from npm, OIDC publish in topological order (.github/scripts/publish-koenig-packages.js)workflow_dispatchescape hatch for one-off publishes (publish-koenig-package.yml)Release packaging
ghost/core/scripts/pack.jsbundles kg packages as component tarballs inside the Ghost archive, pinned via pnpm overrides — production installs can never resolve Koenig from the registry, so no version-skew windowCI
corepath filters coverkoenig/**(editor-UI packages excluded); Playwright matrix selection is now tag-based (--projects 'tag:playwright') instead of directory-basedkoenig/**Dev workflow
pnpm dev:lexicalnow runs the in-repo editor watcher + preview server (no external checkout needed)Verification
nx build; 12 koenig projectstest:unit+lintgreenghost/coreunit 7180/7180; ghost-admin ember 1066/1066CI=true pnpm install --prodresolves all kg packages fromfile:components/*.tgz,require('@tryghost/kg-html-to-lexical')workspnpm dev:lexicalrebuild loop verified through the gatewayOrg-side follow-ups (before/at landing)
TENOR_API_KEY/KLIPY_API_KEYsecrets for the demo deployIS_SHIPPINGflow) into the publish job — deliberately dropped for now