← All context files

classic-asp-jscript/es6-builtins.md

Classic ASP / JScript – ES6 Built-ins That Don't Survive the Build

KYD apps (SPM, EMP, LPA, CFT, lite) build their Node-style API source into a single Classic ASP file with the goldfish bundler. Babel transpiles syntax (arrow functions, const/let, destructuring, spread, classes, async/await), but the ASP runtime is JScript (an old ES3-era engine), so any ES6 built-in object or method that Babel can't down-compile must exist at runtime. The goldfish polyfill server (/v2/polyfill.min.js) provides a limited set of these. Some it cannot provide at all.

The code runs fine under npm run dev (real Node) and only breaks once it's running as ASP — which is why these bugs slip through.

❌ NOT available — never use in API source

These have no JScript equivalent and the polyfill server cannot shim them (a faithful Set/Map needs the iterator protocol / Symbol.iterator, which JScript lacks). Replace them with plain objects/arrays.

Construct Why it breaks Use instead
new Set() No Set constructor in JScript Plain object {} as a lookup map
new Map() No Map constructor in JScript Plain object {} keyed by string
Array.from(x) Not defined [].slice.call(x) or build the array explicitly
[...Array(n)] Spread of a sparse array — the polyfilled .map() skips holes (i in arr), yielding empty results An explicit array literal [1, 2, 3, …, n]
Object.fromEntries(x) Not polyfilled — throws "Object doesn't support this property or method" A plain loop that builds the object (see below)

⚠️ Object.fromEntries is NOT available even though Object.entries IS. The polyfill server ships keys/entries/assign but not fromEntries, so the common Object.fromEntries(Object.entries(x).filter(…)) idiom blows up only once it runs as ASP.

Replacing Set with a plain object

A Set is just membership + uniqueness. A plain object does both.

// ❌ Breaks in ASP
const seen = new Set()
rows.filter((row) => {
  if (seen.has(row.id)) return false
  seen.add(row.id)
  return true
})

// ✅ Works everywhere
const seen = {}
rows.filter((row) => {
  if (seen[row.id] === true) return false
  seen[row.id] = true
  return true
})

Use === true, not just truthiness. A bare if (seen[key]) can false-match inherited prototype members — seen['toString'] returns a function (truthy) even though you never added it. Storing true and testing === true sidesteps that, because inherited members are never === true.

When you need the Set's contents back as a list:

// ❌ const roles = [...roleSet].sort()
// ✅
const roles = Object.keys(roleObj).sort()          // string keys
const years = Object.keys(yearObj).map(Number).sort((a, b) => b - a)  // numeric

Replacing Map with a plain object

// ❌ const index = new Map();  index.get(key);  index.set(key, entry)
// ✅
const index = {}
let entry = index[key]
if (!entry) {
  entry = { /* … */ }
  index[key] = entry
}

Safe as long as the key can't collide with an Object.prototype member. Keys that are composites like [year, tid, status].join('|') are always safe — they contain the | separator, so they can never equal toString, constructor, __proto__, etc. If a key could be arbitrary user text, guard with Object.prototype.hasOwnProperty.call(index, key).

Replacing Object.fromEntries

// ❌ Breaks in ASP — "Object doesn't support this property or method"
const picked = Object.fromEntries(
  Object.entries(settings).filter(([key]) => allowed.includes(key))
)

// ✅ Works everywhere
const picked = {}
for (const key of allowed) {
  if (Object.prototype.hasOwnProperty.call(settings, key)) {
    picked[key] = settings[key]
  }
}

❌ Static namespace methods — NONE are polyfilled

autopolyfiller only detects prototype methods, so static Number.* / Math.* (ES2015) / Object.values are never requested from the polyfill server and throw "Object doesn't support this property or method" at runtime. These run fine under Node, so they slip through until ASP.

Construct Use instead
Number.isNaN(x) global isNaN(x)
Number.isFinite(x) global isFinite(x)
Number.parseInt / Number.parseFloat global parseInt / parseFloat
Number.isInteger(x) typeof x === 'number' && isFinite(x) && Math.floor(x) === x
Math.trunc(x) x < 0 ? Math.ceil(x) : Math.floor(x)
Math.sign(x) x > 0 ? 1 : x < 0 ? -1 : x
other ES2015 Math.* (cbrt,hypot,log2,log10,clz32,fround,imul,expm1,log1p,sinh…) implement manually
Object.values(o) Object.keys(o).map(function (k) { return o[k] })

⚠️ Number.isNaN/isFinite differ from the globals only in that the globals coerce their argument first. When the value is already numeric (the usual case: Number(x), parseInt, .getTime(), .getFullYear()) the swap is exact.

❌ Prototype methods that are NOT polyfilled

autopolyfiller (circa 2017) fetches only a subset of prototype methods. Anything it doesn't request is silently skipped and throws "Object doesn't support this property or method" at runtime — even though it's a prototype method and even when it's ES2015. Don't assume "ES2015 = safe." Verify against a shipped bundle if unsure (grep -c "prototype.theMethod" Default.asp).

Construct Era Use instead
str.startsWith(x) ES2015 s.indexOf(x) === 0 or /^x/.test(s)
str.endsWith(x) ES2015 /x$/.test(s) or s.slice(-x.length) === x
str.padStart(n, '0') ES2017 ("0".repeat(n) + str).slice(-n) (or ("0" + n).slice(-2) for 2-digit)
str.padEnd(n, c) ES2017 concatenate manually
str.trimStart() / trimEnd() ES2019 .replace(/^\s+/, "") / .replace(/\s+$/, "")
arr.flat() ES2019 flatten with reduce/concat
arr.flatMap(fn) ES2019 .map(fn).reduce(function (a, b) { return a.concat(b) }, [])
str.replaceAll(a, b) ES2021 .replace(/a/g, b) or .split(a).join(b)
str.matchAll(re) ES2021 global regex with .exec() in a loop
arr.findLast() / findLastIndex() ES2023 iterate in reverse

.repeat() and .includes() ARE polyfilled, but the closely-related .startsWith/.endsWith are not — a classic foot-gun, since all three are ES2015.

✅ Available — the polyfill server DOES provide these

These are safe to use in API source; KYD codebases already rely on them in shipped ASP builds:

Because Object.keys is available, the idiomatic Set replacement Object.keys(obj).sort() is fully supported.

How to catch these before deploying

node --check file.js only validates syntax — it will happily pass code full of new Set(). To find runtime built-ins, grep the API source:

grep -rn "new Set(\|new Map(\|Array\.from\|\[\.\.\.Array\|Object\.fromEntries" api/src

The application's own source is usually the only offender. The lite framework itself is clean — these patterns creep in when API utilities/routes are written "Node-first" and never exercised as an ASP build.