Modern JavaScript skill for Codex

Newer built-ins, checked against your real targets. Codex reads browserslist, engines and the transpiler setup before using any newer feature, separating syntax a compiler can down-level from runtime APIs that need real support. It swaps lodash and moment calls for platform built-ins one call site at a time, flags semantic traps such as || versus ?? on zero, and drops a dependency only when nothing still imports it.

  • Languages
  • $javascript-modern
  • 150 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- javascript-modern

Installs to ~/.agents/skills/javascript-modern/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $javascript-modern, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Replaces legacy JavaScript patterns with modern built-ins checked against the project's real browser and Node targets - optional chaining, nullish coalescing, structuredClone, non-mutating array methods, Object.groupBy, Set methods, iterator helpers, Promise.withResolvers, AbortSignal.timeout, private fields and async iteration - and removes lodash, moment and polyfills that the platform now covers. Use when a user asks to modernize old JS, drop a utility dependency, fix var or callback-heavy code, or check whether a newer feature is safe for their browserslist or Node version.

Use it when

  • A codebase still uses var, callbacks or manual prototype tricks
  • Utility libraries duplicate what modern JavaScript ships
  • You need to know whether a newer API runs on your supported browsers or Node
  • Async code is serialized or mixes forEach with await

Use something else when

How to check the result

Tests pass unchanged, the features used are supported by the declared browserslist and engines targets, and removed dependencies no longer appear in any import.

The mistake it prevents

Replacing || with ?? everywhere, which changes behavior wherever a legitimate 0, empty string or false used to fall back to the default.

~/.agents/skills/javascript-modern/SKILL.md

--- name: javascript-modern description: Replaces legacy JavaScript patterns with modern built-ins checked against the project's real browser and Node targets - optional chaining, nullish… license: MIT ---

Done well means: every feature used is supported by the project’s declared targets (not the developer’s browser), each replacement keeps identical behavior including edge cases, and dependencies or polyfills are removed only when nothing else needs them.

Before changing anything

  1. Read AGENTS.md and find the targets: browserslist (in package.json or .browserslistrc), engines.node, tsconfig target/lib, bundler build.target.
  2. Check whether a transpiler covers syntax (TypeScript, Babel, esbuild, SWC). Syntax can be down-leveled; runtime APIs (structuredClone, Object.groupBy) cannot without polyfills.
  3. Check support for each API you plan to use: MDN compatibility tables or Baseline status (“Baseline widely available” = supported in all major browsers for 30+ months). Offline, rely on npx browserslist output plus the versions below and say so.
  4. Make sure tests cover the code; idiom changes must not change behavior.

Syntax: safe nearly everywhere

LegacyModernWatch out
varconst by default, let when reassignedBlock scoping changes closures in loops (usually fixes bugs)
a && a.b && a.b.ca?.b?.cReturns undefined, not false/null, when short-circuiting
x || 10 for defaultsx ?? 10|| also replaced 0, "", false; decide which you meant
obj.x = obj.x || {}obj.x ??= {}Same semantics question as above
Object.assign({}, a, b){ ...a, ...b }Spread does not invoke setters on the target
arguments, fn.apply(null, arr)rest params, fn(...arr)Spread of huge arrays can exceed argument limits
function(){}.bind(this) callbacksarrow functionsArrows have no own this/arguments; do not convert methods that rely on dynamic this
_private naming convention#private fieldsTruly inaccessible; breaks tests that poke at internals and proxies
.then chainsasync/await with try/catchKeep independent awaits parallel with Promise.all
IIFE for async startuptop-level await (ESM only)Blocks importers until resolved

Built-ins that replace helpers

Legacy or libraryModernNotes
JSON.parse(JSON.stringify(x)), _.cloneDeepstructuredClone(x)Handles Dates, Maps, cycles; throws on functions and DOM nodes; drops class prototypes
arr[arr.length - 1]arr.at(-1)
arr.slice().sort(), .reverse() copiestoSorted(), toReversed(), toSpliced(), with(i, v)ES2023; non-mutating
_.findLast, reverse-then-findfindLast, findLastIndex
_.groupBy, reduce into objectObject.groupBy(items, fn), Map.groupByES2024; result has null prototype
Object.prototype.hasOwnProperty.call(o, k)Object.hasOwn(o, k)
_.uniq, _.intersection[...new Set(a)], setA.intersection(setB), union, differenceSet methods are ES2025; check targets
_.flatten, _.flatMaparr.flat(depth), arr.flatMap(fn)
Object.keys(o).map(k => [k, o[k]])Object.entries(o), Object.fromEntries(pairs)
Deferred pattern with outer let resolveconst { promise, resolve, reject } = Promise.withResolvers()ES2024
Manual timeout with setTimeout + abortfetch(url, { signal: AbortSignal.timeout(5000) })Combine signals with AbortSignal.any([a, b])
momentIntl.DateTimeFormat, Intl.RelativeTimeFormat, date-fns, or Temporal where supportedMoment is in maintenance mode
_.debounceKeep a small helper; no built-inNot everything has a platform replacement
new Error(msg) losing the originalnew Error(msg, { cause: err })Logged by Node and browsers
Converting iterators to arrays to mapIterator helpers: iter.map().filter().take(5).toArray()ES2025; lazy
str.replace(/x/g, y)str.replaceAll("x", y)String argument needs no escaping

Do not convert lodash wholesale. Replace calls one by one, keep helpers that have no exact equivalent (debounce, throttle, deep merge, get with paths), and remove the dependency only when rg "from 'lodash" and require('lodash return nothing.

Async patterns

// Parallel independent work, fail fast
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);

// Parallel, keep partial results
const results = await Promise.allSettled(urls.map((u) => fetch(u)));
const failed = results.filter((r) => r.status === "rejected");

// Bounded concurrency without a library
async function mapLimit(items, limit, fn) {
  const out = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      out[i] = await fn(items[i], i);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return out;
}

// Async iteration over streams and paginated APIs
for await (const page of paginate("/api/items")) render(page);

Never await inside a for loop over independent items unless ordering or rate limiting is the point. Never mix async functions with forEach; it does not wait.

Callbacks to promises (Node)

import { readFile } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";
import { promisify } from "node:util";

const data = await readFile(path, "utf8");
await sleep(250);
const legacyAsync = promisify(legacyCallbackFn);

Use the node: prefix for built-in modules.

Newer features: check before using

FeatureStatus to verify
using / await using (explicit resource management)Shipping in recent V8 (Chrome, Node 24+) and TypeScript 5.2+ down-leveling; not in every browser yet
TemporalShipping in some browsers only; use @js-temporal/polyfill or date-fns elsewhere
RegExp.escape, Promise.try, Float16ArrayES2025; recent engines only
Decorators (TC39 version)Need a transpiler; different from TypeScript experimentalDecorators
Import attributes (with { type: "json" })Bundlers and Node 22+; old assert syntax is removed

If the target matrix lacks support, either keep the legacy code, add a targeted polyfill (core-js imports for that feature only), or raise the target with the user’s agreement. Never add a polyfill for something every target already has.

Automate the safe parts

  • ESLint core rules: no-var, prefer-const, prefer-object-has-own, prefer-spread, prefer-rest-params, object-shorthand, and eslint-plugin-unicorn rules such as prefer-at, prefer-structured-clone, no-array-for-each (judgment needed).
  • eslint-plugin-compat or eslint-plugin-es-x flags APIs your browserslist cannot run.
  • Big repos: codemods ($codemods) for mechanical rewrites; hand-review semantic ones like || to ??.

Gotchas

  • ?? vs ||: replacing count || 10 with count ?? 10 changes behavior when count is 0. Decide per call site, and add a test for the falsy case.
  • structuredClone loses class identity: instanceof MyClass is false on the clone.
  • Object.groupBy returns a null-prototype object; result.hasOwnProperty is undefined.
  • toSorted() with no comparator sorts as strings ([10, 9].toSorted() gives [10, 9]), same as sort(). Always pass a comparator for numbers.
  • Arrow functions as object methods or prototype methods lose dynamic this.
  • for...in iterates inherited enumerable keys; use Object.keys/entries or for...of.
  • Private #fields do not survive structuredClone, JSON.stringify, or Vue/MobX proxies.

Verification

  • Tests pass with no edits to assertions.
  • npx browserslist matches what you checked; eslint-plugin-compat (if configured) is clean.
  • Bundle size or dependency count did not grow; report removed dependencies.
  • For each || to ?? or similar semantic change, the PR lists the call site and why the falsy case is now handled correctly.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Modern JavaScript"
  short_description: "Newer built-ins, checked against your real targets"
  default_prompt: "$javascript-modern Modernize this old jQuery-era utility file to current JavaScript"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc