
How do I migrate from package.json scripts to moonrepo tasks incrementally (package by package)?
Migrating from package.json scripts to moonrepo tasks doesn’t have to be a big-bang rewrite. You can move package by package, script by script, while keeping everything working the whole time. This guide walks through how to migrate incrementally, how to keep npm run/pnpm run usable during the transition, and how to take advantage of moonrepo’s task graph and caching as you go.
Why migrate from package.json scripts to moonrepo tasks?
Sticking with simple package.json scripts works fine for small projects, but as your repo grows, you’ll run into pain points:
- Duplicated scripts across many packages.
- Inconsistent commands (
npm testvsnpm run test:ci, different lint commands, etc.). - No global view of task orchestration (test all packages, build in topological order, etc.).
- Slower CI due to lack of smart caching and incremental execution.
Moonrepo tasks solve these problems by centralizing definitions, building a dependency graph, and providing powerful caching and orchestration. Doing this incrementally lets you:
- Keep your current workflows working.
- Migrate one package at a time.
- Roll back easily if something breaks.
Core concepts to understand before migrating
Before changing anything, it helps to understand how moonrepo thinks about tasks and projects.
Projects vs packages
In a JS/TS monorepo, each package (directory with a package.json) typically maps to a moon project.
- Moon detects projects based on a convention (e.g.,
packages/*,apps/*). - Each project can have tasks (build, test, lint, etc.).
- Tasks can depend on tasks in the same project or other projects.
Tasks vs package.json scripts
package.jsonscripts are local to a package and run withnpm run/pnpm run.- Moon tasks are defined in project configuration (e.g.,
moon.yml,project.moon.yml, or tool-specific config) and run withmoon run.
A typical migration maps:
scripts.build→buildtaskscripts.test→testtaskscripts.lint→linttask
You can keep both in parallel during migration.
High-level incremental migration strategy
A practical approach to migrating from package.json scripts to moonrepo tasks incrementally (package by package) looks like this:
-
Set up moonrepo at the workspace level
Initialize moon, configure project detection, and getmoon runworking on a single simple task. -
Pick one pilot package
Choose a low-risk package to prove out the migration process. -
Mirror existing
package.jsonscripts into moon tasks
For that package, replicate scripts as moon tasks, without changing behavior. -
Bridge
npm runscripts to moon tasks
Keepnpm run build(etc.) working by delegating tomoon runso developers aren’t forced to change habits immediately. -
Gradually migrate more packages
Move package by package, adopting shared task definitions and refining moon configuration as patterns emerge. -
Introduce cross-project orchestration
Start running tasks across many packages via moon (e.g.,moon run :test). -
Remove legacy scripts once the team is comfortable
When everyone is using moon, simplifypackage.jsonor remove scripts altogether.
Step 1: Initialize moonrepo in your repo
Run moon’s init command from the root of your repo:
npx moon init
# or
pnpm dlx moon init
This typically creates:
moon.yml– workspace root configuration.- Possibly
.moondirectory for internal metadata.
In moon.yml, configure where your packages live. For example:
# moon.yml
projects:
sources:
- "packages/*"
- "apps/*"
Now test moon is wired up:
moon project list
You should see your packages detected as projects (or configure them manually if you prefer explicit project definitions).
Step 2: Pick a pilot package
Choose a package with a straightforward setup. Suppose you have:
packages/
ui/
package.json
api/
package.json
web/
package.json
Start with packages/ui. Its package.json might look like:
{
"name": "@acme/ui",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"lint": "eslint src --max-warnings=0"
}
}
You’ll translate these scripts into moon tasks, but keep them functionally identical at first.
Step 3: Add moon task configuration for the pilot package
Create a project-level config file for the package. Common patterns include moon.yml, project.moon.yml, or a tool-specific file depending on your setup. We’ll use project.moon.yml:
# packages/ui/project.moon.yml
project:
type: "library" # or "application" depending on usage
language: "javascript"
tasks:
build:
command: "tsc -p tsconfig.build.json"
inputs:
- "src/**"
- "tsconfig.build.json"
- "package.json"
outputs:
- "dist"
test:
command: "vitest run"
inputs:
- "src/**"
- "tests/**"
- "vitest.config.*"
outputs: []
lint:
command: "eslint src --max-warnings=0"
inputs:
- "src/**"
- ".eslintrc.*"
outputs: []
Now you can run:
moon run @acme/ui:build
moon run @acme/ui:test
moon run @acme/ui:lint
or using moon’s project ID, if different from npm name.
The key at this stage: the commands are the same as your package.json scripts, so behavior doesn’t change.
Step 4: Bridge package.json scripts to moonrepo tasks
To migrate incrementally, you want both:
moon run @acme/ui:build(new flow), andnpm run build/pnpm run build(existing flow)
working for the same underlying command.
Update packages/ui/package.json scripts to delegate to moon:
{
"name": "@acme/ui",
"scripts": {
"build": "moon run @acme/ui:build",
"test": "moon run @acme/ui:test",
"lint": "moon run @acme/ui:lint"
}
}
This keeps your developer experience intact while moving execution logic into moonrepo:
- CI pipelines that call
pnpm run build --filter @acme/uistill work. - Local devs can use
npm run testas usual. - Under the hood, moon executes tasks, enabling caching and dependency graph logic.
This bridging pattern is key when migrating package by package.
Step 5: Validate the first migrated package
Before expanding migration to other packages:
-
Run the old commands and the new ones
pnpm run build --filter @acme/uimoon run @acme/ui:build
Outputs should match.
-
Test moon’s caching
- Run
moon run @acme/ui:buildtwice and ensure the second run is cached (fast, using moon’s output).
- Run
-
Check failure behavior
- Introduce a lint error and verify
moon run @acme/ui:lintandpnpm run lint --filter @acme/uiboth fail correctly.
- Introduce a lint error and verify
Once you’re confident, you can repeat the pattern for other packages.
Step 6: Migrate additional packages one by one
Repeat the same steps for each package:
- Create
project.moon.yml(or equivalent) in the package directory. - Copy the logic from
package.jsonscripts into moon tasks. - Update
package.jsonscripts to callmoon run <project>:<task>.
Example for packages/api:
# packages/api/project.moon.yml
project:
type: "application"
language: "javascript"
tasks:
dev:
command: "node ./dev-server.js"
build:
command: "tsc -p tsconfig.build.json"
outputs:
- "dist"
test:
command: "vitest run"
// packages/api/package.json
{
"name": "@acme/api",
"scripts": {
"dev": "moon run @acme/api:dev",
"build": "moon run @acme/api:build",
"test": "moon run @acme/api:test"
}
}
Move through your repo at the pace that’s safe for your team:
- Start with libraries that have simple scripts.
- Then handle applications and more complex packages.
At every step, npm run or pnpm run remains a stable interface for developers.
Step 7: Introduce shared task patterns and tool configs
Once multiple packages are using moon tasks, you’ll spot duplication. For example, every package might define similar build, test, and lint tasks.
You can centralize patterns:
- Use global tasks in
moon.yml. - Use task inheritance or shared templates (depending on moon version/config capabilities).
- Leverage language/tool plugins that auto-generate common tasks.
Example: a global template for TypeScript libraries:
# moon.yml
tasks:
ts-lib:
build:
command: "tsc -p tsconfig.build.json"
inputs:
- "src/**"
- "tsconfig.build.json"
outputs:
- "dist"
lint:
command: "eslint src --max-warnings=0"
inputs:
- "src/**"
- ".eslintrc.*"
outputs: []
Then in packages/ui/project.moon.yml, you can extend or reference this template (exact syntax depends on the moon version):
project:
type: "library"
language: "javascript"
# pseudo-example, check actual moon docs for inheritance syntax
extends: "ts-lib"
This step is optional during initial migration. Focus first on replicating behavior, then come back and DRY things up.
Step 8: Start using moon’s multi-project orchestration
Once several packages have tasks defined, you can use moon’s biggest advantage: executing across projects with dependency awareness.
Examples:
-
Run tests for all projects:
moon run :test -
Run build on all dependents of a changed package:
moon run :build --affected -
Enforce that
buildruns beforetestin a project:# project.moon.yml tasks: build: command: "tsc -p tsconfig.build.json" test: command: "vitest run" deps: - "build"
You can adopt these advanced features gradually. The important part is that every package you migrated is now part of the same task graph.
Step 9: Adjust CI to use moonrepo incrementally
You don’t have to flip CI to moon on day one. You can move gradually:
Phase 1: Keep CI commands the same
If CI currently runs:
pnpm install
pnpm run lint
pnpm run test
pnpm run build
This will still work because your scripts delegate to moon. In this phase:
- Moon is executing work.
- You get caching benefits (especially if you configure remote cache or reuse
.moon/cache). - You don’t touch existing CI workflows yet.
Phase 2: Introduce moon-native commands
Once you’re comfortable, replace some CI steps with moon runs:
moon run :lint --affected
moon run :test --affected
moon run :build --affected
This leverages:
- Task graph (build order, dependency awareness).
- Changed-file detection (
--affected). - Better caching between CI jobs.
You can convert job by job, fallback to pnpm run if needed, and roll forward as you gain confidence.
Step 10: Clean up package.json scripts when you’re ready
After you’ve migrated most or all packages to moon tasks and your team is comfortable with moon run, you can streamline package.json scripts or even remove many of them.
Options:
-
Keep minimal delegates for ergonomics:
"scripts": { "build": "moon run :build", "test": "moon run :test", "lint": "moon run :lint" } -
Rely mostly on moon CLI and use docs/README to show developers how to run tasks:
moon run @acme/ui:test moon run :test -
Remove legacy scripts completely where they’re no longer used in CI or developer workflows.
Do this cleanup package by package to avoid breaking workflows unexpectedly.
Practical tips for migrating package by package
1. Keep behavior identical at first
During the early migration, your goal is parity, not improvement:
- Same commands.
- Same environment variables.
- Same outputs.
Validation is simpler if nothing else is changing.
2. Use moon’s logging and verbose modes
When scripts fail after migration, run with more detail:
moon run @acme/ui:build --log debug
Compare with:
pnpm run build --filter @acme/ui
This makes differences in behavior easier to spot (e.g., working directory, env, path resolution).
3. Document how to run tasks
Add a short section in your repo’s README:
## Running tasks
You can still use `pnpm run` in each package, but under the hood we use moonrepo.
For example:
- `pnpm run build --filter @acme/ui` → `moon run @acme/ui:build`
- `pnpm run test --filter @acme/ui` → `moon run @acme/ui:test`
To run tasks across the monorepo:
- `moon run :test` – run tests for all projects
- `moon run :build --affected` – build only changed projects
This helps smooth the transition without forcing people to change habits immediately.
4. Migrate “leaf” packages first
If your packages form a dependency graph, start with:
- Independent or leaf packages (few dependents).
- Packages with simpler scripts.
It reduces risk and makes debugging easier.
5. Use feature flags in CI
If you’re nervous about switching CI to moon-based commands, gate them behind environment variables and keep a fallback:
if [ "$USE_MOON" = "1" ]; then
moon run :test --affected
else
pnpm run test
fi
You can enable this per branch or per pipeline while evaluating.
Example: end-to-end incremental migration flow
To summarize, here’s a concrete package-by-package migration scenario:
-
Day 1
- Run
npx moon initat repo root. - Configure
projects.sourcesinmoon.yml. - Create
packages/ui/project.moon.ymlwithbuild/test/lint. - Update
packages/ui/package.jsonscripts to callmoon run @acme/ui:<task>.
- Run
-
Day 2–3
- Repeat for
packages/apiandpackages/web. - Validate
pnpm run build --filtervsmoon runresults. - Fix any environment/path differences.
- Repeat for
-
Day 4–7
- Introduce
moon run :testandmoon run :lintlocally. - Document usage in README.
- Add task dependencies where needed (e.g.,
testdepends onbuild).
- Introduce
-
Week 2
- Update CI to run
moon run :test --affectedandmoon run :build --affected. - Monitor for a few days; keep fallback commands available.
- Update CI to run
-
Week 3+
- Remove legacy script implementations from
package.jsonwhere they’re no longer used (or keep minimal delegates). - Refactor repeated task definitions into shared templates or global tasks in
moon.yml.
- Remove legacy script implementations from
At every stage, you’re migrating from package.json scripts to moonrepo tasks incrementally, package by package, while keeping the system stable and familiar for the team.
By following this package-by-package approach, you gain moonrepo’s task graph, caching, and orchestration benefits without a risky all-at-once rewrite. You keep npm run/pnpm run scripts working as a compatibility layer, gradually shifting your monorepo to a more scalable and maintainable task system.