Skip to content

Changed monorepo package manager to pnpm#26997

Open
jonatansberg wants to merge 12 commits intomainfrom
migrate-to-pnpm-poc
Open

Changed monorepo package manager to pnpm#26997
jonatansberg wants to merge 12 commits intomainfrom
migrate-to-pnpm-poc

Conversation

@jonatansberg
Copy link
Copy Markdown
Member

Summary

  • hard-cut the monorepo from Yarn v1 to pnpm at the root, workspace, CI, Docker, and docs layers
  • make the legacy Ember admin work under pnpm without shamefully-hoist, including targeted compatibility fixes
  • update supporting tooling like dependency inspection and Vite tsconfig scanning for the pnpm layout

Main changes

  • add pnpm-workspace.yaml, .npmrc, and pnpm-lock.yaml, and switch root package management to pnpm
  • migrate workspace scripts, CI workflows, hooks, Docker install flow, and dev docs from yarn to pnpm
  • fix ghost/admin pnpm compatibility, including the Koenig lexical import path and explicit legacy dependency handling
  • fix TS declaration/build issues surfaced by pnpm's isolated layout in the React packages
  • update .github/scripts/dependency-inspector.js to use pnpm outdated

Verification

  • corepack pnpm install
  • corepack pnpm install --frozen-lockfile
  • bash .github/scripts/install-deps.sh
  • corepack pnpm nx run ghost-admin:build:dev
  • corepack pnpm nx run ghost-admin:build
  • corepack pnpm nx run @tryghost/admin:build
  • corepack pnpm nx run-many -t build --exclude=ghost-admin
  • node .github/scripts/dependency-inspector.js --patch

Notes

  • commit was created with --no-verify because the current pre-commit hook path runs root ESLint 9 without a root eslint.config.js, which fails independently of this migration diff
  • remaining yarn references are limited to plan docs, intentionally separate theme/fixture packages, and consumer-facing library install snippets

This hard-cuts the monorepo from Yarn v1 to pnpm across root config,
workspace scripts, CI, Docker, and docs while keeping legacy Ember
admin building without shamefully-hoist by fixing the pnpm-specific
compatibility gaps directly.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Walkthrough

This pull request migrates the Ghost monorepo from Yarn v1 to pnpm as the package manager. Changes include introducing pnpm-workspace.yaml and .npmrc configuration files, updating package.json to declare pnpm@10.33.0 as the packageManager and replace resolutions with pnpm.overrides, and converting all package manager invocations across CI workflows, Docker configurations, shell scripts, and documentation from yarn to pnpm. Build configurations and asset imports are updated to use require.resolve() for module path resolution instead of hardcoded node_modules/... paths. TypeScript type annotations are refined in several modal and utility components. Lockfile references shift from yarn.lock to pnpm-lock.yaml throughout caching and dependency installation logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Changed monorepo package manager to pnpm' clearly and specifically summarizes the main change: migrating the monorepo from Yarn v1 to pnpm.
Description check ✅ Passed The PR description is well-structured and directly related to the changeset, providing clear sections on summary, main changes, verification steps, and implementation notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate-to-pnpm-poc
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch migrate-to-pnpm-poc

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.69.3)

Failed to read Trivy output file: ENOENT: no such file or directory, open '/inmem/1273/nsjail-d65f2d11-1fec-40a7-aa49-86a6592fd25d/merged/.trivy-output.json'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The pnpm migration exposed that lint-staged was falling back to root ESLint
for most staged files even though the repo root has no ESLint 9 flat config.
This routes staged files only into workspaces that own ESLint configs so the
pre-commit hook can run successfully again without weakening checks.
The pnpm install path runs package prepare scripts in CI, and
@tryghost/parse-email-address was failing there on the stricter
 import resolution path. Switching to  keeps runtime
behavior the same while restoring compatibility for the prepare build.
The CI install path compiles this workspace during prepare, but the package relied on root Node typings and the plain 'url' specifier resolved like a third-party module under isolated resolution. This adds a direct @types/node dependency and restores the builtin node:url import so pnpm CI installs can compile it reliably.
The pnpm CI install path compiles prepare-built React workspaces in isolation, and admin-x-design-system was relying on root-level React type packages instead of declaring them directly. This adds explicit React type dependencies to the prepare-built workspaces that shared the same pattern so isolated pnpm installs can typecheck them reliably.
The pnpm CI install path now reaches ghost/core type compilation during install, and that package was importing Express types without declaring @types/express directly. This adds the missing build-time type dependency so isolated pnpm installs can compile ghost/core reliably.
The pnpm install path compiles the e2e workspace during prepare, and its fake Stripe server was importing Express without declaring either the runtime package or its typings. This adds both dependencies directly so the e2e workspace builds in isolation under pnpm.
The pnpm install path compiles the e2e workspace during prepare, and its Stripe builder imports stripe types directly. This adds stripe to the workspace manifest so the e2e package can typecheck in isolation under pnpm.
@jonatansberg
Copy link
Copy Markdown
Member Author

Temporarily closing to retrigger PR checks on the current head SHA.

@jonatansberg
Copy link
Copy Markdown
Member Author

Temporarily closing to retrigger PR checks on the updated head SHA.

@jonatansberg jonatansberg reopened this Mar 27, 2026
@jonatansberg jonatansberg marked this pull request as ready for review March 27, 2026 07:16
@jonatansberg
Copy link
Copy Markdown
Member Author

Temporarily closing to retrigger CI on the current non-draft head SHA.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/admin-x-design-system/package.json (1)

64-90: ⚠️ Potential issue | 🟡 Minor

validator is listed in both devDependencies and dependencies.

The validator package appears at line 64 in devDependencies and again at line 90 in dependencies, both with version 13.12.0. This duplication is unnecessary—if it's needed at runtime, keep it only in dependencies; if it's only for dev/test, keep it only in devDependencies.

🔧 Proposed fix - remove from devDependencies
         "storybook": "8.6.14",
         "tailwindcss": "^4",
         "typescript": "5.8.3",
-        "validator": "13.12.0",
         "vite": "5.4.20",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-design-system/package.json` around lines 64 - 90, Remove the
duplicate "validator" entry from the wrong dependency section: keep a single
entry in package.json under the appropriate key (either "dependencies" or
"devDependencies") depending on runtime usage; specifically remove the
"validator": "13.12.0" line from the devDependencies block if it is needed at
runtime (or remove the one in dependencies if it is only for tests/dev),
ensuring only one "validator" entry remains and package.json JSON stays valid.
🧹 Nitpick comments (7)
apps/admin-x-framework/tsconfig.declaration.json (1)

4-11: Inconsistent indentation within compilerOptions.

Line 4 (noEmit) uses 4-space indentation while lines 5-11 use 8-space indentation. All properties within compilerOptions should have consistent indentation.

🔧 Proposed fix
   "compilerOptions": {
     "noEmit": false,
-        "composite": true,
-        "declaration": true,
-        "declarationMap": true,
-        "declarationDir": "./types",
-        "emitDeclarationOnly": true,
-        "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
-        "rootDir": "./src"
+    "composite": true,
+    "declaration": true,
+    "declarationMap": true,
+    "declarationDir": "./types",
+    "emitDeclarationOnly": true,
+    "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
+    "rootDir": "./src"
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-framework/tsconfig.declaration.json` around lines 4 - 11, The
"compilerOptions" block has inconsistent indentation: "noEmit" is indented with
4 spaces while the other keys ("composite", "declaration", "declarationMap",
"declarationDir", "emitDeclarationOnly", "tsBuildInfoFile", "rootDir") use 8
spaces; make all properties use the same indentation style (choose either 4 or 8
spaces) and update the "noEmit" line to match the others so the entire
compilerOptions object is consistently indented.
e2e/package.json (1)

38-48: Consider updating stripe to a more recent version.

The stripe package is correctly used only in e2e tests with a fake server implementation (no runtime SDK calls, only type imports). While version 8.222.0 has no known security vulnerabilities, it is significantly outdated (current version is 20+). Since the package is used exclusively for TypeScript types in test helpers, updating to a recent version is harmless and recommended for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/package.json` around lines 38 - 48, Update the "stripe" dependency in
package.json from "8.222.0" to a recent stable v20+ release (or the latest
compatible minor) since it’s used only for TypeScript types in e2e test helpers;
modify the dependency entry for "stripe" in the package.json manifest so the
project uses the newer SDK version and then run npm/yarn install to regenerate
lockfiles and ensure types remain compatible with any test helpers that import
Stripe types.
apps/shade/src/components/ui/animated-number.tsx (1)

15-21: Consider a more meaningful Suspense fallback.

The current empty <div></div> fallback provides no visual feedback during loading. While this works, a brief flash of nothing might be slightly jarring for animated numbers.

💡 Optional: show a static placeholder
-const AnimatedNumber = (props: NumberFlowProps) => {
+const AnimatedNumber = ({value, ...props}: NumberFlowProps) => {
     return (
-        <Suspense fallback={<div></div>}>
+        <Suspense fallback={<span>{value}</span>}>
-            <NumberFlow {...props} />
+            <NumberFlow value={value} {...props} />
         </Suspense>
     );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/components/ui/animated-number.tsx` around lines 15 - 21, The
Suspense fallback for AnimatedNumber is an empty <div> which gives no visual
feedback; replace it with a small meaningful placeholder (for example a
visually-neutral static value or lightweight skeleton) so users see a stable
element while NumberFlow loads. Update the AnimatedNumber component to render
that placeholder in the Suspense fallback (referencing AnimatedNumber and
NumberFlow) and ensure the fallback is accessible (aria attributes or
aria-hidden as appropriate) and visually matches surrounding typography to avoid
layout shift.
.lintstagedrc.cjs (1)

62-67: Redundant .filter(Boolean) call.

Line 65 already filters out null values returned by buildScopedEslintCommand. The outer .filter(Boolean) on line 66 is redundant since the spread produces an array without additional nulls.

♻️ Suggested simplification
         return [
-            ...ESLINT_WORKSPACES
+            ...ESLINT_WORKSPACES
                 .map(workspace => buildScopedEslintCommand(workspace, workspaceGroups.get(workspace)))
                 .filter(Boolean)
-        ].filter(Boolean);
+        ];
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.lintstagedrc.cjs around lines 62 - 67, The returned array is being filtered
twice for falsy values; remove the redundant outer .filter(Boolean) so the
function returns [...ESLINT_WORKSPACES.map(workspace =>
buildScopedEslintCommand(workspace,
workspaceGroups.get(workspace))).filter(Boolean)] (i.e., keep the inner
.filter(Boolean) that removes nulls from buildScopedEslintCommand results and
delete the final .filter(Boolean)); locate the code using the symbols
ESLINT_WORKSPACES, buildScopedEslintCommand, and workspaceGroups to make this
change.
ghost/admin/ember-cli-build.js (1)

268-272: Inconsistent asset import path: consider using require.resolve() for consistency.

The @tryghost/koenig-lexical import uses a hardcoded node_modules/ path while other assets in the same file use require.resolve() (including reframe.js/dist/noframe.js on line 260, which follows the same pattern). This inconsistency can be resolved by using require.resolve() to support different package manager layouts without code changes.

♻️ Suggested fix
     if (app.env === 'development' || app.env === 'test') {
         // pull dynamic imports into the assets folder so that they can be lazy-loaded in tests
         // also done in development env so http://localhost:4200/tests works
-        app.import('node_modules/@tryghost/koenig-lexical/dist/koenig-lexical.umd.js', {outputFile: 'ghost/assets/koenig-lexical/koenig-lexical.umd.js'});
+        app.import(require.resolve('@tryghost/koenig-lexical/dist/koenig-lexical.umd.js'), {outputFile: 'ghost/assets/koenig-lexical/koenig-lexical.umd.js'});
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/admin/ember-cli-build.js` around lines 268 - 272, The app.import call
that pulls in '@tryghost/koenig-lexical/dist/koenig-lexical.umd.js' uses a
hardcoded 'node_modules/...' path; update the import to use require.resolve() so
package manager layouts are handled consistently—replace the string argument
passed to app.import in the same env block with the result of
require.resolve('@tryghost/koenig-lexical/dist/koenig-lexical.umd.js') while
keeping the existing { outputFile:
'ghost/assets/koenig-lexical/koenig-lexical.umd.js' } options and the
surrounding if (app.env === 'development' || app.env === 'test') block intact.
apps/announcement-bar/README.md (1)

7-8: Consider removing the redundant install step.

With pnpm workspaces, running pnpm install at the monorepo root installs dependencies for all workspace packages. The second install in this directory is likely unnecessary. Other READMEs in this PR (e.g., apps/comments-ui/README.md) only mention the root install.

📝 Suggested change
 ### Pre-requisites
 
-- Run `pnpm install` in Ghost monorepo root
-- Run `pnpm install` in this directory
++ Run `pnpm install` in Ghost monorepo root
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/announcement-bar/README.md` around lines 7 - 8, The README duplicates
installation instructions by telling users to run "pnpm install" both at the
monorepo root and again in the package directory; remove the redundant second
step so the instructions match other package READMEs (e.g.,
apps/comments-ui/README.md) and only instruct users to run "pnpm install" at the
monorepo root (or clarify if a separate per-package install is actually
required).
apps/sodo-search/README.md (1)

7-8: Same redundant install step as other READMEs.

Consider aligning with apps/comments-ui/README.md which only requires the root install with pnpm workspaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/sodo-search/README.md` around lines 7 - 8, The README contains a
redundant local install instruction—remove the second bullet "Run `pnpm install`
in this directory" and keep only the root workspace instruction (as in
apps/comments-ui/README.md) so users run a single `pnpm install` at the monorepo
root; update the README bullets to reflect that only the root pnpm workspace
install is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/scripts/dependency-inspector.js:
- Around line 201-237: The filter and mapping in parsePnpmOutdatedOutput are
using a non-existent info.current field (causing undefined values and
always-true filters); update parsePnpmOutdatedOutput to use info.wanted (the
installed/version-in-constraint value returned by pnpm) instead: change the
filter to compare info.wanted !== info.latest (or other appropriate comparison
using wanted/latest), and in the map return info.wanted in place of the current
column (i.e., packageName, info.wanted, info.wanted, info.latest,
info.dependencyType or packageName, info.wanted, info.wanted, info.latest,
info.dependencyType depending on the expected CSV order) so no undefined current
values are emitted.

In @.github/scripts/install-deps.sh:
- Around line 34-43: The command substitution that sets sqlite3_dir currently
calls process.exit(1) on error which aborts the whole script under set -e and
prevents the fallback; modify the Node snippet used for sqlite3_dir so it does
not call process.exit on failure but instead tries two resolution locations
(first require.resolve('sqlite3/package.json', {paths:
[path.resolve('ghost/core')]}) or equivalent, then a fallback require.resolve
from project root) and if both fail simply write an empty string (or nothing) to
stdout; update the Node block that uses require.resolve to return empty output
on failure rather than exiting so the surrounding shell script can continue and
execute the existing fallback logic that checks sqlite3_dir.

In @.github/workflows/ci.yml:
- Line 271: The job output casing is mismatched: change all references of
needs.job_setup.outputs.BASE_COMMIT to the actual output name
needs.job_setup.outputs.base_commit (e.g., in the pnpm nx affected -t lint
command and the other occurrences) so the job_setup output (base_commit) is read
correctly; search for any uses of needs.job_setup.outputs.BASE_COMMIT and
replace them with needs.job_setup.outputs.base_commit.
- Around line 107-110: The CI's shared file list (anchor "shared") omits pnpm
config files; update the shared anchor entries to include '.npmrc' and
'pnpm-workspace.yaml' so change detection triggers on those files, and then add
those same filenames to any cache hashing or cache key inputs used by workflow
jobs (the places that build cache keys from the shared list) so the dependency
install cache reflects changes to these pnpm configuration files.

In `@apps/admin/vite.config.ts`:
- Around line 46-48: The skip callback passed to tsconfigPaths currently uses an
exact equality check (dir === '.pnpm-store') which fails when dir is an absolute
or relative path; update the skip implementation used in the tsconfigPaths(...)
plugin so it detects the directory robustly (for example, use
dir.includes('.pnpm-store') or check path.basename(dir) === '.pnpm-store') and
import/use Node's path.basename if choosing the basename approach; update the
tsconfigPaths skip callback referenced in the vite config to use one of these
more reliable checks.

In `@docs/plans/2026-03-26-pnpm-migration-design.md`:
- Around line 51-55: Replace user-specific absolute paths with
repository-relative or placeholder paths in the design doc and any referenced
code examples: update occurrences of
`/Users/jonatan/.../Monorepo/ghost/admin/ember-cli-build.js` and
`/Users/jonatan/.../Monorepo/ghost/admin/lib/asset-delivery/index.js` to use
relative paths (e.g., `admin/ember-cli-build.js`,
`admin/lib/asset-delivery/index.js`) or repository-root placeholders, and ensure
the note about `lodash/camelCase` references the module and
`ghost/admin/package.json` so it highlights the missing dependency rather than
exposing local filesystem details.
- Around line 40-45: Replace the developer-specific absolute paths in the docs
with repository-relative paths: find the entries that currently mention a local
home prefix and edit them to reference the repo files by their relative names
(e.g., package.json at repo root, docker/development.entrypoint.sh,
docker/ghost-dev/Dockerfile, .github/scripts/install-deps.sh,
.github/workflows/ci.yml, ghost/core/package.json, and
ghost/core/monobundle.js); ensure you remove the /Users/... prefix and verify
links/rendering still work and are consistent with the repo root convention.

In `@docs/plans/2026-03-26-pnpm-migration.md`:
- Line 13: The headings jump from the document title (H1) directly to "### Task
1: Create pnpm workspace and root package-manager config" (H3); change that
specific heading text "### Task 1: Create pnpm workspace and root
package-manager config" to an H2 (use "##") so the outline flows H1 → H2 → H3
and satisfies markdownlint MD001, and scan the surrounding headings (e.g., any
other "###" under Task 1) to ensure the heading hierarchy remains consistent.
- Around line 34-40: The plan uses absolute workstation paths when instructing
to add a packageManager field (e.g. "/Users/jonatan/.codex/.../package.json");
update the document to use repo-relative paths instead (e.g. "package.json",
"ghost/core/package.json") everywhere the path is referenced, including the
other occurrences around lines noted (68-70). Ensure the instruction text and
the example JSON snippet refer to the repo-relative files and state to use the
exact installed pnpm version (packageManager: "pnpm@<chosen-version>") so the
doc remains portable.

In `@e2e/README.md`:
- Around line 14-20: The Quick Start snippet currently runs only "pnpm install"
and "pnpm test" but omits starting the required infra; update the README Quick
Start to include a step to boot the infra before running tests (e.g., run "pnpm
dev" or "pnpm --filter `@tryghost/e2e` infra:up") so first-time users don’t hit
the infra-not-running failure—insert this command immediately after "pnpm
install" and before "pnpm test" and add a short note that infra must be running
as referenced later in the README.

In `@ghost/parse-email-address/README.md`:
- Around line 35-36: Update the README bullet descriptions to improve grammar
and readability: change the line containing "`pnpm lint` run just eslint" to
"`pnpm lint` runs only ESLint" and change the line containing "`pnpm test` run
lint and tests" to "`pnpm test` runs lint and tests" so the verbs and casing are
consistent and clear (locate these exact strings in README.md to edit).

---

Outside diff comments:
In `@apps/admin-x-design-system/package.json`:
- Around line 64-90: Remove the duplicate "validator" entry from the wrong
dependency section: keep a single entry in package.json under the appropriate
key (either "dependencies" or "devDependencies") depending on runtime usage;
specifically remove the "validator": "13.12.0" line from the devDependencies
block if it is needed at runtime (or remove the one in dependencies if it is
only for tests/dev), ensuring only one "validator" entry remains and
package.json JSON stays valid.

---

Nitpick comments:
In @.lintstagedrc.cjs:
- Around line 62-67: The returned array is being filtered twice for falsy
values; remove the redundant outer .filter(Boolean) so the function returns
[...ESLINT_WORKSPACES.map(workspace => buildScopedEslintCommand(workspace,
workspaceGroups.get(workspace))).filter(Boolean)] (i.e., keep the inner
.filter(Boolean) that removes nulls from buildScopedEslintCommand results and
delete the final .filter(Boolean)); locate the code using the symbols
ESLINT_WORKSPACES, buildScopedEslintCommand, and workspaceGroups to make this
change.

In `@apps/admin-x-framework/tsconfig.declaration.json`:
- Around line 4-11: The "compilerOptions" block has inconsistent indentation:
"noEmit" is indented with 4 spaces while the other keys ("composite",
"declaration", "declarationMap", "declarationDir", "emitDeclarationOnly",
"tsBuildInfoFile", "rootDir") use 8 spaces; make all properties use the same
indentation style (choose either 4 or 8 spaces) and update the "noEmit" line to
match the others so the entire compilerOptions object is consistently indented.

In `@apps/announcement-bar/README.md`:
- Around line 7-8: The README duplicates installation instructions by telling
users to run "pnpm install" both at the monorepo root and again in the package
directory; remove the redundant second step so the instructions match other
package READMEs (e.g., apps/comments-ui/README.md) and only instruct users to
run "pnpm install" at the monorepo root (or clarify if a separate per-package
install is actually required).

In `@apps/shade/src/components/ui/animated-number.tsx`:
- Around line 15-21: The Suspense fallback for AnimatedNumber is an empty <div>
which gives no visual feedback; replace it with a small meaningful placeholder
(for example a visually-neutral static value or lightweight skeleton) so users
see a stable element while NumberFlow loads. Update the AnimatedNumber component
to render that placeholder in the Suspense fallback (referencing AnimatedNumber
and NumberFlow) and ensure the fallback is accessible (aria attributes or
aria-hidden as appropriate) and visually matches surrounding typography to avoid
layout shift.

In `@apps/sodo-search/README.md`:
- Around line 7-8: The README contains a redundant local install
instruction—remove the second bullet "Run `pnpm install` in this directory" and
keep only the root workspace instruction (as in apps/comments-ui/README.md) so
users run a single `pnpm install` at the monorepo root; update the README
bullets to reflect that only the root pnpm workspace install is required.

In `@e2e/package.json`:
- Around line 38-48: Update the "stripe" dependency in package.json from
"8.222.0" to a recent stable v20+ release (or the latest compatible minor) since
it’s used only for TypeScript types in e2e test helpers; modify the dependency
entry for "stripe" in the package.json manifest so the project uses the newer
SDK version and then run npm/yarn install to regenerate lockfiles and ensure
types remain compatible with any test helpers that import Stripe types.

In `@ghost/admin/ember-cli-build.js`:
- Around line 268-272: The app.import call that pulls in
'@tryghost/koenig-lexical/dist/koenig-lexical.umd.js' uses a hardcoded
'node_modules/...' path; update the import to use require.resolve() so package
manager layouts are handled consistently—replace the string argument passed to
app.import in the same env block with the result of
require.resolve('@tryghost/koenig-lexical/dist/koenig-lexical.umd.js') while
keeping the existing { outputFile:
'ghost/assets/koenig-lexical/koenig-lexical.umd.js' } options and the
surrounding if (app.env === 'development' || app.env === 'test') block intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a1f6114-4f7f-49f0-b876-80b6ce96b227

📥 Commits

Reviewing files that changed from the base of the PR and between d5be59a and 4a419a4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (91)
  • .github/CONTRIBUTING.md
  • .github/actions/restore-cache/action.yml
  • .github/hooks/pre-commit
  • .github/scripts/check-app-version-bump.js
  • .github/scripts/clean.js
  • .github/scripts/dependency-inspector.js
  • .github/scripts/install-deps.sh
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • .lintstagedrc.cjs
  • .npmrc
  • AGENTS.md
  • apps/activitypub/package.json
  • apps/admin-x-design-system/README.md
  • apps/admin-x-design-system/package.json
  • apps/admin-x-design-system/src/global/form/code-editor-view.tsx
  • apps/admin-x-design-system/src/global/modal/confirmation-modal.tsx
  • apps/admin-x-design-system/src/global/modal/limit-modal.tsx
  • apps/admin-x-design-system/src/global/modal/preview-modal.tsx
  • apps/admin-x-design-system/src/utils/format-url.ts
  • apps/admin-x-framework/README.md
  • apps/admin-x-framework/package.json
  • apps/admin-x-framework/src/playwright.ts
  • apps/admin-x-framework/tsconfig.declaration.json
  • apps/admin-x-settings/README.md
  • apps/admin-x-settings/package.json
  • apps/admin/README.md
  • apps/admin/vite.config.ts
  • apps/announcement-bar/README.md
  • apps/announcement-bar/package.json
  • apps/comments-ui/README.md
  • apps/comments-ui/package.json
  • apps/comments-ui/playwright.config.ts
  • apps/portal/README.md
  • apps/portal/package.json
  • apps/posts/package.json
  • apps/shade/AGENTS.md
  • apps/shade/README.md
  • apps/shade/package.json
  • apps/shade/src/components/ui/animated-number.tsx
  • apps/shade/src/docs/contributing.mdx
  • apps/shade/src/lib/utils.ts
  • apps/shade/tsconfig.declaration.json
  • apps/signup-form/README.md
  • apps/signup-form/package.json
  • apps/signup-form/playwright.config.ts
  • apps/sodo-search/README.md
  • apps/sodo-search/package.json
  • apps/stats/README.md
  • apps/stats/package.json
  • docker/development.entrypoint.sh
  • docker/ghost-dev/Dockerfile
  • docker/ghost-dev/README.md
  • docker/watch-admin-apps.js
  • docs/README.md
  • docs/plans/2026-03-26-pnpm-migration-design.md
  • docs/plans/2026-03-26-pnpm-migration.md
  • e2e/AGENTS.md
  • e2e/README.md
  • e2e/data-factory/README.md
  • e2e/helpers/environment/constants.ts
  • e2e/helpers/environment/service-managers/ghost-manager.ts
  • e2e/helpers/services/stripe/fake-stripe-server.ts
  • e2e/package.json
  • e2e/scripts/prepare-ci-e2e-job.sh
  • e2e/scripts/run-playwright-container.sh
  • e2e/scripts/run-playwright-host.sh
  • e2e/visual-regression/playwright.config.ts
  • ghost/admin/README.md
  • ghost/admin/ember-cli-build.js
  • ghost/admin/lib/asset-delivery/package.json
  • ghost/admin/package.json
  • ghost/core/bin/create-migration.js
  • ghost/core/bin/generate-golden-email.js
  • ghost/core/core/server/data/tinybird/README.md
  • ghost/core/core/server/data/tinybird/scripts/README.md
  • ghost/core/core/server/data/tinybird/scripts/benchmark-top-pages.sh
  • ghost/core/core/server/data/tinybird/scripts/compare-top-pages.sh
  • ghost/core/core/server/data/tinybird/scripts/docker-analytics-manager.js
  • ghost/core/core/server/services/link-redirection/README.md
  • ghost/core/monobundle.js
  • ghost/core/package.json
  • ghost/core/test/e2e-browser/README.md
  • ghost/i18n/README.md
  • ghost/i18n/generate-context.js
  • ghost/i18n/package.json
  • ghost/parse-email-address/README.md
  • ghost/parse-email-address/package.json
  • package.json
  • pnpm-workspace.yaml

Comment on lines +201 to +237
const result = execSync('corepack pnpm outdated --recursive --format json', {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output
});

const lines = result.trim().split('\n');
const outdatedData = [];

for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.type === 'table' && data.data && data.data.body) {
outdatedData.push(...data.data.body);
}
} catch (e) {
// Skip non-JSON lines
}
}

return outdatedData;
return this.parsePnpmOutdatedOutput(result);
} catch (error) {
// yarn outdated exits with code 1 when there are outdated packages
// pnpm outdated exits with code 1 when there are outdated packages
if (error.status === 1 && error.stdout) {
const lines = error.stdout.trim().split('\n');
const outdatedData = [];

for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.type === 'table' && data.data && data.data.body) {
outdatedData.push(...data.data.body);
}
} catch (e) {
// Skip non-JSON lines
}
}

return outdatedData;
return this.parsePnpmOutdatedOutput(error.stdout);
} else {
console.error('Failed to run yarn outdated:', error.message);
console.error('Failed to run pnpm outdated:', error.message);
return [];
}
}
}

parsePnpmOutdatedOutput(output) {
const trimmedOutput = output.trim();

if (!trimmedOutput) {
return [];
}

const outdatedData = JSON.parse(trimmedOutput);

return Object.entries(outdatedData)
.filter(([, info]) => info.current !== info.wanted || info.current !== info.latest)
.map(([packageName, info]) => {
return [
packageName,
info.current,
info.wanted,
info.latest,
info.dependencyType
];
});
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

output="$(corepack pnpm outdated --recursive --format json 2>/dev/null || true)"

python - <<'PY' "$output"
import json, sys
payload = sys.argv[1].strip()
print("empty output:", not bool(payload))
if not payload:
    raise SystemExit(0)

data = json.loads(payload)
print("top-level type:", type(data).__name__)

if isinstance(data, list) and data:
    first = data[0]
    print("first entry keys:", sorted(first.keys()))
    print("package field:", first.get("packageName") or first.get("name"))
elif isinstance(data, dict):
    print("sample top-level keys:", list(data.keys())[:5])
PY

Repository: TryGhost/Ghost

Length of output: 118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Try to run pnpm outdated and save to file
output_file="/tmp/pnpm_outdated.json"
corepack pnpm outdated --recursive --format json > "$output_file" 2>/dev/null || echo "pnpm command exited with code: $?"

# Check if file exists and has content
if [ -s "$output_file" ]; then
  echo "Output size: $(wc -c < "$output_file") bytes"
  echo "First 500 chars of output:"
  head -c 500 "$output_file"
  echo ""
  echo ""
  echo "Attempting to parse structure..."
  python3 <<'PY'
import json

with open('/tmp/pnpm_outdated.json', 'r') as f:
    data = json.load(f)

print(f"Top-level type: {type(data).__name__}")

if isinstance(data, list):
    print(f"Array length: {len(data)}")
    if data:
        print(f"First record keys: {sorted(data[0].keys())}")
        print(f"First record sample: {json.dumps(data[0], indent=2)[:300]}")
elif isinstance(data, dict):
    print(f"Dict keys (first 5): {list(data.keys())[:5]}")
    # Try to understand the structure
    first_key = next(iter(data.keys()))
    print(f"Value type for key '{first_key}': {type(data[first_key]).__name__}")
    print(f"Sample value: {json.dumps(data[first_key], indent=2)[:300] if isinstance(data[first_key], dict) else str(data[first_key])[:300]}")
PY
else
  echo "No output from pnpm outdated command"
fi

Repository: TryGhost/Ghost

Length of output: 1408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Run pnpm outdated and capture output
output_file="/tmp/pnpm_outdated.json"
corepack pnpm outdated --recursive --format json 2>/dev/null > "$output_file" || true

# Find where JSON starts (first {)
json_start=$(grep -n "^{" "$output_file" | head -1 | cut -d: -f1)
if [ -n "$json_start" ]; then
  tail -n +$json_start "$output_file" > "$output_file.clean"
  
  echo "Parsing cleaned JSON..."
  python3 <<'PY'
import json

try:
  with open('/tmp/pnpm_outdated.json.clean', 'r') as f:
    data = json.load(f)
  
  print(f"Top-level type: {type(data).__name__}")
  
  if isinstance(data, dict):
    sample_keys = list(data.keys())[:3]
    print(f"Sample dict keys: {sample_keys}")
    
    for key in sample_keys:
      value = data[key]
      print(f"\nKey '{key}' value type: {type(value).__name__}")
      if isinstance(value, dict):
        print(f"  Fields in value: {sorted(value.keys())}")
        print(f"  Value sample: {json.dumps({k: (v if not isinstance(v, list) else f'[{len(v)} items]') for k, v in list(value.items())[:5]}, indent=2)}")
except json.JSONDecodeError as e:
  print(f"JSON parse error: {e}")
  # Show first 1000 chars
  with open('/tmp/pnpm_outdated.json.clean', 'r') as f:
    print(f"First 1000 chars:\n{f.read()[:1000]}")
PY
else
  echo "Could not find JSON start"
fi

Repository: TryGhost/Ghost

Length of output: 1122


🏁 Script executed:

#!/bin/bash
# Read the actual code snippet from the file
cat -n .github/scripts/dependency-inspector.js | sed -n '201,237p'

Repository: TryGhost/Ghost

Length of output: 1392


Fix missing current field in filter and map logic.

The code references info.current on lines 228 and 232, but pnpm outdated --format json does not include a current field in its output. The actual structure is {[packageName]: {latest, wanted, isDeprecated, dependencyType, dependentPackages}}. Accessing the non-existent info.current returns undefined, which causes the filter to always match (since undefined !== wanted is always true), and the returned array will contain undefined for the current version, corrupting the output.

Fix: Replace info.current with info.wanted (the version within the semver constraint) or determine the correct field name from pnpm's actual output schema. Simplify the filter logic accordingly.

🛠️ Proposed fix
    return Object.entries(outdatedData)
-      .filter(([, info]) => info.current !== info.wanted || info.current !== info.latest)
+      .filter(([, info]) => info.wanted !== info.latest)
      .map(([packageName, info]) => {
        return [
          packageName,
-         info.current,
+         info.wanted,
          info.wanted,
          info.latest,
          info.dependencyType
        ];
      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/scripts/dependency-inspector.js around lines 201 - 237, The filter
and mapping in parsePnpmOutdatedOutput are using a non-existent info.current
field (causing undefined values and always-true filters); update
parsePnpmOutdatedOutput to use info.wanted (the installed/version-in-constraint
value returned by pnpm) instead: change the filter to compare info.wanted !==
info.latest (or other appropriate comparison using wanted/latest), and in the
map return info.wanted in place of the current column (i.e., packageName,
info.wanted, info.wanted, info.latest, info.dependencyType or packageName,
info.wanted, info.wanted, info.latest, info.dependencyType depending on the
expected CSV order) so no undefined current values are emitted.

Comment on lines +34 to +43
sqlite3_dir="$(node <<'NODE'
const path = require('path');

try {
process.stdout.write(path.dirname(require.resolve('sqlite3/package.json')));
} catch (error) {
process.exit(1);
}
NODE
)"
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo-root resolution:"
node - <<'NODE'
try {
  console.log(require.resolve('sqlite3/package.json'));
} catch (e) {
  console.log('NOT_RESOLVED');
}
NODE

echo "ghost/core resolution:"
node - <<'NODE'
const path = require('path');
try {
  console.log(require.resolve('sqlite3/package.json', {
    paths: [path.resolve(process.cwd(), 'ghost/core')]
  }));
} catch (e) {
  console.log('NOT_RESOLVED');
}
NODE

echo "set -e with a failing command substitution:"
if bash -euo pipefail -c 'x=$(node -e "process.exit(1)"); echo UNREACHABLE'; then
  echo "unexpected: assignment succeeded"
else
  echo "expected: shell exits before the fallback branch"
fi

Repository: TryGhost/Ghost

Length of output: 221


🏁 Script executed:

head -n 70 .github/scripts/install-deps.sh | cat -n

Repository: TryGhost/Ghost

Length of output: 2354


Resolve sqlite3 from ghost/core and prevent script abort on missing module.

With set -e, the process.exit(1) inside the command substitution (line 40) causes the entire script to exit before the fallback logic on line 59 can execute. The fallback design assumes the script continues if sqlite3_dir is empty, but the current implementation aborts instead. Additionally, require.resolve() from the repo root may fail to find sqlite3 when it's declared in ghost/core under pnpm's isolated workspace layout.

🛠️ Proposed fix
 sqlite3_dir="$(node <<'NODE'
 const path = require('path');
 
 try {
-    process.stdout.write(path.dirname(require.resolve('sqlite3/package.json')));
+    process.stdout.write(path.dirname(require.resolve('sqlite3/package.json', {
+        paths: [path.resolve(process.cwd(), 'ghost/core')]
+    })));
 } catch (error) {
-    process.exit(1);
+    if (error.code !== 'MODULE_NOT_FOUND') {
+        throw error;
+    }
 }
 NODE
 )"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/scripts/install-deps.sh around lines 34 - 43, The command
substitution that sets sqlite3_dir currently calls process.exit(1) on error
which aborts the whole script under set -e and prevents the fallback; modify the
Node snippet used for sqlite3_dir so it does not call process.exit on failure
but instead tries two resolution locations (first
require.resolve('sqlite3/package.json', {paths: [path.resolve('ghost/core')]})
or equivalent, then a fallback require.resolve from project root) and if both
fail simply write an empty string (or nothing) to stdout; update the Node block
that uses require.resolve to return empty output on failure rather than exiting
so the surrounding shell script can continue and execute the existing fallback
logic that checks sqlite3_dir.

Comment on lines 107 to +110
shared: &shared
- '.github/**'
- 'package.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Root pnpm inputs present:"
for f in package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
  [ -e "$f" ] && echo " - $f"
done

echo
echo "Current shared filter + hash inputs:"
sed -n '102,156p' .github/workflows/ci.yml

Repository: TryGhost/Ghost

Length of output: 1979


Include .npmrc and pnpm-workspace.yaml in CI change detection and cache hashing.

.npmrc and pnpm-workspace.yaml are pnpm configuration files that influence workspace setup and dependency installation. Currently, they're absent from the shared filters, so a PR modifying only these files will skip all affected jobs despite changing the dependency graph. The hash functions should also include them to keep cache keys aligned with actual install inputs.

Suggested patch
@@
             shared: &shared
               - '.github/**'
               - 'package.json'
+              - '.npmrc'
+              - 'pnpm-workspace.yaml'
               - 'pnpm-lock.yaml'
@@
-        run: echo "hash=lockfile-${{ hashFiles('pnpm-lock.yaml') }}" >> "$GITHUB_ENV"
+        run: echo "hash=lockfile-${{ hashFiles('pnpm-lock.yaml', 'package.json', '.npmrc', 'pnpm-workspace.yaml') }}" >> "$GITHUB_ENV"
@@
-        run: echo "cachekey=dep-cache-${{ hashFiles('pnpm-lock.yaml') }}-${{ env.HEAD_COMMIT }}" >> "$GITHUB_ENV"
+        run: echo "cachekey=dep-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json', '.npmrc', 'pnpm-workspace.yaml') }}-${{ env.HEAD_COMMIT }}" >> "$GITHUB_ENV"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 107 - 110, The CI's shared file list
(anchor "shared") omits pnpm config files; update the shared anchor entries to
include '.npmrc' and 'pnpm-workspace.yaml' so change detection triggers on those
files, and then add those same filenames to any cache hashing or cache key
inputs used by workflow jobs (the places that build cache keys from the shared
list) so the dependency install cache reflects changes to these pnpm
configuration files.

key: eslint-cache

- run: yarn nx affected -t lint --base=${{ needs.job_setup.outputs.BASE_COMMIT }}
- run: pnpm nx affected -t lint --base=${{ needs.job_setup.outputs.BASE_COMMIT }}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Defined job outputs:"
sed -n '201,215p' .github/workflows/ci.yml

echo
echo "References to the base commit output:"
rg -n "needs\\.job_setup\\.outputs\\.(BASE_COMMIT|base_commit)" .github/workflows/ci.yml

Repository: TryGhost/Ghost

Length of output: 1334


🌐 Web query:

GitHub Actions job outputs casing normalization uppercase needs context

💡 Result:

GitHub Actions job outputs accessed via the needs context (e.g., ${{ needs.job1.outputs.output1 }}) are case-sensitive. Job IDs (keys under needs or jobs.<job_id>.outputs) and output names must match the casing defined in the workflow YAML exactly. No automatic normalization to uppercase occurs for job outputs in the needs context. Official documentation consistently uses lowercase casing in examples for job IDs like "job1" and outputs like "output1", with no mention of casing insensitivity or normalization for these identifiers (1,10,11,12). Property dereference syntax requires exact alphanumeric matching, starting with letter or _ (11). Some GitHub Actions contexts exhibit case-insensitivity (e.g., secrets context, string comparisons in expressions, function names like toJSON), but this does not extend to job/output identifiers in needs (6). Action inputs have environment variable normalization to UPPER_SNAKE_CASE (e.g., file-path -> INPUT_FILE_PATH), but this is specific to action inputs, not job outputs (3,9). If casing mismatch occurs (e.g., defining Output1 but accessing needs.job1.outputs.output1), the access returns empty string or fails. To access, ensure exact casing match in jobs.<job_id>.outputs.<output_name> definition and needs.<job_id>.outputs.<output_name> usage. The needs context provides outputs from directly dependent jobs only (11). Example from docs: jobs: job1: outputs: output1: ${{ steps.step1.outputs.test }} # lowercase job2: needs: job1 steps: - env: MY_OUTPUT: ${{ needs.job1.outputs.output1 }} # must match casing No evidence of uppercase normalization requirement or "needs context" specifically; mismatches likely cause access failures, prompting casing checks.

Citations:


Fix casing mismatch between job output definition and references.

The job job_setup defines the output as base_commit (lowercase), but lines 271, 449, and 458 reference it as BASE_COMMIT (uppercase). GitHub Actions job outputs are case-sensitive with no automatic normalization. The mismatched casing will cause these references to evaluate to an empty string, resulting in --base= being passed to pnpm nx, which prevents proper affected-project selection.

Change all references from needs.job_setup.outputs.BASE_COMMIT to needs.job_setup.outputs.base_commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 271, The job output casing is mismatched:
change all references of needs.job_setup.outputs.BASE_COMMIT to the actual
output name needs.job_setup.outputs.base_commit (e.g., in the pnpm nx affected
-t lint command and the other occurrences) so the job_setup output (base_commit)
is read correctly; search for any uses of needs.job_setup.outputs.BASE_COMMIT
and replace them with needs.job_setup.outputs.base_commit.

Comment on lines +46 to +48
plugins: [tailwindcss() as PluginOption, react(), emberAssetsPlugin(), ghostBackendProxyPlugin(), deepLinksPlugin(), tsconfigPaths({
skip: dir => dir === '.pnpm-store'
})],
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check vite-tsconfig-paths documentation or source for how 'dir' is passed to skip callback
rg -n "skip" --type ts -A5 -B5 -g '**/vite-tsconfig-paths/**'

Repository: TryGhost/Ghost

Length of output: 206


🏁 Script executed:

cat -n apps/admin/vite.config.ts

Repository: TryGhost/Ghost

Length of output: 3109


🌐 Web query:

vite-tsconfig-paths skip callback parameter documentation

💡 Result:

The skip option in the vite-tsconfig-paths plugin is a callback function that determines which directories to skip when searching for tsconfig.json files. It receives a single parameter: the directory path (dir: string) as a string, and must return a boolean (true to skip the directory, false to include it). Usage example in vite.config.ts: import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ plugins: [tsconfigPaths({ skip: (dir) => dir.includes('some-directory-to-skip') })], }) Note: .git and node_modules directories are always skipped regardless of this option. This is from the official plugin documentation (v6+), the most recent and maintained fork as of 2026.

Citations:


🏁 Script executed:

rg "skip.*dir" --type ts -A2 -B2

Repository: TryGhost/Ghost

Length of output: 929


Consider using a more robust path check for the skip callback.

The current exact string match dir === '.pnpm-store' may not work reliably if the dir parameter is passed as an absolute or relative full path. The official vite-tsconfig-paths documentation shows using dir.includes('some-directory-to-skip') for this case. Use either dir.includes('.pnpm-store') or path.basename(dir) === '.pnpm-store' for robustness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/vite.config.ts` around lines 46 - 48, The skip callback passed to
tsconfigPaths currently uses an exact equality check (dir === '.pnpm-store')
which fails when dir is an absolute or relative path; update the skip
implementation used in the tsconfigPaths(...) plugin so it detects the directory
robustly (for example, use dir.includes('.pnpm-store') or check
path.basename(dir) === '.pnpm-store') and import/use Node's path.basename if
choosing the basename approach; update the tsconfigPaths skip callback
referenced in the vite config to use one of these more reliable checks.

Comment on lines +51 to +55
- `/Users/jonatan/.codex/worktrees/393f/Monorepo/ghost/admin/ember-cli-build.js` imports assets from `node_modules/...`, which is brittle under pnpm.

2. Missing or indirect dependency usage
- `/Users/jonatan/.codex/worktrees/393f/Monorepo/ghost/admin/lib/asset-delivery/index.js` requires `lodash/camelCase`, but `ghost/admin/package.json` does not declare `lodash`.
- Old Ember addons and test tooling are likely relying on undeclared peers/transitives that Yarn's flatter tree currently exposes.
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.

⚠️ Potential issue | 🟡 Minor

Same issue: absolute local paths should be relative.

These paths also expose the developer's local environment.

📝 Suggested fix
 1. Hardcoded package layout assumptions
-   - `/Users/jonatan/.codex/worktrees/393f/Monorepo/ghost/admin/ember-cli-build.js` imports assets from `node_modules/...`, which is brittle under pnpm.
+   - `ghost/admin/ember-cli-build.js` imports assets from `node_modules/...`, which is brittle under pnpm.
 
 2. Missing or indirect dependency usage
-   - `/Users/jonatan/.codex/worktrees/393f/Monorepo/ghost/admin/lib/asset-delivery/index.js` requires `lodash/camelCase`, but `ghost/admin/package.json` does not declare `lodash`.
+   - `ghost/admin/lib/asset-delivery/index.js` requires `lodash/camelCase`, but `ghost/admin/package.json` does not declare `lodash`.
🧰 Tools
🪛 LanguageTool

[grammar] ~55-~55: Ensure spelling is correct
Context: ...es not declare lodash. - Old Ember addons and test tooling are likely relying on ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-26-pnpm-migration-design.md` around lines 51 - 55, Replace
user-specific absolute paths with repository-relative or placeholder paths in
the design doc and any referenced code examples: update occurrences of
`/Users/jonatan/.../Monorepo/ghost/admin/ember-cli-build.js` and
`/Users/jonatan/.../Monorepo/ghost/admin/lib/asset-delivery/index.js` to use
relative paths (e.g., `admin/ember-cli-build.js`,
`admin/lib/asset-delivery/index.js`) or repository-root placeholders, and ensure
the note about `lodash/camelCase` references the module and
`ghost/admin/package.json` so it highlights the missing dependency rather than
exposing local filesystem details.


---

### Task 1: Create pnpm workspace and root package-manager config
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.

⚠️ Potential issue | 🟡 Minor

Fix the heading jump.

Line 13 goes from an H1 straight to H3, which trips markdownlint MD001 and makes the outline harder to scan.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-26-pnpm-migration.md` at line 13, The headings jump from
the document title (H1) directly to "### Task 1: Create pnpm workspace and root
package-manager config" (H3); change that specific heading text "### Task 1:
Create pnpm workspace and root package-manager config" to an H2 (use "##") so
the outline flows H1 → H2 → H3 and satisfies markdownlint MD001, and scan the
surrounding headings (e.g., any other "###" under Task 1) to ensure the heading
hierarchy remains consistent.

Comment on lines +34 to +40
Add a `packageManager` field to `/Users/jonatan/.codex/worktrees/393f/Monorepo/package.json`:

```json
"packageManager": "pnpm@<chosen-version>"
```

Use the exact version installed for the migration.
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.

⚠️ Potential issue | 🟡 Minor

Use repo-relative paths in the plan.

The hardcoded /Users/jonatan/.codex/... paths only work on one workstation and will go stale immediately in a checked-in doc. Use repo-relative paths like package.json and ghost/core/package.json throughout instead.

Also applies to: 68-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-26-pnpm-migration.md` around lines 34 - 40, The plan uses
absolute workstation paths when instructing to add a packageManager field (e.g.
"/Users/jonatan/.codex/.../package.json"); update the document to use
repo-relative paths instead (e.g. "package.json", "ghost/core/package.json")
everywhere the path is referenced, including the other occurrences around lines
noted (68-70). Ensure the instruction text and the example JSON snippet refer to
the repo-relative files and state to use the exact installed pnpm version
(packageManager: "pnpm@<chosen-version>") so the doc remains portable.

Comment on lines 14 to 20
```bash
# Install dependencies
yarn
pnpm install

# All tests
yarn test
pnpm test
```
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.

⚠️ Potential issue | 🟡 Minor

Quick Start still skips the required infra bootstrap.

The opening recipe says pnpm install then pnpm test, but later on Line 198 the README says infra must already be running. First-time users will hit a failing run unless Quick Start also includes pnpm dev or pnpm --filter @tryghost/e2e infra:up.

📝 Suggested README tweak
 # Install dependencies
 pnpm install
+
+# From the repo root, start the required services
+pnpm dev
+# or: pnpm --filter `@tryghost/e2e` infra:up
 
 # All tests
 pnpm test

Also applies to: 198-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/README.md` around lines 14 - 20, The Quick Start snippet currently runs
only "pnpm install" and "pnpm test" but omits starting the required infra;
update the README Quick Start to include a step to boot the infra before running
tests (e.g., run "pnpm dev" or "pnpm --filter `@tryghost/e2e` infra:up") so
first-time users don’t hit the infra-not-running failure—insert this command
immediately after "pnpm install" and before "pnpm test" and add a short note
that infra must be running as referenced later in the README.

Comment on lines +35 to +36
- `pnpm lint` run just eslint
- `pnpm test` run lint and tests
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.

⚠️ Potential issue | 🟡 Minor

Polish command descriptions for grammar/readability.

The command switch is correct, but the wording is a bit off. Consider: “pnpm lint runs only ESLint” and “pnpm test runs lint and tests.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/parse-email-address/README.md` around lines 35 - 36, Update the README
bullet descriptions to improve grammar and readability: change the line
containing "`pnpm lint` run just eslint" to "`pnpm lint` runs only ESLint" and
change the line containing "`pnpm test` run lint and tests" to "`pnpm test` runs
lint and tests" so the verbs and casing are consistent and clear (locate these
exact strings in README.md to edit).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant