
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. With the right approach, you can move package by package, keep your workflows running, and gradually take advantage of moonrepo’s task graph, caching, and orchestration.
This guide walks through how to migrate incrementally, with concrete examples and patterns you can reuse across your monorepo.
Why migrate from package.json scripts to moonrepo tasks?
Before diving into the step-by-step process, it helps to clarify why you’d want to move scripts into moonrepo tasks at all:
- Centralized task definitions – Common tasks (build, test, lint) can be defined once and shared across packages.
- Better performance – moonrepo provides caching, parallelization, and incremental execution.
- Stronger consistency – The same commands and options run the same way across packages.
- Improved developer experience –
moon run :buildormoon run web:testinstead of remembering each package’s bespoke scripts.
Incremental migration lets you get those benefits without breaking existing npm, pnpm, or yarn workflows.
Core concepts in moonrepo for script migration
When you migrate from package.json scripts, you’re mostly mapping concepts:
- Scripts → Tasks
npm run buildbecomes a task likebuild(project-level) or:build(global pattern). - Per-package definitions → Project configs
package.jsonbecomesmoon.yml(orproject.yml) in each package. - Custom commands → Task command definitions
The script string moves intocommand:in the moon task.
Example mapping from package.json:
// package.json
{
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"lint": "eslint src --ext .ts,.tsx"
}
}
To a moon project config:
# packages/api/moon.yml
tasks:
build:
command: "tsc -p tsconfig.build.json"
test:
command: "vitest run"
lint:
command: "eslint src --ext .ts,.tsx"
The key is that you do not need to move everything at once: you can add tasks gradually while keeping package.json scripts in place.
High-level strategy for incremental migration (package by package)
A practical migration plan from package.json scripts to moonrepo tasks looks like this:
- Introduce moonrepo at the root.
- Pick a single package to migrate first.
- Mirror its key
package.jsonscripts into amoon.yml. - Run the tasks through moon and ensure parity.
- Update CI and internal docs for that package only.
- Repeat for other packages at your own pace.
- Gradually centralize shared tasks and remove old scripts.
You can stop at any point and your repo will still work: packages that aren’t migrated yet keep using npm run, while migrated ones can be run via moon.
Step 1: Set up moonrepo in your monorepo root
If moonrepo is not yet installed:
# With npm
npm install --save-dev @moonrepo/cli
# Or pnpm
pnpm add -D @moonrepo/cli
# Or yarn
yarn add -D @moonrepo/cli
Initialize moonrepo (depending on your package manager and layout):
npx moon init
This will create a base .moon directory and a moon.yml (or equivalent root config) that identifies your workspace and projects.
Make sure:
- moon recognizes your packages (check
.moon/workspace.ymland project globs). - You can run something like
moon run :noopormoon runto see the CLI is working (or any sample task the init wizard created).
At this point, you haven’t changed any package.json scripts yet. Everything continues to work as before.
Step 2: Choose your first package to migrate
Pick a package that is:
- Commonly used or critical (so the migration provides real value).
- Not the most complex one (for a smoother first migration).
Assume a typical layout:
.
├─ packages/
│ ├─ web/
│ │ ├─ package.json
│ ├─ api/
│ │ ├─ package.json
│ └─ shared/
│ ├─ package.json
└─ .moon/
Let’s say you start with packages/web.
Step 3: Mirror scripts into a moon.yml for that package
Check the existing scripts in packages/web/package.json:
{
"name": "@acme/web",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
Create a moon.yml (or project.yml) in packages/web:
# packages/web/moon.yml
type: "application"
tasks:
dev:
command: "next dev"
local: true # dev server: don't cache, run locally
build:
command: "next build"
inputs:
- "src/**/*"
- "next.config.*"
- "package.json"
outputs:
- ".next"
start:
command: "next start"
local: true
lint:
command: "next lint"
inputs:
- "src/**/*"
- ".eslintrc*"
Key points:
- Use the same task names (
dev,build,lint) as the original scripts for clarity. - Use
local: truefor development commands that should never be cached or run remotely. - Define
inputsandoutputsfor build-like tasks to unlock moonrepo caching.
At this stage, you still keep the original scripts in package.json. They continue to work:
cd packages/web
npm run build
Step 4: Run the new moon tasks alongside existing scripts
From the repo root, run the tasks through moonrepo:
# Run build for the web package through moon
moon run web:build
# Run lint
moon run web:lint
# Run dev (still via moon, but local only)
moon run web:dev
Compare behavior with the old npm run commands:
- Outputs: Are built artifacts in the same places (e.g.,
.next,dist)? - Logs: Are there any differences in environment or working directory?
- Exit codes: Do tasks fail and succeed in the same scenarios?
If you see differences, adjust the command, environment variables, or working directory options in moon.yml until they match.
You can also temporarily add a script that calls moon, to ease the transition:
// packages/web/package.json
{
"scripts": {
"build": "moon run web:build",
"lint": "moon run web:lint",
"dev": "moon run web:dev"
}
}
This is optional but can be helpful if developers are used to npm run.
Step 5: Keep package.json scripts as fallbacks during migration
While migrating package by package, there are three reasonable patterns:
-
Dual mode (transitional)
package.jsonscripts call moon tasks, but the underlying command lives in moon:{ "scripts": { "build": "moon run web:build" } }Pros: No behavior change for devs.
Cons: Slight overhead, but minor. -
Shadow mode
Bothpackage.jsonscripts and moon tasks exist, but CI and tooling only use moon:- Devs can run
npm run buildormoon run web:build. - You eventually remove
package.jsonscripts.
- Devs can run
-
Moon-only (after migration)
Fully migrated packages no longer define redundant scripts:{ "scripts": { "postinstall": "next telemetry disable" // only non-task scripts remain } }
For an incremental migration, dual mode or shadow mode works best until every CI job and internal tool references moon tasks.
Step 6: Migrate more packages, one by one
Repeat the same process for each package:
- Inspect
package.jsonscripts. - Create a
moon.ymlin that package. - Define tasks that mirror the scripts.
- Verify behavior via
moon run package:task. - Optionally rewire
package.jsonscripts to call moon.
For example, packages/api:
// packages/api/package.json
{
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"lint": "eslint src --ext .ts"
}
}
# packages/api/moon.yml
type: "application"
tasks:
build:
command: "tsc -p tsconfig.build.json"
inputs:
- "src/**/*"
- "tsconfig.*"
- "package.json"
outputs:
- "dist"
test:
command: "vitest run"
inputs:
- "src/**/*"
- "vitest.config.*"
lint:
command: "eslint src --ext .ts"
inputs:
- "src/**/*"
- ".eslintrc*"
Now you can run:
moon run api:build
moon run api:test
moon run api:lint
And you haven’t touched other packages yet, so they still work purely via package.json.
Step 7: Use global and shared tasks for consistency
Once several packages are using moon tasks, you’ll start seeing patterns. For example, every package may have build, test, and lint. Instead of repeating everything, you can:
- Create global tasks at the root
moon.yml. - Use task inheritance or templates (depending on your moonrepo version and features).
Root moon.yml example:
# moon.yml at repo root
tasks:
lint:
command: "eslint ."
local: true
test:
command: "vitest"
local: true
build-all:
deps:
- ":build" # run all build tasks across all projects
Now you can:
# Run build across all projects that define a "build" task
moon run :build
# Run lint in all projects with a lint task
moon run :lint
For package-level configs, you can define only the specifics (like inputs/outputs) while pointing to shared commands or scripts, depending on how advanced you want your setup to be.
Handling complex and chained scripts
In real-world repos, scripts often chain other scripts or use tools like concurrently or npm-run-all. For example:
{
"scripts": {
"build": "npm run build:types && npm run build:bundle",
"build:types": "tsc -p tsconfig.types.json",
"build:bundle": "rollup -c",
"watch": "npm-run-all --parallel watch:types watch:bundle",
"watch:types": "tsc -w -p tsconfig.types.json",
"watch:bundle": "rollup -c -w"
}
}
In moonrepo, you can model this more explicitly:
# packages/shared/moon.yml
type: "library"
tasks:
build:
deps:
- "build-types"
- "build-bundle"
build-types:
command: "tsc -p tsconfig.types.json"
outputs:
- "dist/types"
build-bundle:
command: "rollup -c"
outputs:
- "dist/bundle"
watch:
deps:
- "watch-types"
- "watch-bundle"
# moon will run dependent tasks, and you can choose concurrency options
watch-types:
command: "tsc -w -p tsconfig.types.json"
local: true
watch-bundle:
command: "rollup -c -w"
local: true
This approach makes dependencies explicit and lets moonrepo parallelize and cache appropriately.
You don’t need to model all of this initially. You can:
- Start by mirroring the single top-level script as a single
command: "npm run build:types && npm run build:bundle"in moon. - Once everything is working, refactor into multiple tasks and dependencies.
Updating CI incrementally
As you migrate tasks package by package, update your CI pipeline gradually:
- Start with non-critical jobs (e.g., lint in one package) to validate moon behavior.
- Replace
npm runcalls withmoon run package:task.
For example, GitHub Actions:
# Before
- name: Build web
run: |
cd packages/web
npm ci
npm run build
# After (once the package is migrated)
- name: Build web via moon
run: |
npm ci
npx moon run web:build
Later, as more packages migrate, you can switch to broader commands:
- name: Build all packages
run: |
npm ci
npx moon run :build
This way, CI also migrates incrementally and mirrors what developers already do locally.
When is it safe to remove package.json scripts?
You can safely remove scripts from a package’s package.json when:
- All its tasks are fully defined in
moon.yml. - Your CI/config automation uses moon tasks only for that package.
- Developer workflows no longer rely on
npm runfor those scripts (or you’ve communicated the change clearly).
A cautious approach is:
- Keep scripts for at least one release cycle after CI and documentation are updated.
- Announce the change to your team, including how to use
moon run package:task. - Remove the scripts in a dedicated “cleanup” PR.
Because moonrepo works at the repo level, partially migrated packages won’t break non-migrated ones.
Common pitfalls and how to avoid them
While migrating from package.json scripts to moonrepo tasks incrementally, watch out for:
1. Mismatched working directories
npm run usually runs scripts in the package directory. moonrepo also runs project tasks in their project’s root, but if you define tasks at the workspace level or call scripts that assume a specific path, you might see differences.
Tip:
Keep project-level commands relative to the project root. If needed, explicitly set runIn: "project" or use path variables according to moonrepo’s docs.
2. Forgotten environment variables
Some scripts depend on env vars from .env, shell profiles, or CI settings, like:
"scripts": {
"build": "NODE_ENV=production next build"
}
Make sure to preserve or move these into moon task definitions or CI environment configuration:
tasks:
build:
command: "next build"
env:
NODE_ENV: "production"
3. Incomplete inputs/outputs for caching
If you define outputs but miss important inputs, moonrepo might not rebuild when needed. Start with a conservative approach:
- Include all source directories and config files.
- Add
package.jsonand lockfiles when relevant.
You can refine inputs later once everything is stable.
4. Over-migrating too soon
It’s tempting to convert every script and package at once. But if your team is new to moonrepo, this can introduce friction.
Sticking to a package-by-package migration keeps risk low:
- Migrate a package.
- Verify behavior locally and in CI.
- Stabilize, then move on.
Putting it all together: a sample incremental migration timeline
Here’s how a practical migration might look over a few weeks:
Week 1
- Install and initialize moonrepo at the root.
- Migrate a single package (e.g.,
web) by addingmoon.yml. - Keep
package.jsonscripts but run moon tasks in parallel for testing.
Week 2
- Update CI to use
moon run web:buildandmoon run web:test. - Start migrating
apipackage scripts into amoon.yml. - Introduce shared root tasks for
:lintand:test.
Week 3
- Move remaining frequent packages to moon tasks (e.g.,
shared,docs). - Update team documentation to use
moon run package:task. - Remove redundant scripts from the earliest migrated packages.
Week 4+
- Optimize
inputs/outputsfor better caching. - Refactor complex scripts into multiple composable tasks.
- Use
moon run :build/:testacross the entire monorepo.
By treating the migration as a sequence of small, package-level changes, you preserve stability while steadily gaining the benefits of moonrepo.
Summary
To migrate from package.json scripts to moonrepo tasks incrementally, package by package:
- Install and initialize moonrepo at the repo root without changing existing scripts.
- Pick one package, mirror its scripts into a
moon.yml, and verify behavior withmoon run. - Keep
package.jsonscripts as fallbacks or wrappers until CI and docs are updated. - Repeat for other packages, gradually centralizing shared tasks and using
:build,:test, etc. - Once stable, remove redundant scripts and fully embrace moonrepo’s task orchestration and caching.
This incremental approach lets you modernize your workflow smoothly while maintaining developer productivity and reliability throughout the transition.