| | |

Parcel 27 ๐Ÿ“ฆ Migration from Parcel 1 โ€” Package rename, cache location & code changes

โฑ๏ธ Estimated read: 12 min | |

The migration from Parcel 1 to Parcel 2 is not a configuration update. It is a change in the package identity, the caching model, and the default transpilation behavior. The old package is parcel-bundler, and the new one is parcel. The old cache directory is .cache, and the new one is .parcel-cache. The old bundler transpiled JavaScript by default, and the new one does not. Each of these changes produces a failure that points somewhere other than the actual cause: the package rename produces a “command not found,” the cache rename produces a stale-build mystery, and the transpilation change produces a syntax error in an older browser that worked before the upgrade.

The migration guide lists the changes in three categories. The first is the mechanical update: the dependency, the scripts, and the .gitignore. The second is the code changes: the <script type="module"> requirement, the url: prefix for non-code assets, and the import.meta.url pattern for workers. The third is the configuration changes: the --target flag, the --out-dir rename, and the main field inference. The three categories are independent, and a project that skips one produces a different class of error.

Key point: The package name changed from parcel-bundler to parcel. The cache directory changed from .cache to .parcel-cache. Parcel 2 does not transpile JavaScript by default; the browserslist field in package.json enables it. The <script type="module"> tag is required for module scripts, and the main field in package.json is inferred as the library output path unless it is explicitly disabled.


Why the migration fails at the mechanical changes

The package rename problem. The dependency is parcel-bundler in Parcel 1 and parcel in Parcel 2. A project that updates the version without changing the name installs the old package, and the parcel command in the scripts section resolves to nothing. The error is a “command not found” or a “parcel is not recognized,” which points to the script rather than the dependency name. The fix is a two-step update: yarn remove parcel-bundler and yarn add parcel --dev.

The cache location problem. Parcel 1 writes its cache to .cache. Parcel 2 writes it to .parcel-cache. A project that upgrades without updating the .gitignore commits the new cache directory, and a project that deletes the old .cache directory does not delete the new one. The stale cache is the source of the “it works on my machine but not in CI” failures, because the CI environment starts with no cache and the developer’s environment starts with a cache that the new version may not invalidate correctly. The fix is to add .parcel-cache to .gitignore and to delete the old .cache directory.

The transpilation problem. Parcel 1 transpiled JavaScript to a default browser set automatically. Parcel 2 does not transpile by default. A project that used modern syntaxโ€”optional chaining, nullish coalescing, class fieldsโ€”and supported older browsers in Parcel 1 will output the modern syntax in Parcel 2, and the older browsers will fail with a syntax error. The fix is to add a browserslist field to package.json that matches the targets the project supported before the upgrade.

The trade-off. The mechanical changes are small and fast. The code changes are larger and slower, because they touch the source files rather than the configuration. The <script type="module"> change is a one-line edit in the HTML, but the url: prefix and the import.meta.url pattern are code-level changes that may require a search-and-replace across the project. The migration is a sequence of small fixes, and each fix is independent.


a. The mechanical changes and the package setup

The first step is the dependency update. The old package is removed and the new one is installed. The scripts section is updated to use the parcel command.

# Remove the old package and install the new one.
yarn remove parcel-bundler
yarn add parcel --dev
// package.json โ€” the updated scripts
{
  "scripts": {
    "start": "parcel index.html",
    "build": "parcel build index.html"
  },
  "devDependencies": {
    "parcel": "^2.0.0"
  }
}

The .gitignore update is the second mechanical step. The old .cache directory is replaced by the new .parcel-cache directory.

# .gitignore โ€” the Parcel 2 cache
.parcel-cache
dist

The dist directory is the output directory. Parcel 1 used dist as the default, and Parcel 2 uses dist as the default as well, but the --out-dir flag was renamed to --dist-dir. A project that passes --out-dir in a script will see an “unknown option” error.


b. The code changes and the configuration changes

The <script type="module"> change is the most common code-level failure. Parcel 1 treated a <script> tag as a module script, and imports and exports worked inside it. Parcel 2 matches the browser’s behavior: a classic <script> does not support imports or exports, and a module script requires the type="module" attribute.

<!-- Parcel 1: imports worked in a classic script -->
<script src="./index.js"></script>

<!-- Parcel 2: a module script requires type="module" -->
<script type="module" src="./index.js"></script>

The url: prefix is the second code-level change. Parcel 1 returned a URL for any imported non-JavaScript file. Parcel 2 returns a URL for known file typesโ€”images, fontsโ€”but an unknown file type requires the url: prefix or the new URL() constructor.

// Parcel 1: any import returned a URL
import fileUrl from './file.custom';

// Parcel 2: the url: prefix returns a URL
import fileUrl from 'url:./file.custom';

// Parcel 2: the URL constructor is the preferred approach
const fileUrl = new URL('./file.custom', import.meta.url);

The main field inference is the configuration change that produces the most surprising failure. Parcel 2 reads the main field in package.json and treats the project as a library if the field is present. A web application that has a main field from npm init will have its output written to the path in main, which is often index.js at the project root. The fix is to remove the main field or to set "targets": { "main": false }.

// package.json โ€” disabling the main field inference
{
  "targets": {
    "main": false
  }
}

The --target flag is replaced by the engines field in package.json. A Node.js target is set with "engines": { "node": ">=18" }, and an Electron target is set with an electron key. The --experimental-scope-hoisting flag is replaced by the default behavior; scope hoisting is enabled by default, and the --no-scope-hoist flag disables it.


Complete Example Session

// ============================================
// PART 1: THE PACKAGE RENAME
// ============================================

// The old dependency:
// "parcel-bundler": "^1.0.0"

// The new dependency:
// "parcel": "^2.0.0"

// yarn remove parcel-bundler
// yarn add parcel --dev

// ============================================
// PART 2: THE CACHE LOCATION
// ============================================

// .gitignore
// .cache          โ† remove
// .parcel-cache   โ† add
// dist

// The old cache directory is .cache.
// The new cache directory is .parcel-cache.

// ============================================
// PART 3: THE BROWSERSLIST
// ============================================

// package.json
{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead"
  ]
}

// Without the browserslist field, Parcel 2 outputs
// the modern syntax without transpilation.
// The field enables the transpilation.

// ============================================
// PART 4: THE SCRIPT MODULE
// ============================================

// Parcel 1: <script src="./index.js"></script>
// Parcel 2: <script type="module" src="./index.js"></script>

// The type="module" attribute is required for imports.

// ============================================
// PART 5: THE URL PREFIX
// ============================================

// Parcel 1: import fileUrl from './file.custom';
// Parcel 2: import fileUrl from 'url:./file.custom';

// The url: prefix returns a URL for a file type
// that Parcel does not handle by default.

// ============================================
// PART 6: THE MAIN FIELD
// ============================================

// package.json โ€” the main field inference
{
  "main": "index.js",
  "targets": {
    "main": false
  }
}

// The main field is inferred as the library output.
// The targets.main: false disables the inference.

// ============================================
// PART 7: THE TARGET FLAG
// ============================================

// Parcel 1: parcel build index.html --target node
// Parcel 2: the engines field in package.json
{
  "engines": {
    "node": ">=18"
  }
}

// The --target flag is replaced by the engines field.

// ============================================
// PART 8: THE OUT DIR RENAME
// ============================================

// Parcel 1: parcel build index.html --out-dir dist
// Parcel 2: parcel build index.html --dist-dir dist

// The flag was renamed.

// ============================================
// PART 9: THE SCRIPT MODULE IN HTML
// ============================================

// The HTML file:
<!DOCTYPE html>
<html>
  <body>
    <script type="module" src="./index.js"></script>
  </body>
</html>

// The type="module" attribute is the migration change.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The dependency is parcel, not parcel-bundler.
// [ ] The .gitignore has .parcel-cache, not .cache.
// [ ] The browserslist field is set in package.json.
// [ ] The <script> tag has type="module".
// [ ] The non-code imports use url: or new URL().
// [ ] The main field is removed or targets.main is false.
// [ ] The --out-dir flag is renamed to --dist-dir.
// [ ] The --target flag is replaced by the engines field.

The ten parts covered the package rename, the cache location, the browserslist, the script module, the URL prefix, the main field, the target flag, the out-dir rename, the HTML change, and the checklist. The through-line is that each change is small, and the failures are specific to the change that was skipped.


Quick Reference

The mechanical changes

Parcel 1Parcel 2
parcel-bundlerparcel
.cache.parcel-cache
--out-dir--dist-dir
--out-fileRemoved (use package.json)
--targetengines field

The code changes

Parcel 1Parcel 2
<script> supports imports<script type="module"> required
Any import returns URLurl: prefix or new URL()
Automatic transpilationbrowserslist required
main field ignoredmain field inferred as output

The configuration changes

OptionParcel 1Parcel 2
Scope hoisting--experimental-scope-hoistingEnabled by default
Node modules bundling--bundle-node-modulesincludeNodeModules in targets
Log levelNumbersNames (none, error, warn, info, verbose)

Best Practices

โœ… Do This:

# Use the new package name
yarn add parcel --dev                                            # โœ…
// Set the browserslist field
{ "browserslist": ["> 0.5%", "last 2 versions", "not dead"] }   // โœ…
<!-- Use type="module" for the script -->
<script type="module" src="./index.js"></script>                 // โœ…

โŒ Don’t Do This:

# Use the old package name
yarn add parcel-bundler --dev // installs Parcel 1              // โŒ
// Import an unknown file type without the url: prefix
import fileUrl from './file.custom'; // may fail in Parcel 2    // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The parcel command is not foundThe old package is installedInstall parcel, not parcel-bundler
The cache is stale after the upgradeThe old .cache is not deletedDelete .cache and .parcel-cache
The build outputs to the wrong pathThe main field is inferredRemove main or set targets.main: false
Older browsers fail on modern syntaxNo browserslistAdd the browserslist field
Imports fail in a classic scriptThe <script> has no type="module"Add the attribute

Real-World Examples

1. The package rename

yarn remove parcel-bundler && yarn add parcel --dev

2. The cache rename

.parcel-cache

3. The browserslist

{ "browserslist": ["> 0.5%", "last 2 versions"] }

4. The script module

<script type="module" src="./index.js"></script>

5. The url prefix

import fileUrl from 'url:./file.custom';

6. The URL constructor

const fileUrl = new URL('./file.custom', import.meta.url);

7. The main field disable

{ "targets": { "main": false } }

8. The engines field

{ "engines": { "node": ">=18" } }

9. The out-dir rename

parcel build index.html --dist-dir dist

10. The cache delete

rm -rf .cache .parcel-cache dist

Visual

The Migration Failures

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  EACH CHANGE PRODUCES A DIFFERENT FAILURE                     โ”‚
โ”‚                                                              โ”‚
โ”‚  Package rename โ”€โ”€โ–ถ "parcel: command not found"             โ”‚
โ”‚  Cache rename โ”€โ”€โ”€โ”€โ–ถ stale build, "works on my machine"      โ”‚
โ”‚  Transpilation โ”€โ”€โ”€โ–ถ syntax error in older browsers          โ”‚
โ”‚  Script module โ”€โ”€โ”€โ–ถ "Cannot use import statement"           โ”‚
โ”‚  Main field โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ output written to the wrong path        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Three Categories

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MECHANICAL, CODE, AND CONFIGURATION                          โ”‚
โ”‚                                                              โ”‚
โ”‚  Mechanical:                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Package name        โ”‚  parcel-bundler โ†’ parcel           โ”‚
โ”‚  โ”‚ Cache location      โ”‚  .cache โ†’ .parcel-cache            โ”‚
โ”‚  โ”‚ .gitignore          โ”‚  add .parcel-cache                 โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Code:                                                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Script module       โ”‚  type="module"                     โ”‚
โ”‚  โ”‚ URL prefix          โ”‚  url: or new URL()                 โ”‚
โ”‚  โ”‚ Worker pattern      โ”‚  import.meta.url                   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Configuration:                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Main field          โ”‚  targets.main: false               โ”‚
โ”‚  โ”‚ Target flag         โ”‚  engines field                     โ”‚
โ”‚  โ”‚ Out-dir flag        โ”‚  --dist-dir                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Transpilation Change

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PARCEL 1 TRANSPILES, PARCEL 2 DOES NOT                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Parcel 1:                                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Source: modern JS   โ”‚                                    โ”‚
โ”‚  โ”‚ Output: transpiled  โ”‚  The default browser set           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Parcel 2 without browserslist:                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Source: modern JS   โ”‚                                    โ”‚
โ”‚  โ”‚ Output: modern JS   โ”‚  No transpilation                  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Parcel 2 with browserslist:                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Source: modern JS   โ”‚                                    โ”‚
โ”‚  โ”‚ Output: transpiled  โ”‚  The browserslist targets          โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Package renameparcel-bundler โ†’ parcel
Cache rename.cache โ†’ .parcel-cache
TranspilationDisabled by default; browserslist enables it
Script module<script type="module"> required
URL prefixurl: or new URL() for non-code assets
main fieldInferred as library output; targets.main: false disables
--out-dirRenamed to --dist-dir
--targetReplaced by engines field

Key takeaways:

  • The package rename is the first step. The dependency is parcel, not parcel-bundler. The parcel command resolves from the new package, and the old package is removed. The cache directory is .parcel-cache, not .cache.
  • Parcel 2 does not transpile by default. The browserslist field in package.json enables the transpilation. A project that supported older browsers in Parcel 1 must set the field to the same targets, or the output will contain modern syntax that the older browsers cannot parse.
  • The <script type="module"> tag is required for imports. Parcel 2 matches the browser’s behavior: a classic script does not support imports or exports. The type="module" attribute is the fix.
  • The main field is inferred as the library output. A web application that has a main field will have its output written to that path. The fix is to remove the field or to set "targets": { "main": false }.
  • The --out-dir flag was renamed to --dist-dir. The --out-file flag was removed, and the output path is specified in package.json. The --target flag was replaced by the engines field.

Remember: The Parcel 1 to Parcel 2 migration is a sequence of small, independent fixes. The package rename, the cache rename, and the .gitignore update are the mechanical changes. The <script type="module"> tag, the url: prefix, and the import.meta.url pattern are the code changes. The browserslist field, the main field inference, and the --dist-dir rename are the configuration changes. Each change produces a different failure, and each failure points to the change that was skipped. The migration is the sequence, and the sequence is the discipline.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!