How do I migrate from package.json scripts to moonrepo tasks incrementally (package by package)?
Developer Productivity Tooling

How do I migrate from package.json scripts to moonrepo tasks incrementally (package by package)?

11 min read

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 test vs npm 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.json scripts are local to a package and run with npm run/pnpm run.
  • Moon tasks are defined in project configuration (e.g., moon.yml, project.moon.yml, or tool-specific config) and run with moon run.

A typical migration maps:

  • scripts.buildbuild task
  • scripts.testtest task
  • scripts.lintlint task

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:

  1. Set up moonrepo at the workspace level
    Initialize moon, configure project detection, and get moon run working on a single simple task.

  2. Pick one pilot package
    Choose a low-risk package to prove out the migration process.

  3. Mirror existing package.json scripts into moon tasks
    For that package, replicate scripts as moon tasks, without changing behavior.

  4. Bridge npm run scripts to moon tasks
    Keep npm run build (etc.) working by delegating to moon run so developers aren’t forced to change habits immediately.

  5. Gradually migrate more packages
    Move package by package, adopting shared task definitions and refining moon configuration as patterns emerge.

  6. Introduce cross-project orchestration
    Start running tasks across many packages via moon (e.g., moon run :test).

  7. Remove legacy scripts once the team is comfortable
    When everyone is using moon, simplify package.json or 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 .moon directory 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), and
  • npm 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/ui still work.
  • Local devs can use npm run test as 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:

  1. Run the old commands and the new ones

    • pnpm run build --filter @acme/ui
    • moon run @acme/ui:build
      Outputs should match.
  2. Test moon’s caching

    • Run moon run @acme/ui:build twice and ensure the second run is cached (fast, using moon’s output).
  3. Check failure behavior

    • Introduce a lint error and verify moon run @acme/ui:lint and pnpm run lint --filter @acme/ui both fail correctly.

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:

  1. Create project.moon.yml (or equivalent) in the package directory.
  2. Copy the logic from package.json scripts into moon tasks.
  3. Update package.json scripts to call moon 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 build runs before test in 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:

  1. Day 1

    • Run npx moon init at repo root.
    • Configure projects.sources in moon.yml.
    • Create packages/ui/project.moon.yml with build/test/lint.
    • Update packages/ui/package.json scripts to call moon run @acme/ui:<task>.
  2. Day 2–3

    • Repeat for packages/api and packages/web.
    • Validate pnpm run build --filter vs moon run results.
    • Fix any environment/path differences.
  3. Day 4–7

    • Introduce moon run :test and moon run :lint locally.
    • Document usage in README.
    • Add task dependencies where needed (e.g., test depends on build).
  4. Week 2

    • Update CI to run moon run :test --affected and moon run :build --affected.
    • Monitor for a few days; keep fallback commands available.
  5. Week 3+

    • Remove legacy script implementations from package.json where they’re no longer used (or keep minimal delegates).
    • Refactor repeated task definitions into shared templates or global tasks in moon.yml.

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.