Skip to content

Terminal

Blueprint terminal ready. Run help to see available commands.

Press Tab to complete commands and paths

Keyboard shortcuts

11 min read By Billy Noyes

Run Vite and Shopify CLI together with npm or pnpm

Two long parallel task tracks above two shorter stages connected in sequence.

A Shopify theme workflow often needs two commands running at once: a frontend development server such as Vite, and Shopify CLI to preview and synchronize a development theme. Opening two terminals works, but one script can make the routine easier.

Deployment is different. The production build must finish successfully before the theme is uploaded. Running those two jobs in parallel can upload stale or incomplete assets.

The rule is simple: develop in parallel; build and push in sequence.

What each process does

Vite handles frontend source files, asset serving, and hot module replacement. shopify theme dev uploads the theme as a development theme, provides a storefront preview, and synchronizes changes while it runs. Neither process normally finishes until you stop it.

Running both commands does not, by itself, connect Vite to Liquid. Your theme still needs a build integration that loads the right assets during development and production.

I built Easel for that layer. It brings Vite to Shopify Liquid themes, including generated Liquid asset loading, HMR, and safe publication of the build output it owns. Shopify CLI remains responsible for store access, theme previews, and deployment.

Easel: https://easel.billynoyes.co.uk/

This guide assumes Vite and your Shopify integration already work, Shopify CLI is installed and available, and the scripts run from the theme root. If the deployable theme lives elsewhere, configure Shopify CLI's --path accordingly. Vite's default output folder alone is not necessarily a complete Shopify theme.

Choose npm or pnpm for the project and keep its lockfile. The alternatives below do not require mixing package managers.

Use the same script names as Easel

Easel's generated theme keeps dev as vite and build as vite build. Its documentation names the optional Shopify CLI script dev:shopify and the combined development runner dev:theme.

Keep that distinction: dev starts Vite; dev:shopify starts Shopify CLI; dev:theme starts both. The generator does not add the combined runner for you.

Start with these independent scripts. This example adds dev:shopify and theme:push to the Vite scripts; theme:push and the later push wrapper are names chosen for this article, not generated Easel commands. Merge the entries into your existing scripts rather than removing any checks the generator created:

{
  "scripts": {
    "dev": "vite",
    "dev:shopify": "shopify theme dev --store example.myshopify.com",
    "build": "vite build",
    "theme:push": "shopify theme push --store example.myshopify.com --unpublished"
  }
}

Replace example.myshopify.com with your store. No access token or password belongs in package.json.

These names are conventions, not built-in package-manager commands. Both package managers make locally installed binaries available to scripts, so a local Vite installation can be invoked as vite.

Run Shopify CLI directly for first-time authentication, store selection, or troubleshooting. Interactive prompts are easier to handle before putting it inside a combined process runner.

When passing extra flags to an individual script, npm uses a separator: npm run dev:shopify -- --port 9293. pnpm forwards flags after the script name directly: pnpm run dev:shopify --port 9293. Keep runner options before the script name, and do not assume an argument to the combined dev:theme script reaches both children.

Without a package: two terminals work everywhere

With npm, run these in separate terminals:

# Terminal 1
npm run dev
# Terminal 2
npm run dev:shopify

With pnpm:

# Terminal 1
pnpm run dev
# Terminal 2
pnpm run dev:shopify

Both processes stay active, each has its own output and input, and you can stop each with Ctrl+C. This is often the simplest setup when debugging.

Do not use npm run dev && npm run dev:shopify here. The second command waits for the first to exit successfully, so it will not start while the Vite server is running.

pnpm: native parallel scripts in one package

pnpm can match script names with a regular expression and run the matches concurrently. You do not need an additional runner package:

pnpm run "/^(dev|dev:shopify)$/"

For a single entry point, choose this dev:theme definition and keep the two independent scripts:

{
  "scripts": {
    "dev:theme": "pnpm run \"/^(dev|dev:shopify)$/\"",
    "dev": "vite",
    "dev:shopify": "shopify theme dev --store example.myshopify.com"
  }
}

Then run:

pnpm run dev:theme

The expression matches only dev and dev:shopify. It does not match the parent dev:theme script, so it cannot recursively launch itself. Quoting keeps the expression together as one argument across shells.

This is different from writing pnpm run dev dev:shopify. That invokes the first script with dev:shopify as an argument; it does not launch two scripts. npm behaves the same way with that argument pattern.

npm: shell concurrency is not a native task runner

npm does not provide pnpm's regex-based parallel script selection. On a POSIX shell, such as those commonly used on macOS and Linux, you can background the first command:

npm run dev & npm run dev:shopify

That is shell syntax, not an npm concurrency feature. It has trade-offs: output is mixed, a failed process does not automatically stop its sibling, and background-process cleanup needs care. Do not assume stopping the foreground command always stops everything it launched.

It is also not a portable Windows recipe. npm scripts use /bin/sh on Unix-like systems and cmd.exe on Windows by default; & has different behavior in those shells. Your interactive terminal and the shell used inside a package script are not necessarily the same.

For an npm project, I would choose two terminals or a cross-platform process runner rather than putting a bare & in the team's main development script.

With a package: concurrently

concurrently provides named output and explicit handling for a child process exiting. It works with npm and pnpm on macOS, Linux, and Windows.

Install it using the project's package manager:

npm install --save-dev concurrently

Or:

pnpm add -D concurrently

Check the package's Node.js requirements before installing; newer runner releases can require a newer Node version.

For npm, use this dev:theme definition instead of the pnpm-native alternative:

{
  "scripts": {
    "dev:theme": "concurrently --kill-others --names vite,shopify \"npm run dev\" \"npm run dev:shopify\"",
    "dev": "vite",
    "dev:shopify": "shopify theme dev --store example.myshopify.com"
  }
}

For pnpm, use pnpm in the child commands:

{
  "scripts": {
    "dev:theme": "concurrently --kill-others --names vite,shopify \"pnpm run dev\" \"pnpm run dev:shopify\"",
    "dev": "vite",
    "dev:shopify": "shopify theme dev --store example.myshopify.com"
  }
}

Run npm run dev:theme or pnpm run dev:theme. The two processes start together, and their logs carry separate names. --kill-others stops the remaining command when one exits, whether successfully or with an error. That policy suits two servers that should stay running together. Ctrl+C stops the group.

Easel’s documentation uses the npm:dev and npm:dev:shopify shorthand inside concurrently; the explicit npm run commands above start the same scripts. The --names values only label log output; they do not choose which scripts execute.

Do not use that policy for unrelated one-off tasks that are expected to finish at different times. Also, starting commands concurrently does not guarantee either is ready first. If one depends on a file or service becoming available, add an explicit readiness step instead of relying on startup timing.

Another package option: npm-run-all2

npm-run-all2 is a maintenance fork of npm-run-all. It provides run-p for parallel scripts and run-s for sequential scripts, and supports npm and pnpm. Check its Node.js engine requirements too.

npm install --save-dev npm-run-all2

Or:

pnpm add -D npm-run-all2

Keeping the four independent scripts from the first example, you can add these two entries to scripts:

{
  "dev:theme": "run-p --race dev dev:shopify",
  "push": "run-s build theme:push"
}

run-p starts the development scripts in parallel. With --race, a successful exit from either also ends the other; by default, a non-zero exit already aborts the other tasks. run-s runs the build before the upload and stops the sequence if a task fails.

You only need one runner. Installing both concurrently and npm-run-all2 is unnecessary for this workflow.

Build, then push: no runner package required

Stop the development processes before building the same theme directory. A dev server and a production build can otherwise compete over generated files. Easel explicitly documents stopping Vite before building or deploying because its development and production Liquid loaders differ.

With the independent build and theme:push scripts, npm can use:

{
  "push": "npm run build && npm run theme:push"
}

pnpm can use:

{
  "push": "pnpm run build && pnpm run theme:push"
}

Add the appropriate entry to your existing scripts, then run npm run push or pnpm run push.

&& starts the upload only if the build exits with status zero. It works in the default Unix and Windows command shells used for package scripts. If you type the chain directly into a different shell, such as Windows PowerShell 5.1, its syntax support can differ.

Do not run concurrently "npm run build" "npm run theme:push", run-p build theme:push, or a pnpm regex matching both. Those introduce a race between writing and uploading the assets.

The --unpublished flag in the example creates a new unpublished theme on each push. To repeatedly update a chosen staging theme, replace it with --theme and that theme's ID after verifying the target. theme push uploads files; --publish makes the uploaded theme live. Do not add --publish, --live, or --allow-live casually to a default script.

An explicit push sequence is easier to audit than hiding a build inside prepush or an upload inside postbuild. npm supports pre/post script hooks; current pnpm 10 does too, controlled by enablePrePostScripts. Existing hooks can cause a build to run twice if you also chain it explicitly. Inspect predev and postdev too: wrapping dev still invokes that script’s lifecycle hooks. Keep a production build or deployment out of postdev; stopping a development process is not a deployment decision. An npm script called prepush is not a Git pre-push hook.

Where Easel fits

A process runner starts and coordinates commands. Easel connects Vite's asset pipeline to Shopify Liquid. Shopify CLI previews and uploads the theme. Each tool has a separate job.

If you are using Easel, its optional Shopify notification hook can be added to the development script:

{
  "dev:shopify": "shopify theme dev --store example.myshopify.com --notify .easel/shopify-ready"
}

That lets Easel request a full page reload after Shopify CLI finishes an update. It is not a requirement for the parallel runner, and Vite HMR and Shopify CLI live reload still work without it.

Easel's generated theme also provides check, check:theme, and check:all. For a TypeScript starter, check:all runs the type checker, Vite build, and Shopify Theme Check in sequence. JavaScript starters omit the type-checking step. check:theme runs Theme Check alone.

For a generated Easel theme, you can use the more thorough deployment sequence instead of the build-only version:

{
  "push": "npm run check:all && npm run theme:push"
}

Or with pnpm:

{
  "push": "pnpm run check:all && pnpm run theme:push"
}

Do not add build before check:all: that would build twice. These checks apply to Easel's generated starter; adding only the Vite plugin to an existing theme does not automatically create those scripts.

Before uploading, confirm the generated snippets/easel-assets.liquid references production Shopify assets rather than the local Vite server. Deploy that loader together with its matching built assets.

Setup and build details: https://easel.billynoyes.co.uk/docs/

If your project is a workspace

Running two scripts in one theme is different from running the same script across multiple packages.

For pnpm workspaces, pnpm -r --parallel run dev starts the matching package scripts without waiting for topological dependency order. The workspace root is excluded by default for recursive script execution unless includeWorkspaceRoot is enabled. That is useful for long-running development servers. It is not a substitute for dependency-aware builds.

With this article's naming, dev still means Vite alone. If each intended workspace defines the combined dev:theme script, use pnpm -r --parallel run dev:theme to run those combined workflows instead. Scripts missing from a package are skipped, so verify which packages actually define the command.

npm's npm run dev --workspaces runs workspace scripts in workspace order, not as a parallel server supervisor. A long-running first script can prevent later ones from starting. Use a runner with explicit workspace commands if you need them running together.

My recommendation

For one Shopify theme: use pnpm's native regex runner if you already use pnpm, or concurrently if you want explicit process-exit handling and named output with either package manager. Two terminals remain a perfectly good no-dependency option.

For deployment, keep the important part visible: build successfully, then push to a deliberate theme target. A command named push only builds because you wrote that sequence; Shopify CLI does not run your Vite build automatically.

References

Search suggestions are unavailable. Try again.

Loading...