Skip to content

Adopt monorepo structure#2565

Open
fredrikekelund wants to merge 52 commits intotrunkfrom
stu-1288-adopt-monorepo-structure
Open

Adopt monorepo structure#2565
fredrikekelund wants to merge 52 commits intotrunkfrom
stu-1288-adopt-monorepo-structure

Conversation

@fredrikekelund
Copy link
Contributor

@fredrikekelund fredrikekelund commented Feb 11, 2026

Related issues

Proposed Changes

Note

The grunt work in this PR was done using OpenAI Codex. 🚨 I know the size of the diff looks daunting, but aside from package-lock.json changes, this PR is mostly just moving files around, changing import paths and tweaking config files 🚨

Note

As noted in #2565 (comment), it's expected that the performance tests are failing.

Warning

A consequence of this change is that package-lock.json will no longer govern the version of dependencies we ship in production releases. That's because we now have a single lockfile for all npm workspaces, but we use only the package-specific package.json files to install the node_modules directories shipped in production (without a lockfile). This is probably fine, but it means we should be a bit more conservative about the semver ranges we use in package.json. For example, we cannot use ^3.0.46 for @wp-playground dependencies if the intention is to ship 3.0.46 to production. For those cases, we should save the exact version in package.json.

The CLI used to be more of an add-on to Studio, but as of v1.7.0, it's become integral to how Studio works. To reflect this, and to make the codebase easier to work with (for humans and AI agents), this PR adopts a proper monorepo structure.

So, what does "proper monorepo structure" mean here?

  • The CLI code is moved to apps/cli and the Electron app code is moved to apps/studio. They now live side by side.
  • The apps/cli code and the apps/studio code are now isolated – there are no cross imports.
  • ./common had been made into a proper package, using @studio/common as the package name. It now lives in tools/common.
    • We still use a Vite import alias for @studio/common, so it's not consumed as a true npm module. This is because we'd need to add package exports for everything we want to consume outside that package (i.e., for basically every function/type/class). This seemed tedious to me, but if it's good practice for whatever reason, we can easily revisit it later.
  • Other auxiliary tools have also been moved into tools/, like tools/eslint-plugin-studio.
  • We use npm workspaces to make it easy and convenient to install dependencies for all packages. Run npm install once in the repo root, and you're good to go.
    • This means that all packages have individual package.json files and npm reconciles and dedupes everything into a single node_modules directory and a single package-lock.json file.
  • The CLI and the app have special requirements for packaging (see pfHvTO-XV-p2#comment-820), namely that they each need separate node_modules directories that get copied into the packaged output. We handle this with install:bundle npm scripts in apps/cli/package.json and apps/studio/package.json.
    • A notable caveat of this approach is that it mutates the apps/cli/node_modules and apps/studio/node_modules directories, which potentially messes up the npm workspace-powered dependency tree. So, if you run npm run package locally, you would typically need to rerun npm ci after the script finishes to reconcile the dependency tree. It's easy for other developers to be stumped by this requirement, so I addressed it by adding a scripts/package-in-isolation.ts script. The gist is that it copies the source files from the repo to a temporary directory, installs dependencies, runs install:bundle and the package/make script, and then copies the output back to the repo.

Testing Instructions

CI should pass, and the dev build should install fine

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@fredrikekelund fredrikekelund requested a review from a team February 11, 2026 10:52
@fredrikekelund fredrikekelund self-assigned this Feb 11, 2026
@fredrikekelund
Copy link
Contributor Author

The performance metrics CI job fails because it uses the same setup command on trunk as on this branch. There's no easy way to reconcile that, so I'd prefer to just let it fail in this PR and have it work for the next PR that runs after this one.

Copy link
Contributor

@gcsecsey gcsecsey left a comment

Choose a reason for hiding this comment

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

Thanks for taking on the task to reorganize to a monorepo @fredrikekelund, I think this is going to make working with the different packages easier on the long run.

Also thanks for the detailed description of the changes, these made reviewing much easier! 🙌

I could npm i from the project root as expected, and using npm ls -ws I can see the newly added packages in the workspace:

Image

When starting the app, I'm currently getting failures to start the site servers, it seems as if the cli package dependencies are not yet installed, or it's not built:

Node.js v22.12.0
  [Exit code] 1
    at ChildProcess.<anonymous> (/Users/gcsecsey/a8c-projects/studio/.conductor/melbourne/dist/main/index.js:62877:22)
    at ChildProcess.emit (node:events:519:28)
    at ChildProcess.emit (node:domain:489:12)
    at maybeClose (node:internal/child_process:1101:16)
    at Socket.<anonymous> (node:internal/child_process:456:11)
    at Socket.emit (node:events:519:28)
    at Socket.emit (node:domain:489:12)
    at Pipe.<anonymous> (node:net:346:12)
[2026-02-12T11:31:54.373Z][info][main] Sentry Logger [log]: Captured error event `[Base message] Failed to start site
  [stderr] node:internal/modules/cjs/loader:1252
  throw err;
  ^

Error: Cannot find module '/Users/gcsecsey/a8c-projects/studio/.conductor/melbourne/dist/cli/main.js'
    at Function._resolveFilename (node:internal/modules/cjs/loader:1249:15)
    at Function._load (node:internal/modules/cjs/loader:1075:27)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:219:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
    at node:internal/main/run_main_module:36:49 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

.gitignore Outdated
Comment on lines 115 to 117
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
apps/studio/e2e/imports/*.tar.gz
apps/studio/e2e/imports/*.zip
apps/studio/e2e/imports/*.wpress

/fastlane/README.md

# CLI npm artifacts
cli/vendor/
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
apps/cli/vendor/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was apparently added in #2033. @bcotrim, do you know if this is still needed? The directory doesn't exist locally for me, and I don't know when or how it'd be created.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure that was generated by Playground CLI at some point. Maybe it's not the case anymore

Comment on lines 8 to 10
const packageVersion = JSON.parse(
readFileSync( resolve( __dirname, 'package.json' ), 'utf-8' )
).version;
Copy link
Contributor

Choose a reason for hiding this comment

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

We didn't add a separate version field to the CLI package.json yet. We could either add one, or point this to the root package.json if we want to have a canonical version.

Suggested change
const packageVersion = JSON.parse(
readFileSync( resolve( __dirname, 'package.json' ), 'utf-8' )
).version;
const packageVersion = JSON.parse(
readFileSync( resolve( __dirname, '../../package.json' ), 'utf-8' )
).version;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. We used to have a separate version field, but decided to drop it to keep the studio --version command output in sync with the Studio UI.

It makes sense to consolidate this and maintain a single canonical version field. My instinctive preference would be to use the root package.json for this, but if Electron Forge complains, maybe it's better to use apps/studio/package.json. I'll take a look

@fredrikekelund
Copy link
Contributor Author

When starting the app, I'm currently getting failures to start the site servers, it seems as if the cli package dependencies are not yet installed, or it's not built

Hmm, would you mind pulling the latest changes and running npm ci from the repo root? I'm not seeing this locally, and since the builds are also passing on CI, I'm inclined to believe that some leftover is causing the issue for you locally.

@gcsecsey
Copy link
Contributor

When starting the app, I'm currently getting failures to start the site servers, it seems as if the cli package dependencies are not yet installed, or it's not built

Hmm, would you mind pulling the latest changes and running npm ci from the repo root? I'm not seeing this locally, and since the builds are also passing on CI, I'm inclined to believe that some leftover is causing the issue for you locally.

Thanks for checking @fredrikekelund. After pulling the latest changes, both npm ci and npm i are working as expected, and I can start sites normally. 👍

@fredrikekelund
Copy link
Contributor Author

The macOS dev build is 601 MB. 1.7.4-beta2 is 579 MB, so we have a 22 MB increase.

I don't know exactly what that comes down to, but it's something we can revisit after landing this PR, IMO.

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.

3 participants