axios Got Poisoned, TypeScript 6 Lands, and JavaScript Finally Handles Dates Like an Adult
Published on 04.04.2026
axios Compromised on npm: Malicious Versions Drop a Remote Access Trojan
TLDR: Two malicious versions of axios were published to npm on March 30, 2026. If you installed [email protected] or [email protected], assume your system is compromised and act accordingly.
Summary: StepSecurity identified [email protected] and [email protected] as poisoned packages. Neither version contains malicious code inside axios itself, which is precisely what makes this attack elegant in the worst possible way. Instead, both versions inject a fabricated dependency called [email protected] that never appears anywhere in the legitimate axios source. Its sole job is to execute a postinstall script that drops a cross-platform remote access trojan targeting macOS, Windows, and Linux simultaneously.
The dropper connects to a live command-and-control server, downloads platform-specific second-stage payloads, then deletes itself and replaces its own package.json with a clean decoy. A developer inspecting node_modules after the fact would see nothing wrong. The malware authors specifically designed it to defeat forensic analysis. This was not opportunistic. Someone studied the axios release process and chose it deliberately because 100 million weekly downloads means extraordinary blast radius.
The attack exposes the structural problem with npm postinstall scripts: you are running arbitrary code from strangers at install time, on the machine doing the installing. Every time. There are tools like Socket and StepSecurity's own Harden Runner that can catch this class of attack. The question is why they are still optional.
Key takeaways:
- Versions [email protected] and [email protected] are malicious. Upgrade to a clean version immediately.
- The attack vector was a fake dependency with a postinstall script, not the axios source itself.
- Supply chain auditing tools can catch this pattern before it executes.
- 100M weekly downloads makes axios one of the highest-value targets in the npm ecosystem.
Why do I care: This is the nightmare scenario for large organizations: a trusted, widely-used package with a spotless reputation gets a single poisoned release. Your security team is looking at the dependency, sees axios, sees the clean source, and moves on. The infection is one layer deeper. Every team running npm install in CI without artifact pinning or postinstall sandboxing is playing Russian roulette. This is not a reason to panic, it is a reason to finally set up proper supply chain controls.
axios Compromised on npm - Malicious Versions Drop Remote Access Trojan
Announcing TypeScript 6.0
TLDR: TypeScript 6.0 is out and it is explicitly a bridge release, cleaning house on decades of legacy options so TypeScript 7.0 (rewritten in Go with parallel type checking) can land cleanly. The deprecation list is long and will break things.
Summary: TypeScript 6.0 ships with the framing that it is the last release built on the current JavaScript codebase. TypeScript 7.0 will be a native port written in Go, and 6.0 exists to align behavior and remove the accumulated baggage that would make that transition messy. That framing is important context before you look at the breaking changes list and wonder what happened.
On the new features side, the headline item is built-in types for Temporal, which reached Stage 4 this cycle. There are also types for the ECMAScript upsert proposal, which adds getOrInsert and getOrInsertComputed to Map and WeakMap. The dom lib now folds in dom.iterable and dom.asynciterable by default, eliminating a genuinely annoying configuration dance that has tripped up developers for years. Subpath imports can now start with just #/ and the --stableTypeOrdering flag helps diagnose ordering differences between 6.0 and the upcoming 7.0 behavior.
The breaking changes are substantial. strict now defaults to true. module defaults to esnext. types now defaults to an empty array instead of "enumerate everything in node_modules/@types," which alone can cut build times 20-50% for large projects. rootDir defaults to the directory containing tsconfig.json rather than inferring from input files. --target es5 is deprecated. --moduleResolution node is deprecated. AMD, UMD, SystemJS, and --outFile are gone. baseUrl is deprecated. --esModuleInterop false is no longer allowed. This list is long enough to warrant a careful migration rather than a blind upgrade.
Key takeaways:
- TypeScript 7.0 (Go-based, parallel type checking) is "within a few months" and already broadly tested inside Microsoft.
- types defaulting to [] instead of all of node_modules/@types is the change most likely to break your build silently.
- You will probably need to explicitly set rootDir: "./src" and types: ["node"] in most existing projects.
- The ts5to6 codemod automates some of the migration, particularly around baseUrl and rootDir.
Why do I care: This is the most consequential TypeScript release in years, and not because of the new features. The deprecation of --moduleResolution node alone will force a lot of teams to finally confront module resolution configurations they copy-pasted years ago and never fully understood. That is probably net positive, but it will generate a lot of confused GitHub issues in the next few months. The Go port is the real story here, and if the performance claims hold up in production, the tooling implications will be significant.
Temporal: The 9-Year Journey to Fix Time in JavaScript
TLDR: The Temporal proposal has reached Stage 4 after nine years of work across multiple companies and browser engines. JavaScript finally has a modern datetime API that handles time zones, calendars, immutability, and daylight saving transitions correctly.
Summary: The original JavaScript Date object was a 10-day port of Java's Date implementation from 1995. Brendan Eich has said as much: a straight port by Ken Smith of Java's Date code. For building simple pages in 1995, fine. For running banking systems, trading terminals, and coordination tools spanning every time zone on earth, catastrophically wrong. Dates are mutable. Month arithmetic silently rolls overflow into the next month. Parsing behavior for "almost ISO" strings varied across browsers in ways the spec never defined.
The community patched this with Moment.js, then date-fns, then Luxon, collectively pulling over 100 million downloads a week. But libraries come with bundle weight, and none of them could be tree-shaken because you rarely know which locales or time zones you will need at build time. Bloomberg faced this problem acutely: the Terminal runs JavaScript in Chromium, Node.js, and SpiderMonkey simultaneously, with users in every time zone, needing nanosecond-precision timestamps, and a user-configured time zone that can differ from the machine's. Bloomberg funded Igalia's work on Temporal for years, and that sustained investment is why the proposal made it to Stage 4 at all.
The API ships as a top-level Temporal namespace containing distinct types: ZonedDateTime for timezone-aware moments, Instant for exact timestamps with nanosecond precision, PlainDate/PlainTime/PlainDateTime for "wall clock" values that deliberately ignore time zones, Duration for arithmetic, and calendar-aware operations that let you add one Hebrew month and actually land on the right day. Temporal is already in Chrome 144, Edge 144, Firefox 139, and TypeScript 6.0. A shared Rust library called temporal_rs now powers both V8 and Boa, which is apparently unprecedented: two competing engines collaborating on a shared implementation for a language proposal.
Key takeaways:
- Temporal.ZonedDateTime is the replacement for new Date() for most use cases.
- Temporal.Instant is nanosecond-precision and has no implicit time zone.
- Plain types are for "wall clock" display values where DST arithmetic would be wrong.
- temporal_rs is a shared Rust library now powering multiple engines, reducing duplicate implementation effort.
- Integration with input date pickers and DOMHighResTimeStamp is still being worked out.
Why do I care: Nine years. Nine years to ship a better date API. The Web standards process is slow by design, but Temporal's complexity and the cross-browser coordination required justify the timeline. What I find genuinely interesting here is temporal_rs: the idea that competing JS engines would co-develop a shared Rust library for a new language feature is the kind of collaboration that was unimaginable a decade ago. If that model generalizes, it could substantially reduce the cost of shipping new built-ins.
Temporal: The 9-Year Journey to Fix Time in JavaScript
Vite 8.0 Is Out
TLDR: Vite 8 ships Rolldown as its single unified Rust-based bundler, replacing the esbuild-for-dev, Rollup-for-production split that powered Vite since the beginning. Real-world build times are dropping 30-64% at companies that have already migrated.
Summary: From the start, Vite made a pragmatic bet: use esbuild for speed during development and Rollup for optimized production builds. That bet worked for years. It also accumulated costs over time: two separate transformation pipelines, two plugin systems, and an increasing volume of glue code to keep them aligned. Every time you fixed an edge case in one pipeline, you risked introducing a difference in the other.
Rolldown is a Rust-based bundler built by the VoidZero team specifically to resolve this. It targets 10-30x faster builds compared to Rollup, matches esbuild's performance level, and supports the same plugin API as Rollup so existing Vite plugins work without modification. The migration was deliberately community-driven: a separate rolldown-vite preview package let early adopters test on real codebases before anything touched stable Vite. Companies like Linear (46s to 6s), Ramp (57% reduction), and Beehiiv (64% reduction) reported production build time drops during the preview period.
Beyond Rolldown, Vite 8 includes integrated Devtools, built-in tsconfig paths support, browser console forwarding to the dev server terminal (which activates automatically when a coding agent is detected, which is a genuinely useful signal), and emitDecoratorMetadata support without external plugins. The install size is about 15MB larger than Vite 7, mostly from lightningcss becoming a normal dependency. There is a compatibility layer that auto-converts existing esbuild and rollupOptions configuration, so most projects should migrate without manual config changes.
Key takeaways:
- Rolldown unifies dev and production bundling for the first time, eliminating the two-pipeline alignment problem.
- Real-world build time reductions of 30-64% have been reported by early adopters.
- Most existing Vite plugins work without changes due to Rolldown's Rollup-compatible plugin API.
- Full Bundle Mode (experimental) targets 3x faster dev server startup and 10x fewer network requests.
- Node.js 20.19+ or 22.12+ required.
Why do I care: The dual-bundler architecture was one of those things developers worked around so long it stopped feeling like a problem. Rolldown makes it a solved problem. The performance numbers from Linear and Beehiiv are not benchmark cherry-picks; these are production builds on real codebases. If you have a large project sitting on Vite 7, the migration is worth the afternoon it will take.
Comprehension Debt: The Hidden Cost of AI-Generated Code
TLDR: AI coding tools are creating a new kind of technical debt: comprehension debt. The codebase grows faster than human understanding of it, creating false confidence that eventually collapses under unexpected pressure.
Summary: Addy Osmani describes comprehension debt as the growing gap between how much code exists in your system and how much of it any human being genuinely understands. Unlike technical debt, which usually announces itself through mounting friction, comprehension debt breeds false confidence. The tests are green. The code looks clean. The reckoning arrives at the worst possible moment, usually when someone needs to explain why a design decision was made or trace an unexpected failure to its root cause.
The speed asymmetry is the core problem. AI generates code far faster than humans can evaluate it. When a developer writes code, the review process has always been a productive bottleneck: reading a PR forces comprehension, surfaces hidden assumptions, and distributes knowledge about what the codebase does across the people responsible for maintaining it. AI-generated code breaks that feedback loop. The volume is too high, and the output is syntactically clean enough that it triggers the signals that historically produced merge confidence, even when systemic correctness is absent.
An Anthropic study cited in the piece found that developers using AI assistance completed a task in roughly the same time as the control group but scored 17% lower on a follow-up comprehension quiz. The largest declines were in debugging. Separate research found developers using AI for passive delegation scored below 40% on comprehension tests, while developers using AI for conceptual inquiry scored above 65%. The tool does not destroy understanding. How you use it does. Tests help but have a hard ceiling: you cannot write a test for behavior you have not thought to specify.
Key takeaways:
- Comprehension debt is invisible to standard velocity metrics, DORA metrics, and code coverage.
- The rate-limiting factor that made code review meaningful (humans writing slower than seniors can review) has been removed by AI.
- Passive delegation ("just make it work") impairs understanding far more than active, question-driven AI use.
- The engineer who genuinely understands the system becomes more valuable as AI volume increases, not less.
Why do I care: This is one of the more honest takes I have read on what actually changes when you go deep on AI coding tools. The point about the comprehension quiz resonates: I have sat in code reviews where someone merged AI-generated code with confidence because the tests passed, and six months later nobody could explain what it was doing or why. The debt is real. The measurement gap is real. The regulation horizon argument at the end, that "the AI wrote it and we did not fully review it" will not hold up in a healthcare or financial post-incident report, is worth taking seriously before it becomes someone else's lesson.
Comprehension Debt - the hidden cost of AI generated code
The Great CSS Expansion
TLDR: A wave of CSS features is landing that explicitly replaces JavaScript-heavy UI patterns. Anchor Positioning, Popover API, native dialog, Scroll-Driven Animations, and View Transitions together eliminate roughly 322kB of JavaScript libraries from a typical modern SPA.
Summary: The pattern is familiar if you have been doing this long enough: something starts as a JavaScript hack, accumulates enough libraries to form an ecosystem, and then the browser makes it native. Rounded corners, custom fonts, smooth scrolling, sticky positioning. Each of those was once JavaScript. The current cycle targets a much larger category of JavaScript: tooltip positioning, modal focus management, scroll-linked animations, and page transitions.
CSS Anchor Positioning replaces @floating-ui/dom and @popperjs/core for the common case. You declare anchor-name on a trigger element and use anchor() to position a floating element relative to it. The browser handles geometry, overflow detection, and position fallbacks automatically, without scroll listeners or resize observers. The Popover API replaces Radix Popover and Headless UI for non-modal overlays: a single popover attribute gives you show/hide toggling, light dismiss, keyboard dismissal, and the top layer, no JavaScript required. Native dialog handles true modal experiences with focus trapping and scroll locking built in. CSS Scroll-Driven Animations replace GSAP ScrollTrigger and run on the compositor thread, meaning they cannot block JavaScript. View Transitions replace react-transition-group and most of Framer Motion's page transition use cases with either three lines of JavaScript for same-document transitions or a single CSS declaration for cross-document navigation.
The bundle math is concrete: @popperjs/core (14.1kB), @floating-ui/dom (8.1kB), @radix-ui/react-dialog (10.6kB), focus-trap (7.4kB), gsap core (26.6kB), gsap/ScrollTrigger (18.3kB), motion/Framer Motion (57.4kB), and react-select (29.1kB) total roughly 171kB of JavaScript that native CSS now covers for most use cases. The full table in the article reaches 322kB across 16 libraries.
Key takeaways:
- CSS Anchor Positioning, Popover API, and native dialog are the highest-leverage replacements to reach for first.
- Scroll-Driven Animations run on the compositor thread; JavaScript scroll listeners do not.
- View Transitions work for both same-document (3 lines of JS) and cross-document navigation (1 line of CSS).
- Drag and drop and overlay scrollbars are still JavaScript territory with no native solution on the horizon.
- Browser support is rolling out; Chrome leads, Firefox and Safari are following at different rates per feature.
Why do I care: The "~322kB" figure is the kind of thing worth printing out and putting next to your dependency audit. Every one of those libraries is something you update, something that can introduce breaking changes, something that can be compromised. The browser-native replacements do not have major versions. They do not get supply chain attacked. For teams building new projects today, this piece should be required reading before pulling in Floating UI or Radix.
The Three Pillars of JavaScript Bloat
TLDR: npm dependency trees are full of redundant packages for three specific reasons: support for ancient engines, atomic micro-package philosophy, and ponyfills that outlived their purpose. Understanding why they exist is the first step to removing them.
Summary: The e18e community has been auditing npm packages and finding the same patterns repeatedly. Why does your app depend on is-string instead of typeof checks? Why does it include hasown instead of Object.hasOwn? The answer is almost always one of three things: the package was written to support ES3-era environments, the author believed in atomic packages as reusable building blocks, or they were shipping a ponyfill for a feature that has been native for a decade.
The ancient engine problem is real for a tiny group of people and a tax on everyone else. The features missing in pre-ES5 engines like Array.prototype.forEach, Object.keys, and Object.defineProperty shipped in 2009. The packages still shipping workarounds for them are in codebases that will never run on IE6. The atomic package philosophy made theoretical sense but produced packages like shebang-regex (a single regex), cli-boxes (a JSON file), and onetime (a function wrapper that prevents a callback from being called twice). These rarely became the reusable building blocks they were supposed to be; shebang-regex is used almost solely by shebang-command by the same maintainer. Finally, ponyfills for features like Object.entries, Array.prototype.indexOf, and globalThis continue pulling 35 million, 2.3 million, and 49 million downloads a week respectively, for features that have had universal engine support for years.
The tools to address this exist: knip finds unused dependencies, the e18e CLI can analyze and automatically migrate some packages to their native equivalents, and npmgraph visualizes where the bloat is coming from. The module-replacements project maintains a central dataset of packages that can be replaced with native functionality.
Key takeaways:
- The e18e CLI's analyze mode can automatically identify replaceable dependencies in your project.
- globalThis ponyfill: 49M downloads/week, has been natively supported since 2019.
- Single-use packages increase supply chain surface area without providing reuse value.
- Larger dependency trees mean more packages that can be compromised, as the axios incident demonstrates.
Why do I care: The axios attack this week is a direct consequence of this problem. Every unnecessary package is another potential point of compromise. The motivation for most of this bloat was legitimate at the time it was written. The problem is that the ecosystem never cleaned house. The e18e initiative is genuinely useful work and worth supporting if you maintain open source packages.
The Three Pillars of JavaScript Bloat
The 49MB Web Page
TLDR: A New York Times article load generates 422 network requests and 49MB of data. This piece tears apart the architecture behind that number: programmatic ad auctions running in the browser, surveillance beacons firing constantly, and deliberate UX hostility engineered to maximize time-on-page metrics.
Summary: Windows 95 fits on 28 floppy disks, totaling 28MB. The New York Times homepage exceeds that. Before you have read a single headline, the browser has downloaded, parsed, and compiled megabytes of JavaScript to run a real-time programmatic ad auction across exchanges like Rubicon Project and Amazon Ad Systems. The auction results inject an iframe above the viewport, shifting layout and destroying the reader's place in the text. That heat on the back of your phone is the browser executing high-frequency financial trading logic on your behalf without asking.
The surveillance apparatus is deliberate. POST beacons fire continuously to first-party tracking endpoints. Invisible pixel drops and redirects to doubleclick.net and casalemedia stitch cross-site identity together across ad networks. The consent framework endpoint, which is supposed to give users control, is named purr.nytimes.com/tcf. A cat purring while it rifles through your pockets. The author points out the structural paradox: Google's search arm penalizes pages with intrusive interstitials and high CLS, while Google's ads arm sells the products causing both.
The UX failures are a specific result of optimizing for viewability and time-on-page CPM metrics. Publishers know that the longer you are trapped on the page, the more they can charge advertisers. Every hostile UX decision follows from that incentive: deliberately low-contrast close buttons, sticky video players that follow you down the page, article truncation at the halfway point to generate a click event, and modals that occupy the bottom 30% of the viewport the moment the page loads. No individual engineer decided to make reading miserable. This architecture emerged from thousands of small incentive decisions, each locally rational and collectively catastrophic.
Key takeaways:
- Reserve space for async ad slots with defined aspect-ratios or min-heights to eliminate CLS.
- Newsletter signups placed between paragraphs 4-5 outperform page-load modals on conversion because intent aligns with action.
- text.npr.org, lite.cnn.com, and cbc.ca/lite demonstrate that readable news sites are technically feasible.
- Save cookie consent state to localStorage and never show it again in the same session after dismissal.
Why do I care: This is a systems architecture problem masquerading as a UX problem. The engineers at these publications are not incompetent; they are optimizing for the metrics they are measured on. The fix requires changing the incentive structure, which requires either regulation or reader behavior change. RSS is not dead. An adblocker is not just a preference; it is a reasonable response to a system that treats you as an extractable resource.
Form-Associated Custom Elements in Practice
TLDR: If you are building web components that wrap form controls, they will not appear in FormData, respect form.reset(), or respond to fieldset disabled unless you implement the Form-Associated Custom Element API. This piece explains how to actually do that.
Summary: The author discovered, three-quarters of the way through rewriting AgnosticUI with Lit, that none of his form components participated in native form submissions. A custom ag-input wrapped in a form and submitted would produce an empty FormData object. This is not a Lit problem or a Web Components problem specifically; it is the expected behavior when an input lives inside a Shadow DOM boundary, isolated from the parent document's form infrastructure.
The Form-Associated Custom Element API starts with static formAssociated = true and this.attachInternals() in the constructor. What looks like a simple opt-in is actually opening a contract: the browser now expects you to call setFormValue() to register values, setValidity() to participate in native validation, and handle lifecycle callbacks for formResetCallback, formDisabledCallback, and formStateRestoreCallback. None of this happens automatically. Browser support is broad as of early 2026, covering Chromium, Firefox, and Safari 16.4+.
The piece walks through two validation strategies. Strategy 1 applies when a component renders a native input, textarea, or select inside its Shadow DOM: delegate to the inner element's .validity object rather than reimplementing constraint validation logic. Strategy 2 applies to custom widgets like toggles or star ratings that have no inner native control: implement setValidity() directly against component state. Radio groups require a third approach because Shadow DOM isolation breaks the browser's native cross-group coordination; you have to manually find siblings using getRootNode() rather than document.querySelectorAll, because the latter cannot pierce Shadow Roots from encapsulating components.
Key takeaways:
- formAssociated = true is just an invitation. Nothing appears in FormData until you call setFormValue().
- Inputs inside a Shadow DOM are invisible to ancestor forms, even with name and value attributes.
- firstUpdated must call setFormValue() or pre-filled values will not register until user interaction.
- null passed to setFormValue() means "absent from FormData," which is the correct behavior for unchecked checkboxes.
- A submit button inside a Shadow Root cannot trigger a parent form; use this.closest('form').requestSubmit().
Why do I care: This is exactly the kind of hard-won knowledge that saves weeks of debugging. The Shadow DOM/form isolation behavior is not obvious, the fix is non-trivial, and the Lit mixin pattern shown here is genuinely reusable. If you are building or evaluating a Web Component design system, understanding FACE is not optional. It is the difference between components that work as form controls and components that look like form controls.
Form-Associated Custom Elements in Practice
The Big Gotcha of Anchor Positioning
TLDR: CSS Anchor Positioning does not work regardless of DOM position, despite what the marketing says. The anchor element must be fully laid out before the anchored element, and they need to be in the same containing block or have the anchor positioned statically. This breaks when you do sensible DOM things.
Summary: Chris Coyier has been telling people that one of the great benefits of CSS Anchor Positioning is that you can position elements relative to other elements regardless of where they are in the DOM. He now admits that is not quite right. There are significant DOM ordering constraints that are both real and counterintuitive, representing a new class of CSS troubleshooting that has not existed before.
The rules, as distilled from Temani Afif's deeper analysis: the anchor element must be fully laid out before the anchored element. If they are siblings and the anchor has any position value other than the default static, the anchor must appear first in the DOM. If they are in different parts of the DOM, they need to be in the same containing block, or the anchor's parent must maintain static positioning. The safest advice is to make the anchor and positioned element siblings, with the anchor first in the DOM.
The author's frustration is not with the technical reality but with the footgun: it is far too easy to place these elements in a sensible DOM order and watch anchoring silently fail. He argues the CSS Working Group should do something about it, possibly a "second pass" in anchor resolution, and points to an open issue in the CSS spec repository (w3c/csswg-drafts#13751) where relaxing the absolute positioning ordering requirement is being discussed.
Key takeaways:
- Make the anchor and positioned element siblings, with the anchor first in the DOM for maximum reliability.
- Static positioning on the anchor's parent is required if the anchor and positioned element are not siblings.
- anchor-tool is useful for remembering how anchor() and span work.
- The Inset-Modified Containing Block (IMCB) is the coordinate space your anchored element lives in.
Why do I care: Anchor Positioning is genuinely exciting because it eliminates a huge category of JavaScript layout code. But these DOM ordering constraints are going to bite teams that reach for it without reading the fine print. Tooltips live in portals for a reason: DOM order does not always match visual hierarchy. If Anchor Positioning requires you to restructure your DOM to match layout intentions, that undermines one of its main selling points. Worth watching the csswg-drafts issue.
The Big Gotcha of Anchor Positioning
When Deno or Bun Is a Better Solution than Node.js
TLDR: A freelance developer who ships production code across all three runtimes explains when each one is actually the right choice: Deno for security requirements and self-contained distribution, Bun for iteration speed, Node.js when hiring speed and ecosystem depth matter more than anything else.
Summary: The default to Node.js without evaluation is what the author calls "the default approach." It ignores three friction points that consistently signal a runtime mismatch: the configuration tax (TypeScript needs five packages and three config files before you write business logic), the security assumption (your code has access to everything by default), and tooling fragmentation (separate package manager, test runner, bundler, and TypeScript compiler with their own upgrade cycles).
Deno earns its place in specific scenarios. Running user-submitted code becomes tractable with Deno's permission system: whitelist specific API domains with --allow-net=api.banking-partner.com and a malicious or buggy user script literally cannot phone home. The self-contained binary story is also real; deno compile produces a 60MB self-contained executable that runs on a locked-down RHEL server without npm install or version conflicts. Bun earns its place on speed: fresh installs go from 38 seconds to 6, dev server cold start from 2.8 seconds to 0.7, test suites from 29 seconds to 9. These numbers were measured on a real Next.js 15 app with 200 dependencies and 52k lines of TypeScript. The author also notes that runtime choice is a cultural filter: Deno and Bun requirements in job postings disproportionately attract engineers who read changelogs for fun.
The honest failure mode is also documented: a SaaS client's migration to Bun hit race conditions under load related to worker_threads compatibility. They kept Bun for the dev toolchain and Node.js in production. That turned out to be the right call, not a compromise.
Key takeaways:
- Deno's permission model can replace Docker containers for sandboxing untrusted code in many scenarios.
- Bun's toolchain unification (package manager + test runner + bundler + TypeScript) reduces upgrade surface area.
- The dev environment, CI pipeline, and production server can have different runtime answers.
- Bun for dev toolchain + Node.js in production is a legitimate hybrid strategy, not a failure state.
- Runtime choice signals culture: Deno/Bun requirements filter for engineers who actively explore new tools.
Why do I care: I have seen teams stay on Node.js for years past the point where it was clearly causing friction, simply because it was there. The framing here is useful: treat runtime selection as an engineering decision against explicit requirements, not an inherited assumption. The Deno security model in particular is underutilized. For any architecture that runs user-provided code, the permission-based sandboxing is a genuine architectural simplification compared to Docker container isolation.
When Deno or Bun is a Better Solution than Node.js
No AI in Node.js Core: A Petition
TLDR: A petition signed by hundreds of Node.js contributors asks the Technical Steering Committee to reject AI-generated pull requests to Node.js core, following a 19,000-line PR generated with Claude Code by a long-time contributor.
Summary: In January 2026, a well-known and long-time Node.js core contributor opened a 19,000-line pull request with a disclaimer in the description: "I've used a significant amount of Claude Code tokens to create this PR. I've reviewed all changes myself." This triggered a debate about whether AI-generated code satisfies the Developer Certificate of Origin, whether reviewers can meaningfully audit code they did not write at that volume, and what the acceptance of AI-generated changes does to the reputational foundation of a critical infrastructure project running on millions of servers.
The OpenJS Foundation's legal opinion is that LLM-assisted changes are not in violation of the DCO. The petition authors argue that is only a small part of the problem. They raise concerns about the training data sourcing behind major LLMs, the educational value of the review process for contributors who are learning, the fact that LLMs cannot learn from code review feedback so reviewer time is consumed without advancing anyone's skills, and the reproducibility concern that reviewers should be able to reproduce a change without going through a paid subscription.
The petition has been signed by Node.js TSC Emeritus members, long-time core collaborators, and recognizable names in the JavaScript community. Kyle Simpson (YDKJS author) is on the list. The debate it represents is broader than Node.js: it is the question of what "authored by" means when a contributor reviews rather than writes.
Key takeaways:
- The 19k-line AI-assisted PR is the specific incident that triggered this.
- OpenJS Foundation says LLM-assisted code does not violate the DCO; the petitioners say that misses the point.
- The educational argument is interesting: reviewers who cannot build understanding from reviewing AI-generated code waste time without knowledge transfer.
- Reproducibility of a change without a paid LLM subscription is a genuine open source equity concern.
Why do I care: This is a genuinely hard problem and the petition represents one reasonable position. My instinct is that the right frame is not "was AI involved" but "does the human contributor genuinely understand and take responsibility for every line." A 19,000-line PR is hard to review whether AI wrote it or a human did. The volume is the problem as much as the tool. But the Node.js core is not the place to experiment with what responsible AI-assisted contribution looks like. The stakes are too high and the review burden too asymmetric.
NoJS 3: Making Flappy Bird with Pure HTML and CSS
TLDR: Someone built a fully playable Flappy Bird clone using only HTML and CSS, no JavaScript, by exploiting animated custom properties, radio button state, the :has() selector, and CSS collision detection math. It is absolutely wild.
Summary: The author previously built a calculator and tic-tac-toe entirely in CSS. A colleague misremembered these as "Flappy Bird in CSS," and the thought would not leave. The result is a complete Flappy Bird implementation with click-to-flap, pseudo-random pipe generation, scoring, and collision detection, all in HTML and CSS with JavaScript disabled in browser settings during testing.
The click-to-flap mechanism uses radio buttons and labels stacked on top of each other, animated along with the bird. Only the label at the bird's current height is exposed to the user through a narrow visible slit; clicking it checks the corresponding radio button, which CSS detects via :has(), which updates a CSS custom property tracking the bird's position. Two identical animations named differently allow the jump animation to restart on each click by tricking CSS into treating the second as a new animation.
Pseudo-random pipe heights use trigonometric functions applied to a --pipe-index counter that increments deterministically as pipes scroll off-screen. A seed variable, animated while the start screen is visible, provides different values each game. Collision detection is pure math: --overlap-in-x and --overlap-in-y are calculated using calc() with max() to determine whether the bird and pipe rectangles intersect. When --collision exceeds zero, the game over screen gets height: 100vh * var(--collision), making it appear. The animations pause when the player hovers over it.
Key takeaways:
- Animated CSS custom properties via @property enable the entire jump/fall physics.
- The :has() selector provides the event listener equivalent for radio button state changes.
- Two identically-specced animations with different names can restart an animation by swapping them.
- CSS trig functions (sin, cos) enable pseudo-random number generation from a deterministic seed.
- Units matter: dividing by 1px converts area units back to lengths in multiplication chains.
Why do I care: This is delightful nonsense in the best possible way. No one should build Flappy Bird in CSS for production. But the techniques here, especially @property for animatable custom properties, :has() for state-driven styling, and the collision detection math using calc(), are completely real and applicable in legitimate contexts. The author also mentions Gemini 3 Pro gave up when given the same problem with the same amount of guidance. That part I found particularly interesting.