Skip to content

Piero Bosio Social Web Site Personale Logo Fediverso

Social Forum federato con il resto del mondo. Non contano le istanze, contano le persone

Cose da vedere nel tempo aziendale: vecchi sistemi operativi emulati in #JavaScript

Uncategorized
1 1 8

Gli ultimi otto messaggi ricevuti dalla Federazione
Post suggeriti
  • 0 Votes
    1 Posts
    12 Views
    Consider Git's -C option: git -C /path/to/repo checkout <TAB> When you hit <kbd>Tab</kbd>, Git completes branch names from /path/to/repo, not your current directory. The completion is context-aware—it depends on the value of another option. Most CLI parsers can't do this. They treat each option in isolation, so completion for --branch has no way of knowing the --repo value. You end up with two unpleasant choices: either show completions for all possible branches across all repositories (useless), or give up on completion entirely for these options. Optique 0.10.0 introduces a dependency system that solves this problem while preserving 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 if csv is true, you'll have a delimiter field. The parser enforces this at runtime, and shell completion will suggest --pretty only when --json is present. This works well when the valid combinations are known at definition time. But it can't handle cases where valid values depend on runtime input—like branch 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 user typed for the dependency option. Optique 0.10.0 introduces dependency() and derive() to handle exactly this. The dependency system The core idea is simple: mark one option as a dependency source, then create derived 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 source const repoParser = dependency(string()); // Create a derived parser const 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 the actual value the user provided for --repo and returns a parser that validates against 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 has already typed --repo /some/path, the --ref completion will show refs from that path. Repository-aware completion with @optique/git The @optique/git package provides async value parsers that read from Git repositories. Combined with the dependency system, you can build CLIs with repository-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>, the completion will show branches from /path/to/project. The defaultValue of "." means that if --repo isn't specified, it falls back to the current directory. 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. If some dependencies aren't provided, Optique uses the defaultValues. Async support Real-world dependency resolution often involves I/O—reading from Git repositories, querying APIs, accessing databases. Optique provides async variants 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, so gitBranch(), gitTag(), and gitRef() all work in both Node.js and Deno. There's also deriveSync() for when you need to be explicit about synchronous behavior, and deriveFromAsync() for multiple async dependencies. Wrapping up The dependency system lets you build CLIs where options are aware of each other—not just for validation, but for shell completion too. You get type safety throughout: TypeScript knows the relationship between your dependency sources and derived parsers, and invalid combinations are caught at compile time. This is particularly useful for tools that interact with external systems where the set of valid values isn't known until runtime. Git repositories, cloud providers, databases, container registries—anywhere the completion choices depend 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.
  • 0 Votes
    1 Posts
    5 Views
    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: Category What 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.
  • 0 Votes
    5 Posts
    29 Views
    @eniko it's interesting to see how things shape up differently depending on perspective and use case. To embed the RSS feeds of my Fedi accounts on my website, the JS is just a trivial “call this XSLT on this XML”, and I would actually do without the JS if there was a way to, but I'm limited to the number of entries in the RSS and I don't even think it's possible to get user-specific tags via RSS at all. OTOH, once one has to use the API, JS for everything (like in your case) makes more sense.
  • 0 Votes
    1 Posts
    15 Views
    I just published v0.98 of #jSH, a #JavaScript scripting environment for #MSDOS. This is the 'little' text-mode brother to #DOjS. - updated curl, mbedTLS and ziphttps://github.com/SuperIlu/jSH#RetroComputing#FreeDOS#DJGPP#RetroDev#RetroDevelopment