Anyone else running into this? Symbol.iterator polyfill in ecmascript-runtime-client breaks native iterator protocol on the client

Symbol.iterator polyfill in ecmascript-runtime-client breaks native iterator protocol on the client

Versions

  • Meteor: METEOR@3.4
  • ecmascript-runtime-client: 0.12.3
  • core-js (transitive, bundled): 3.47.0
  • @meteorjs/rspack: 1.0.2 (modern web targets only)

Symptom

On the client, any code that uses the native iterator protocol on a built-in iterator — for (const x of arr.entries()), for (const [k, v] of map.entries()), for (const v of map.values()), etc. — can throw at runtime:

TypeError: b.entries is not a function or its return value is not iterable
    at … in our app bundle

The first part of the message is misleading; Array.prototype.entries does exist. What actually fails is the [Symbol.iterator] lookup on the iterator returned by .entries() — the polyfilled Symbol.iterator is a different symbol than the one the engine’s built-in iterators are keyed under, so for..of declines to iterate.

What I found tracing this

Meteor’s ecmascript-runtime-client package pulls in core-js and installs a Symbol.iterator shim with a UID-suffixed symbol (Symbol(Symbol.iterator)_a.<uid> or similar — readable as String(Symbol.iterator) on the client). Code that calls a built-in iterator method then for..of’s the result hits the mismatch: the iterator’s internal @@iterator is the engine’s symbol, the for..of is looking up the polyfilled one.

This already bit us once with mime v4, which uses for..of map.values() internally. Downgrading to mime v3 (indexed loops) made the client boot again. The same pattern then bit us again in our own app code at:

for (const [priority, placement] of placements.entries()) { … }

…where placements was a plain Array. Same root cause.

Workaround

In client code, avoid native-iterator-protocol calls and use indexed iteration or .forEach() instead:

// Bad — relies on Symbol.iterator
for (const [priority, placement] of placements.entries()) { … }

// Fine — never touches the iterator protocol
for (let priority = 0; priority < placements.length; priority++) {
  const placement = placements[priority]
  …
}

For Map/Set, Array.from(map.values()).forEach(…) works too — Array.from does the iterator dance up front, then you’re back on a regular Array.

Question

Is this a known issue with ecmascript-runtime-client@0.12.3 + bundled core-js@3.47.0? Has anyone else hit this in production? Is there a way to opt out of the Symbol.iterator polyfill for modern browser targets (where the engine’s native iterator protocol is already correct)? Our @meteorjs/rspack@1.0.2 build targets modern browsers only, so the polyfill is pure liability for us — it’s only causing breakage, not enabling anything.

Happy to share concrete repros if useful. The two we hit:

  1. mime@4.x on the client.
  2. Our own app code: a for..of arr.entries() loop in a tooltip placement helper.

Both fix the same way: stop using the iterator protocol.

Thank you!

I can see how this is annoying. Every JS environment today already supports iterators and syntax like for-of natively, yet Meteor’ build is getting in the way.

I’ve encouraging Meteor contributors to go for a modern standards-aligned setup, and make everything else optional (making things like compile to lower browser targets, use of SCSS, etc, all optional).

IMO, by default, we should be able to write plain JS and the build should do nothing more than at most bundling+minification for optimization, and at least allowing client-side code to run as native ES modules with native CSS modules. In this setup you would never hit such a problem.


To work around Meteor’s build, I compile my client-side code manually, and then place it into the Meteor app. For example you could compile your JS or TS code into a bundle using esbuild manually, then have rebuild place the output file in your Meteor app.

In this setup I run two things: esbuild in watch mode, and meteor, in two separate terminals (or multiplex them in a single terminal using something like concurrently from NPM). Optionally run tsc in watch mode with noEmit only for type checking. Or similar.

This way, I can totally bypass Meteor’s client-side build, and avoid all issues, and be in full control.

When prototyping, an alternative is to have no client-side build at all, and simply place your client code in the public/ folder and load it natively into the browser (<script type=module src="./index.js"> in client/entry.html will import that from public/index.js). Very easy low-complexity (low-tooling) way to get started.

For optimization, the best way to start is with a tool like esbuild/etc that will bundle your code into a single module, so avoid latency overhead from JS modules fetching many separate module files.


If you have no luck with updates in Meteor build/packages, you might be able to just escape out of Meteor that way, and get moving along.

Thank you for your thoughtful reply.

I’m hoping to avoid a workaround like that if I can, but I appreciate you taking the time to walk me through it.

I’ve opened an issue on GitHub. Let’s see if it gets any traction.

Thanks again.

1 Like