Posts in Javascript (20 found)
devansh Today

Hacking Better-Hub

Better-Hub ( better-hub.com ) is an alternative GitHub frontend — a richer, more opinionated UI layer built on Next.js that sits on top of the GitHub API. It lets developers browse repositories, view issues, pull requests, code blobs, and repository prompts, while authenticating via GitHub OAuth. Because Better-Hub mirrors GitHub content inside its own origin, any unsanitized rendering of user-controlled data becomes significantly more dangerous than it would be on a static page — it has access to session tokens, OAuth credentials, and the authenticated GitHub API. That attack surface is exactly what I set out to explore. Description The repository README is fetched from GitHub, piped through with and enabled — with zero sanitization — then stored in the state and rendered via in . Because the README is entirely attacker-controlled, any repository owner can embed arbitrary JavaScript that executes in every viewer's browser on better-hub.com. Steps to Reproduce Session hijacking via cookie theft, credential exfiltration, and full client-side code execution in the context of better-hub.com. Chains powerfully with the GitHub OAuth token leak (see vuln #10). Description Issue descriptions are rendered with the same vulnerable pipeline: with raw HTML allowed and no sanitization. The resulting is inserted directly via inside the thread entry component, meaning a malicious issue body executes arbitrary script for every person who views it on Better-Hub. Steps to Reproduce Arbitrary JavaScript execution for anyone viewing the issue through Better-Hub. Can be used for session hijacking, phishing overlays, or CSRF-bypass attacks. Description Pull request bodies are fetched from GitHub and processed through with / and no sanitization pass, then rendered unsafely. An attacker opening a PR with an HTML payload in the body causes XSS to fire for every viewer of that PR on Better-Hub. Steps to Reproduce Stored XSS affecting all viewers of the PR. Particularly impactful in collaborative projects where multiple team members review PRs. Description The same unsanitized pipeline applies to PR comments. Any GitHub user who can comment on a PR can inject a stored XSS payload that fires for every Better-Hub viewer of that conversation thread. Steps to Reproduce A single malicious commenter can compromise every reviewer's session on the platform. Description The endpoint proxies GitHub repository content and determines the from the file extension in the query parameter. For files it sets and serves the content inline (no ). An attacker can upload a JavaScript-bearing SVG to any GitHub repo and share a link to the proxy endpoint — the victim's browser executes the script within 's origin. Steps to Reproduce Reflected XSS with a shareable, social-engineered URL. No interaction with a real repository page is needed — just clicking a link is sufficient. Easily chained with the OAuth token leak for account takeover. Description When viewing code files larger than 200 KB, the application hits a fallback render path in that outputs raw file content via without any escaping. An attacker can host a file exceeding the 200 KB threshold containing an XSS payload — anyone browsing that file on Better-Hub gets the payload executed. Steps to Reproduce Any repository owner can silently weaponize a large file. Because code review is often done on Better-Hub, this creates a highly plausible attack vector against developers reviewing contributions. Description The function reads file content from a shared Redis cache . Cache entries are keyed by repository path alone — not by requesting user. The field is marked as shareable, so once any authorized user views a private file through the handler or the blob page, its contents are written to Redis under a path-only key. Any subsequent request for the same path — from any user, authenticated or not — is served directly from cache, completely bypassing GitHub's permission checks. Steps to Reproduce Complete confidentiality breach of private repositories. Any file that has ever been viewed by an authorized user is permanently exposed to unauthenticated requests. This includes source code, secrets in config files, private keys, and any other sensitive repository content. Description A similar cache-keying problem affects the issue page. When an authorized user views a private repo issue on Better-Hub, the issue's full content is cached and later embedded in Open Graph meta properties of the page HTML. A user who lacks repository access — and sees the "Unable to load repository" error — can still read the issue content by inspecting the page source, where it leaks in the meta tags served from cache. Steps to Reproduce Private issue contents — potentially including bug reports, credentials in descriptions, or internal discussion — are accessible to any unauthenticated party who knows or guesses the URL. Description Better-Hub exposes a Prompts feature tied to repositories. For private repositories, the prompt data is included in the server-rendered page source even when the requestor does not have repository access. The error UI correctly shows "Unable to load repository," but the prompt content is already serialized into the HTML delivered to the browser. Steps to Reproduce Private AI prompts — which may contain internal instructions, proprietary workflows, or system prompt secrets — leak to unauthenticated users. Description returns a session object that includes . This session object is passed as props directly to client components ( , , etc.). Next.js serializes component props and embeds them in the page HTML for hydration, meaning the raw GitHub access token is present in the page source and accessible to any JavaScript running on the page — including scripts injected via any of the XSS vulnerabilities above. The fix is straightforward: strip from the session object before passing it as props to client components. Token usage should remain server-side only. When chained with any XSS in this report, an attacker can exfiltrate the victim's GitHub OAuth token and make arbitrary GitHub API calls on their behalf — reading private repos, writing code, managing organizations, and more. This elevates every XSS in this report from session hijacking to full GitHub account takeover . Description The home page redirects authenticated users to the destination specified in the query parameter with no validation or allow-listing. An attacker can craft a login link that silently redirects the victim to an attacker-controlled domain immediately after they authenticate. Steps to Reproduce Phishing attacks exploiting the trusted better-hub.com domain. Can be combined with OAuth token flows for session fixation attacks, or used to redirect users to convincing fake login pages post-authentication. All issues were reported directly to Better-Hub team. The team was responsive and attempted rapid remediation. What is Better-Hub? The Vulnerabilities 01. Unsanitized README → XSS 02. Issue Description → XSS 03. Stored XSS in PR Bodies 04. Stored XSS in PR Comments 05. Reflected XSS via SVG Image Proxy 06. Large-File XSS (>200 KB) 07. Cache Deception — Private File Access 08. Authz Bypass via Issue Cache 09. Private Repo Prompt Leak 10. GitHub OAuth Token Leaked to Client 11. Open Redirect via Query Parameter Disclosure Timeline Create a GitHub repository with the following content in : View the repository at and observe the XSS popup. Create a GitHub issue with the following in the body: Navigate to the issue via to trigger the payload. Open a pull request whose body contains: View the PR through Better-Hub to observe the XSS popup. Post a PR comment containing: View the comment thread via Better-Hub to trigger the XSS. Create an SVG file in a public GitHub repo with content: Direct the victim to: Create a file named containing the payload, padded to exceed 200 KB: Browse to the file on Better-Hub at . The XSS fires immediately. Create a private repository and add a file called . As the repository owner, navigate to the following URL to populate the cache: Open the same URL in an incognito window or as a completely different user. The private file content is served — no authorization required. Create a private repo and create an issue with a sensitive body. Open the issue as an authorized user: Open the same URL in a different session (no repo access). While the access-error UI is shown, view the page source — issue details appear in the tags. Create a private repository and create a prompt in it. Open the prompt URL as an unauthorized user: View the page source — prompt details are present in the HTML despite the access-denied UI. Log in to Better-Hub with GitHub credentials. Navigate to: You are immediately redirected to .

0 views
Kev Quirk 2 days ago

Quick Clarification on Pure Comments

A couple of people have reached out to me asking if I can offer a version of Pure Comments for their site, as they don't run Pure Blog . I obviously didn't make this clear in the announcement , or on the (now updated) Pure Comments site. Pure Comments can be used on ANY website. It's just an embed script, just like Disqus (only with no bloat or tracking). So you just have to upload the files to wherever you want to host Pure Comments, then add the following embed code wherever you want comments to display (replacing the example domain): You can use Pure Comments on WordPress, Bear Blog, Jekyll, 11ty, Hugo, Micro.blog, Kirby, Grav, and even Pure Blog! Anywhere you can inject the little snippet of code above, Pure Comments will work. Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️ You can reply to this post by email , or leave a comment .

0 views
(think) 2 days ago

Building Emacs Major Modes with TreeSitter: Lessons Learned

Over the past year I’ve been spending a lot of time building TreeSitter-powered major modes for Emacs – clojure-ts-mode (as co-maintainer), neocaml (from scratch), and asciidoc-mode (also from scratch). Between the three projects I’ve accumulated enough battle scars to write about the experience. This post distills the key lessons for anyone thinking about writing a TreeSitter-based major mode, or curious about what it’s actually like. Before TreeSitter, Emacs font-locking was done with regular expressions and indentation was handled by ad-hoc engines (SMIE, custom indent functions, or pure regex heuristics). This works, but it has well-known problems: Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter modes) requires defining operator precedence grammars for the language, which is hard to get right. Custom indentation functions tend to grow into large, brittle state machines. Tuareg’s indentation code, for example, is thousands of lines long. TreeSitter changes the game because you get a full, incremental, error-tolerant syntax tree for free. Font-locking becomes “match this AST pattern, apply this face”: And indentation becomes “if the parent node is X, indent by Y”: The rules are declarative, composable, and much easier to reason about than regex chains. In practice, ’s entire font-lock and indentation logic fits in about 350 lines of Elisp. The equivalent in tuareg is spread across thousands of lines. That’s the real selling point: simpler, more maintainable code that handles more edge cases correctly . That said, TreeSitter in Emacs is not a silver bullet. Here’s what I ran into. TreeSitter grammars are written by different authors with different philosophies. The tree-sitter-ocaml grammar provides a rich, detailed AST with named fields. The tree-sitter-clojure grammar, by contrast, deliberately keeps things minimal – it only models syntax, not semantics, because Clojure’s macro system makes static semantic analysis unreliable. 1 This means font-locking forms in Clojure requires predicate matching on symbol text, while in OCaml you can directly match nodes with named fields. To illustrate: here’s how you’d fontify a function definition in OCaml, where the grammar gives you rich named fields: And here’s the equivalent in Clojure, where the grammar only gives you lists of symbols and you need predicate matching: You can’t learn “how to write TreeSitter queries” generically – you need to learn each grammar individually. The best tool for this is (to visualize the full parse tree) and (to see the node at point). Use them constantly. You’re dependent on someone else providing the grammar, and quality is all over the map. The OCaml grammar is mature and well-maintained – it’s hosted under the official tree-sitter GitHub org. The Clojure grammar is small and stable by design. But not every language is so lucky. asciidoc-mode uses a third-party AsciiDoc grammar that employs a dual-parser architecture – one parser for block-level structure (headings, lists, code blocks) and another for inline formatting (bold, italic, links). This is the same approach used by Emacs’s built-in , and it makes sense for markup languages where block and inline syntax are largely independent. The problem is that the two parsers run independently on the same text, and they can disagree . The inline parser misinterprets and list markers as emphasis delimiters, creating spurious bold spans that swallow subsequent inline content. The workaround is to use on all block-level font-lock rules so they win over the incorrect inline faces: This doesn’t fix inline elements consumed by the spurious emphasis – that requires an upstream grammar fix. When you hit grammar-level issues like this, you either fix them yourself (which means diving into the grammar’s JavaScript source and C toolchain) or you live with workarounds. Either way, it’s a reminder that your mode is only as good as the grammar underneath it. Getting the font-locking right in was probably the most challenging part of all three projects, precisely because of these grammar quirks. I also ran into a subtle behavior: the default font-lock mode ( ) skips an entire captured range if any position within it already has a face. So if you capture a parent node like and a child was already fontified, the whole thing gets skipped silently. The fix is to capture specific child nodes instead: These issues took a lot of trial and error to diagnose. The lesson: budget extra time for font-locking when working with less mature grammars . Grammars evolve, and breaking changes happen. switched from the stable grammar to the experimental branch because the stable version had metadata nodes as children of other nodes, which caused and to behave incorrectly. The experimental grammar makes metadata standalone nodes, fixing the navigation issues but requiring all queries to be updated. pins to v0.24.0 of the OCaml grammar. If you don’t pin versions, a grammar update can silently break your font-locking or indentation. The takeaway: always pin your grammar version , and include a mechanism to detect outdated grammars. tests a query that changed between versions to detect incompatible grammars at startup. Users shouldn’t have to manually clone repos and compile C code to use your mode. Both and include grammar recipes: On first use, the mode checks and offers to install missing grammars via . This works, but requires a C compiler and Git on the user’s machine, which is not ideal. 2 The TreeSitter support in Emacs has been improving steadily, but each version has its quirks: Emacs 29 introduced TreeSitter support but lacked several APIs. For instance, (used for structured navigation) doesn’t exist – you need a fallback: Emacs 30 added , sentence navigation, and better indentation support. But it also had a bug in offsets ( #77848 ) that broke embedded parsers, and another in that required to disable its TreeSitter-aware version. Emacs 31 has a bug in where an off-by-one error causes to leave ` *)` behind on multi-line OCaml comments. I had to skip the affected test with a version check: The lesson: test your mode against multiple Emacs versions , and be prepared to write version-specific workarounds. CI that runs against Emacs 29, 30, and snapshot is essential. Most TreeSitter grammars ship with query files for syntax highlighting ( ) and indentation ( ). Editors like Neovim and Helix use these directly. Emacs doesn’t – you have to manually translate the patterns into and calls in Elisp. This is tedious and error-prone. For example, here’s a rule from the OCaml grammar’s : And here’s the Elisp equivalent you’d write for Emacs: The query syntax is nearly identical, but you have to wrap everything in calls, map upstream capture names ( ) to Emacs face names ( ), assign features, and manage behavior. You end up maintaining a parallel set of queries that can drift from upstream. Emacs 31 will introduce which will make it possible to use files for font-locking, which should help significantly. But for now, you’re hand-coding everything. When a face isn’t being applied where you expect: TreeSitter modes define four levels of font-locking via , and the default level in Emacs is 3. It’s tempting to pile everything into levels 1–3 so users see maximum highlighting out of the box, but resist the urge. When every token on the screen has a different color, code starts looking like a Christmas tree and the important things – keywords, definitions, types – stop standing out. Less is more here. Here’s how distributes features across levels: And follows the same philosophy: The pattern is the same: essentials first, progressively more detail at higher levels. This way the default experience (level 3) is clean and readable, and users who want the full rainbow can bump to 4. Better yet, they can use to cherry-pick individual features regardless of level: This gives users fine-grained control without requiring mode authors to anticipate every preference. Indentation issues are harder to diagnose because they depend on tree structure, rule ordering, and anchor resolution: Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: This is the single most important piece of advice. Font-lock and indentation are easy to break accidentally, and manual testing doesn’t scale. Both projects use Buttercup (a BDD testing framework for Emacs) with custom test macros. Font-lock tests insert code into a buffer, run , and assert that specific character ranges have the expected face: Indentation tests insert code, run , and assert the result matches the expected indentation: Integration tests load real source files and verify that both font-locking and indentation survive on the full file. This catches interactions between rules that unit tests miss. has 200+ automated tests and has even more. Investing in test infrastructure early pays off enormously – I can refactor indentation rules with confidence because the suite catches regressions immediately. When I became the maintainer of clojure-mode many years ago, I really struggled with making changes. There were no font-lock or indentation tests, so every change was a leap of faith – you’d fix one thing and break three others without knowing until someone filed a bug report. I spent years working on a testing approach I was happy with, alongside many great contributors, and the return on investment was massive. The same approach – almost the same test macros – carried over directly to when we built the TreeSitter version. And later I reused the pattern again in and . One investment in testing infrastructure, four projects benefiting from it. I know that automated tests, for whatever reason, never gained much traction in the Emacs community. Many popular packages have no tests at all. I hope stories like this convince you that investing in tests is really important and pays off – not just for the project where you write them, but for every project you build after. This one is specific to but applies broadly: compiling TreeSitter queries at runtime is expensive. If you’re building queries dynamically (e.g. with called at mode init time), consider pre-compiling them as values. This made a noticeable difference in ’s startup time. The Emacs community has settled on a suffix convention for TreeSitter-based modes: , , , and so on. This makes sense when both a legacy mode and a TreeSitter mode coexist in Emacs core – users need to choose between them. But I think the convention is being applied too broadly, and I’m afraid the resulting name fragmentation will haunt the community for years. For new packages that don’t have a legacy counterpart, the suffix is unnecessary. I named my packages (not ) and (not ) because there was no prior or to disambiguate from. The infix is an implementation detail that shouldn’t leak into the user-facing name. Will we rename everything again when TreeSitter becomes the default and the non-TS variants are removed? Be bolder with naming. If you’re building something new, give it a name that makes sense on its own merits, not one that encodes the parsing technology in the package name. I think the full transition to TreeSitter in the Emacs community will take 3–5 years, optimistically. There are hundreds of major modes out there, many maintained by a single person in their spare time. Converting a mode from regex to TreeSitter isn’t just a mechanical translation – you need to understand the grammar, rewrite font-lock and indentation rules, handle version compatibility, and build a new test suite. That’s a lot of work. Interestingly, this might be one area where agentic coding tools can genuinely help. The structure of TreeSitter-based major modes is fairly uniform: grammar recipes, font-lock rules, indentation rules, navigation settings, imenu. If you give an AI agent a grammar and a reference to a high-quality mode like , it could probably scaffold a reasonable new mode fairly quickly. The hard parts – debugging grammar quirks, handling edge cases, getting indentation just right – would still need human attention, but the boilerplate could be automated. Still, knowing the Emacs community, I wouldn’t be surprised if a full migration never actually completes. Many old-school modes work perfectly fine, their maintainers have no interest in TreeSitter, and “if it ain’t broke, don’t fix it” is a powerful force. And that’s okay – diversity of approaches is part of what makes Emacs Emacs. TreeSitter is genuinely great for building Emacs major modes. The code is simpler, the results are more accurate, and incremental parsing means everything stays fast even on large files. I wouldn’t go back to regex-based font-locking willingly. But it’s not magical. Grammars are inconsistent across languages, the Emacs APIs are still maturing, you can’t reuse files (yet), and you’ll hit version-specific bugs that require tedious workarounds. The testing story is better than with regex modes – tree structures are more predictable than regex matches – but you still need a solid test suite to avoid regressions. If you’re thinking about writing a TreeSitter-based major mode, do it. The ecosystem needs more of them, and the experience of working with syntax trees instead of regexes is genuinely enjoyable. Just go in with realistic expectations, pin your grammar versions, test against multiple Emacs releases, and build your test suite early. Anyways, I wish there was an article like this one when I was starting out with and , so there you have it. I hope that the lessons I’ve learned along the way will help build better modes with TreeSitter down the road. That’s all I have for you today. Keep hacking! See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎ Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter modes) requires defining operator precedence grammars for the language, which is hard to get right. Custom indentation functions tend to grow into large, brittle state machines. Tuareg’s indentation code, for example, is thousands of lines long. Use to verify the node type at point matches your query. Set to to see which rules are firing. Check the font-lock feature level – your rule might be in level 4 while the user has the default level 3. The features are assigned to levels via . Remember that rule order matters . Without , an earlier rule that already fontified a region will prevent later rules from applying. This can be intentional (e.g. builtin types at level 3 take precedence over generic types) or a source of bugs. Set to – this logs which rule matched for each line, what anchor was computed, and the final column. Use to understand the parent chain. The key question is always: “what is the parent node, and which rule matches it?” Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎

0 views
baby steps 2 days ago

How Dada enables internal references

In my previous Dada blog post, I talked about how Dada enables composable sharing. Today I’m going to start diving into Dada’s permission system; permissions are Dada’s equivalent to Rust’s borrow checker. Dada aims to exceed Rust’s capabilities by using place-based permissions. Dada lets you write functions and types that capture both a value and things borrowed from that value . As a fun example, imagine you are writing some Rust code to process a comma-separated list, just looking for entries of length 5 or more: One of the cool things about Rust is how this code looks a lot like some high-level language like Python or JavaScript, but in those languages the call is going to be doing a lot of work, since it will have to allocate tons of small strings, copying out the data. But in Rust the values are just pointers into the original string and so is very cheap. I love this. On the other hand, suppose you want to package up some of those values, along with the backing string, and send them to another thread to be processed. You might think you can just make a struct like so… …and then create the list and items and store them into it: But as experienced Rustaceans know, this will not work. When you have borrowed data like an , that data cannot be moved. If you want to handle a case like this, you need to convert from into sending indices, owned strings, or some other solution. Argh! Dada does things a bit differently. The first thing is that, when you create a reference, the resulting type names the place that the data was borrowed from , not the lifetime of the reference . So the type annotation for would say 1 (at least, if you wanted to write out the full details rather than leaving it to the type inferencer): I’ve blogged before about how I would like to redefine lifetimes in Rust to be places as I feel that a type like is much easier to teach and explain: instead of having to explain that a lifetime references some part of the code, or what have you, you can say that “this is a that references the variable ”. But what’s also cool is that named places open the door to more flexible borrows. In Dada, if you wanted to package up the list and the items, you could build a type like so: Note that last line – . We can create a new class and move into it along with , which borrows from list. Neat, right? OK, so let’s back up and talk about how this all works. Let’s start with syntax. Before we tackle the example, I want to go back to the example from previous posts, because it’s a bit easier for explanatory purposes. Here is some Rust code that declares a struct , creates an owned copy of it, and then gets a few references into it. The Dada equivalent to this code is as follows: The first thing to note is that, in Dada, the default when you name a variable or a place is to create a reference. So doesn’t move , as it would in Rust, it creates a reference to the stored in . You could also explicitly write , but that is not preferred. Similarly, creates a reference to the value in the field . (If you wanted to move the character, you would write , not as in Rust.) Notice that I said “creates a reference to the stored in ”. In particular, I did not say “creates a reference to ”. That’s a subtle choice of wording, but it has big implications. The reason I wrote that “creates a reference to the stored in ” and not “creates a reference to ” is because, in Dada, references are not pointers . Rather, they are shallow copies of the value, very much like how we saw in the previous post that a acts like an but is represented as a shallow copy. So where in Rust the following code… …looks like this in memory… in Dada, code like this would look like so Clearly, the Dada representation takes up more memory on the stack. But note that it doesn’t duplicate the memory in the heap, which tends to be where the vast majority of the data is found. This gets at something important. Rust, like C, makes pointers first-class. So given , refers to the pointer and refers to its referent, the . Dada, like Java, goes another way. is a value – including in memory representation! The difference between a , , and is not in their memory layout, all of them are the same, but they differ in whether they own their contents . 2 So in Dada, there is no operation to go from “pointer” to “referent”. That doesn’t make sense. Your variable always contains a string, but the permissions you have to use that string will change. In fact, the goal is that people don’t have to learn the memory representation as they learn Dada, you are supposed to be able to think of Dada variables as if they were all objects on the heap, just like in Java or Python, even though in fact they are stored on the stack. 3 In Rust, you cannot move values while they are borrowed. So if you have code like this that moves into … …then this code only compiles if is not used again: There are two reasons that Rust forbids moves of borrowed data: Neither of these apply to Dada: OK, let’s revisit that Rust example that was giving us an error. When we convert it to Dada, we find that it type checks just fine: Woah, neat! We can see that when we move from into , the compiler updates the types of the variables around it. So actually the type of changes to . And then when we move from to , that’s totally valid. In PL land, updating the type of a variable from one thing to another is called a “strong update”. Obviously things can get a bit complicated when control-flow is involved, e.g., in a situation like this: OK, let’s take the next step. Let’s define a Dada function that takes an owned value and another value borrowed from it, like the name, and then call it: We could call this function like so, as you might expect: So…how does this work? Internally, the type checker type-checks a function call by creating a simpler snippet of code, essentially, and then type-checking that . It’s like desugaring but only at type-check time. In this simpler snippet, there are a series of statements to create temporary variables for each argument. These temporaries always have an explicit type taken from the method signature, and they are initialized with the values of each argument: If this type checks, then the type checker knows you have supplied values of the required types, and so this is a valid call. Of course there are a few more steps, but that’s the basic idea. Notice what happens if you supply data borrowed from the wrong place: This will fail to type check because you get: So now, if we go all the way back to our original example, we can see how the example worked: Basically, when you construct a , that’s “just another function call” from the type system’s perspective, except that in the signature is handled carefully. I should be clear, this system is modeled in the dada-model repository, which implements a kind of “mini Dada” that captures what I believe to be the most interesting bits. I’m working on fleshing out that model a bit more, but it’s got most of what I showed you here. 5 For example, here is a test that you get an error when you give a reference to the wrong value. The “real implementation” is lagging quite a bit, and doesn’t really handle the interesting bits yet. Scaling it up from model to real implementation involves solving type inference and some other thorny challenges, and I haven’t gotten there yet – though I have some pretty interesting experiments going on there too, in terms of the compiler architecture. 6 I believe we could apply most of this system to Rust. Obviously we’d have to rework the borrow checker to be based on places, but that’s the straight-forward part. The harder bit is the fact that is a pointer in Rust, and that we cannot readily change. However, for many use cases of self-references, this isn’t as important as it sounds. Often, the data you wish to reference is living in the heap, and so the pointer isn’t actually invalidated when the original value is moved. Consider our opening example. You might imagine Rust allowing something like this in Rust: In this case, the data is heap-allocated, so moving the string doesn’t actually invalidate the value (it would invalidate an value, interestingly). In Rust today, the compiler doesn’t know all the details of what’s going on. has a impl and so it’s quite opaque whether is heap-allocated or not. But we are working on various changes to this system in the Beyond the goal, most notably the Field Projections work. There is likely some opportunity to address this in that context, though to be honest I’m behind in catching up on the details. I’ll note in passing that Dada unifies and into one type as well. I’ll talk in detail about how that works in a future blog post.  ↩︎ This is kind of like C++ references (e.g., ), which also act “as if” they were a value (i.e., you write , not ), but a C++ reference is truly a pointer, unlike a Dada ref.  ↩︎ This goal was in part inspired by a conversation I had early on within Amazon, where a (quite experienced) developer told me, “It took me months to understand what variables are in Rust”.  ↩︎ I explained this some years back in a talk on Polonius at Rust Belt Rust , if you’d like more detail.  ↩︎ No closures or iterator chains!  ↩︎ As a teaser, I’m building it in async Rust, where each inference variable is a “future” and use “await” to find out when other parts of the code might have added constraints.  ↩︎ References are pointers, so those pointers may become invalidated. In the example above, points to the stack slot for , so if were to be moved into , that makes the reference invalid. The type system would lose track of things. Internally, the Rust borrow checker has a kind of “indirection”. It knows that is borrowed for some span of the code (a “lifetime”), and it knows that the lifetime in the type of is related to that lifetime, but it doesn’t really know that is borrowed from in particular. 4 Because references are not pointers into the stack, but rather shallow copies, moving the borrowed value doesn’t invalidate their contents. They remain valid. Because Dada’s types reference actual variable names, we can modify them to reflect moves. I’ll note in passing that Dada unifies and into one type as well. I’ll talk in detail about how that works in a future blog post.  ↩︎ This is kind of like C++ references (e.g., ), which also act “as if” they were a value (i.e., you write , not ), but a C++ reference is truly a pointer, unlike a Dada ref.  ↩︎ This goal was in part inspired by a conversation I had early on within Amazon, where a (quite experienced) developer told me, “It took me months to understand what variables are in Rust”.  ↩︎ I explained this some years back in a talk on Polonius at Rust Belt Rust , if you’d like more detail.  ↩︎ No closures or iterator chains!  ↩︎ As a teaser, I’m building it in async Rust, where each inference variable is a “future” and use “await” to find out when other parts of the code might have added constraints.  ↩︎

0 views
fLaMEd fury 3 days ago

Making fLaMEd fury Glow Everywhere With an Eleventy Transform

What’s going on, Internet? I originally added a CSS class as a fun way to make my name (fLaMEd) and site name (fLaMEd fury) pop on the homepage. shellsharks gave me a shoutout for it, which inspired me to take it further and apply the effect site-wide. Site wide was the original intent and the problem was it was only being applied manually in a handful of places, and I kept forgetting to add it whenever I wrote a new post or created a new page. Classic. Instead of hunting through templates and markdown files, I’ve added an Eleventy HTML transform that automatically applies the glow up. I had Claude Code help me figure out the regex and the transform config. This allowed me to get this done before the kids came home. Don't @ me. The effect itself is a simple utility class using : Swap for whatever gradient custom property you have defined. The repeats the gradient across the text for a more dynamic flame effect. The transform lives in its own plugin file and gets registered in . It runs after Eleventy has rendered each page, tokenises the HTML by splitting on tags, tracks a skip-tag stack, and only replaces text in text nodes. Tags in the set, along with any span already carrying the class, push onto the stack. No replacement happens while the stack is non-empty, so link text, code examples, the page , and already-wrapped instances are all left alone. HTML attributes like and are never touched because they sit inside tag tokens, not text nodes. A single regex handles everything in one pass. The optional group matches " fury" (with space) or “fury” (without), so “flamed fury” and “flamedfury” (as it appears in the domain name) are both wrapped as a unit. The flag covers every capitalisation variant (“fLaMEd fury”, “Flamed Fury”, “FLAMED FURY”) with the original casing preserved in the output. This helps because I can be inconsistent with the styling at times. Export the plugin from wherever you manage your Eleventy plugins: Then register it in . Register it before any HTML prettify transform so the spans are in place before reformatting runs: That’s it. Any mention of the site name (fLaMEd fury) in body text gets the gradient automatically, in posts, templates, data-driven content, wherever. Look out for the easter egg I’ve dropped in. Later. Hey, thanks for reading this post in your feed reader! Want to chat? Reply by email or add me on XMPP , or send a webmention . Check out the posts archive on the website.

0 views
Brain Baking 3 days ago

Managing Multiple Development Ecosystem Installs

In the past year, I occasionally required another Java Development Kit besides the usual one defined in to build certain modules against older versions and certain modules against bleeding edge versions. In the Java world, that’s rather trivial thanks to IntelliJ’s project settings: you can just interactively click through a few panels to install another JDK flavour and get on with your life. The problem starts once you close IntelliJ and want to do some command line work. Luckily, SDKMan , the “The Software Development Kit Manager”, has got you covered. Want to temporarily change the Java compiler for the current session? . Want to change the default? . Easy! will point to , a symlink that gets rewired by SDKMan. A Java project still needs a dependency management system such as Gradle, but you don’t need to install a global specific Gradle version. Instead, just points to the jar living at . Want another one? Change the version number in and it’ll be auto-downloaded. Using Maven instead? Tough luck! Just kidding: don’t use but , the Maven Wrapper that works exactly the same. .NET comes with built-in support to change the toolchain (and specify the runtime target), more or less equal to a typical Gradle project. Actually, the command can both build list its own installed toolchains: . Yet installing a new one is done by hand. You switch toolchains by specifying the SDK version in a global.json file and tell the compiler to target a runtime in the file. In Python , the concept of virtual environments should solve that problem: each project creates its own that points to a specific version of Python. Yet I never really enjoyed working with this system: you’ve got , , , , , … That confusing mess is solved with a relatively new kid in town: uv , “An extremely fast Python package and project manager, written in Rust.” It’s more than as it also manages your multiple development ecosystems. Want to install a new Python distribution? . Want to temporarily change the Python binary for the current session? . Creating a new project with will also create a virtual environment, meaning you don’t run your stuff with but with that auto-selects the correct version. Lovely! What about JS/TS and Node ? Of course there the options are many: there’s nvm —but that’s been semi-abandoned ?—and of course someone built a Rust-alternative called fnm , but you can also manage Node versions with . I personally don’t care and use instead, which is aimed at not managing but replacing the Node JS runtime. But who will manage the bun versions? PHP is more troublesome because it’s tied to a web server. Solutions such as Laravel Nerd combine both PHP and web server dependency management into a sleek looking tool that’s “free”. Of course you can let your OS-system package manager manage your SDK packages: and then . That definitely feels a bit more hacky. For PHP, I’d even consider Mise. Speaking of which… Why use a tool that limits the scope to one specific development environment? If you’re a full-stack developer you’ll still need to know how to manage both your backend and frontend dev environment. That’s not needed with Mise-en-place , a tool that manages all these things . Asdf is another popular one that manages any development environment that doesn’t have its own dedicated tool. I personally think that’s an extraction layer too far. You’ll still need to dissect these tools separately in case things go wrong. Some ecosystems come with built-in multi-toolkit support, such as Go : simply installs into your directory 1 . That means you’ve installed the compiler (!) in exactly the same way as any other (global) dependency, how cool is that? The downside of this is that you’ll have to remember to type instead of so there’s no symlink rewiring involved. or can do that—or the above Mise. But wait, I hear you think, why not just use containers to isolate everything? Spinning up containers to build in an isolated environment: sure, that’s standard practice in continuous integration servers, but locally? Really? Really. Since the inception of Dev Containers by Microsoft, specifically designed for VS Code, working “inside” a container is as easy as opening up the project and “jumping inside the container”. From that moment on, your terminal, IntelliSense, … runs inside that container. That means you won’t have to wrestle Node/PHP versions on your local machine, and you can even use the same container to build your stuff on the CI server. That also means your newly onboarded juniors don’t need to wrestle through a week of “installing stuff”. Microsoft open sourced the Dev Container specification and the JetBrains folks jumped the gun: it has support for but I have yet to try it out. Of course the purpose was to integrate this into GitHub: their cloud-based IDE Codespaces makes heavy use of the idea—and yes, there’s an open-source alternative . Is there Emacs support for Dev Containers? Well, Tramp allows you to remotely open and edit any file, also inside a container . So just install the Dev Container CLI, run it and point Emacs to a source file inside it. From then on, everything Emacs does—including the LSP server, compilation, …—happens inside that container. That means you’ll also have to install your LSP binaries in there. devcontainer.el just wraps complication commands to execute inside the container whilst still letting you edit everything locally in case you prefer a hybrid approach. And then there’s Nix and devenv . Whatever that does, it goes way over my head! You’ll still have to execute after that.  ↩︎ Related topics: / containers / By Wouter Groeneveld on 26 February 2026.  Reply via email . You’ll still have to execute after that.  ↩︎

0 views
Thomasorus 5 days ago

Is frontend development a dead-end career?

It's been almost a year at my current job. I was hired as a frontend developer and UI/UX coordinator, but I've been slowly shifting to a project manager role, which I enjoy a lot and where I think I contribute more. We build enterprise web apps, the kinds you will never see online, never hear about, but that power entire companies. The config application for the car assembly line, the bank counsellor website that outputs your mortgage rate, the warehouse inventory systems... That's the kind of thing we do. For backend engineers that's a very interesting job. You get to understand how entire professional fields work, and try to reproduce them in code. But for us the frontend guys? The interest is, limited to put it simply. We're talking about pages with multi-step conditional forms, tables with lots of columns and filters, 20 nuances of inputs, modals... The real challenge isn't building an innovative UI or UX, it's maintaining a consistency in project that can last years and go into the hands of multiple developers. Hence my UI/UX coordinator role where I look at my colleagues work and sternly say "That margin should be , not " . Because here's the thing: this type of client doesn't care if it's pretty or not and won't pay for design system work or maintenance. To them, stock Bootstrap or Material Design is amazingly beautiful compared to their current Windev application. What they want is stability and predictability, they care it works the same when they encounter the same interface. Sometimes, if a process is too complex for new hires, they will engage into talks to make it more user friendly, but that's it. Until recently, the only generated code we used were the types for TypeScript and API calls functions generated from the backend, which saved us a lot of repetitive work. We made experiments with generative AI and found out we could generate a lot of our template code. All that's left to do is connect both, the front of the frontend and the back of the frontend , mostly click events, stores, reactivity, and so on. People will say that's where the fun is, and sometimes yes, I agree. I've been on projects where building the state was basically building a complex state machine out of dozens of calls from vendor specific APIs. But how often do you do that? And why would you do that if you are developing the backend yourself and can pop an endpoint with all the data your frontend needs? And so I've been wondering about the future. With frameworks, component libraries, LLMs, the recession pushing to deliver fast even if mediocre code and features, who needs someone who can write HTML, CSS, JS? Who can pay for the craft of web development? Are the common frontend developers folks, not the already installed elite freelancers building websites for prestigious clients , only destined to do assembly line of components by prompting LLMs before putting some glue between code blocks they didn't write?

0 views
Jim Nielsen 6 days ago

Making Icon Sets Easy With Web Origami

Over the years, I’ve used different icon sets on my blog. Right now I use Heroicons . The recommended way to use them is to copy/paste the source from the website directly into your HTML. It’s a pretty straightforward process: If you’re using React or Vue, there are also npm packages you can install so you can import the icons as components. But I’m not using either of those frameworks, so I need the raw SVGs and there’s no for those so I have to manually grab the ones I want. In the past, my approach has been to copy the SVGs into individual files in my project, like: Then I have a “component” for reading those icons from disk which I use in my template files to inline the SVGs in my HTML. For example: It’s fine. It works. It’s a lot of node boilerplate to read files from disk. But changing icons is a bit of a pain. I have to find new SVGs, overwrite my existing ones, re-commit them to source control, etc. I suppose it would be nice if I could just and get the raw SVGs installed into my folder and then I could read those. But that has its own set of trade-offs. For example: So the project’s npm packages don’t provide the raw SVGs. The website does, but I want a more programatic way to easily grab the icons I want. How can I do this? I’m using Web Origami for my blog which makes it easy to map icons I use in my templates to Heroicons hosted on Github. It doesn’t require an or a . Here’s an snippet of my file: As you can see, I name my icon (e.g. ) and then I point it to the SVG as hosted on Github via the Heroicons repo. Origami takes care of fetching the icons over the network and caching them in-memory. Beautiful, isn’t it? It kind of reminds me of import maps where you can map a bare module specifier to a URL (and Deno’s semi-abandoned HTTP imports which were beautiful in their own right). Origami makes file paths first-class citizens of the language — even “remote” file paths — so it’s very simple to create a single file that maps your icon names in a codebase to someone else’s icon names from a set, whether those are being installed on disk via npm or fetched over the internet. To simplify my example earlier, I can have a file like : Then I can reference those icons in my templates like this: Easy-peasy! And when I want to change icons, I simply update the entries in to point somewhere else — at a remote or local path. And if you really want to go the extra mile, you can use Origami’s caching feature: Rather than just caching the files in memory, this will cache them to a local folder like this: Which is really cool because now when I run my site locally I have a folder of SVG files cached locally that I can look at and explore (useful for debugging, etc.) This makes vendoring really easy if I want to put these in my project under source control. Just run the file once and boom, they’re on disk! There’s something really appealing to me about this. I think it’s because it feels very “webby” — akin to the same reasons I liked HTTP imports in Deno. You declare your dependencies with URLs, then they’re fetched over the network and become available to the rest of your code. No package manager middleman introducing extra complexity like versioning, transitive dependencies, install bloat, etc. What’s cool about Origami is that handling icons like this isn’t a “feature” of the language. It’s an outcome of the expressiveness of the language. In some frameworks, this kind of problem would require a special feature (that’s why you have special npm packages for implementations of Heroicons in frameworks like react and vue). But because of the way Origami is crafted as a tool, it sort of pushes you towards crafting solutions in the same manner as you would with web-based technologies (HTML/CSS/JS). It helps you speak “web platform” rather than some other abstraction on top of it. I like that. Reply via: Email · Mastodon · Bluesky Go to the website Search for the icon you want Click to “Copy SVG” Go back to your IDE and paste it Names are different between icon packs, so when you switch, names don’t match. For example, an icon might be named in one pack and in another. So changing sets requires going through all your templates and updating references. Icon packs are often quite large and you only need a subset. might install hundreds or even thousands of icons I don’t need.

0 views
Dominik Weber 6 days ago

Lighthouse update February 23rd

During the past week a couple of nice improvements happened. **Finally implemented a 2 week trial without requiring a credit card** Every user now gets the trial by default. This is a nice improvement because, from what I can observe, in B2C most people want to test the product before entering their credit card. It was also a good step to a better first product experience. **Finished the website to feed feature** The last remaining task was automated finding of items. When you enter a website, it automatically checks it and tries to find relevant items. If items are found, they are highlighted and the selectors added, without users having to do anything. **Updated blogroll editor** This is a small free tool on the Lighthouse website. It's for creating collections of feeds, websites, and newsletters. For a long time I wanted to create collections for specific areas, for example company engineering blogs, AI labs, JavaScript ecosystem, and so on. The reworked blogroll editor makes that much simpler to do. ## Next steps An issue that became important is feed URLs being behind bot protection. It doesn't really make sense to be configured that way, because feed URLs are designed to be accessed by bots, but in some cases it may be difficult to configure properly. This affects only for a small number of feeds, but it's enough to be noticable. It prevents people from moving to Lighthouse from other services. Consequently, one of the next tasks is to fix this. Besides that, the first user experience continues to be an ongoing area of improvement. I have a couple of ideas on how to make it better, and will continuously work on it.

0 views
Simon Willison 1 weeks ago

Adding TILs, releases, museums, tools and research to my blog

I've been wanting to add indications of my various other online activities to my blog for a while now. I just turned on a new feature I'm calling "beats" (after story beats, naming this was hard!) which adds five new types of content to my site, all corresponding to activity elsewhere. Here's what beats look like: Those three are from the 30th December 2025 archive page. Beats are little inline links with badges that fit into different content timeline views around my site, including the homepage, search and archive pages. There are currently five types of beats: That's five different custom integrations to pull in all of that data. The good news is that this kind of integration project is the kind of thing that coding agents really excel at. I knocked most of the feature out in a single morning while working in parallel on various other things. I didn't have a useful structured feed of my Research projects, and it didn't matter because I gave Claude Code a link to the raw Markdown README that lists them all and it spun up a parser regex . Since I'm responsible for both the source and the destination I'm fine with a brittle solution that would be too risky against a source that I don't control myself. Claude also handled all of the potentially tedious UI integration work with my site, making sure the new content worked on all of my different page types and was handled correctly by my faceted search engine . I actually prototyped the initial concept for beats in regular Claude - not Claude Code - taking advantage of the fact that it can clone public repos from GitHub these days. I started with: And then later in the brainstorming session said: After some iteration we got to this artifact mockup , which was enough to convince me that the concept had legs and was worth handing over to full Claude Code for web to implement. If you want to see how the rest of the build played out the most interesting PRs are Beats #592 which implemented the core feature and Add Museums Beat importer #595 which added the Museums content type. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Releases are GitHub releases of my many different open source projects, imported from this JSON file that was constructed by GitHub Actions . TILs are the posts from my TIL blog , imported using a SQL query over JSON and HTTP against the Datasette instance powering that site. Museums are new posts on my niche-museums.com blog, imported from this custom JSON feed . Tools are HTML and JavaScript tools I've vibe-coded on my tools.simonwillison.net site, as described in Useful patterns for building HTML tools . Research is for AI-generated research projects, hosted in my simonw/research repo and described in Code research projects with async coding agents like Claude Code and Codex .

0 views
David Bushell 1 weeks ago

Everything you never wanted to know about visually-hidden

Nobody asked for it but nevertheless, I present to you my definitive “it depends” tome on visually-hidden web content. I’ll probably make an amendment before you’ve finished reading. If you enjoy more questions than answers, buckle up! I’ll start with the original premise, even though I stray off-topic on tangents and never recover. I was nerd-sniped on Bluesky. Ana Tudor asked : Is there still any point to most styles in visually hidden classes in ’26? Any point to shrinking dimensions to and setting when to nothing via / reduces clickable area to nothing? And then no dimensions = no need for . @anatudor.bsky.social Ana proposed the following: Is this enough in 2026? As an occasional purveyor of the class myself, the question wriggled its way into my brain. I felt compelled to investigate the whole ordeal. Spoiler: I do not have a satisfactory yes-or-no answer, but I do have a wall of text! I went so deep down the rabbit hole I must start with a table of contents: I’m writing this based on the assumption that a class is considered acceptable for specific use cases . My final section on native visually-hidden addresses the bigger accessibility concerns. It’s not easy to say where this technique is appropriate. It is generally agreed to be OK but a symptom of — and not a fix for — other design issues. Appropriate use cases for are far fewer than you think. Skip to the history lesson if you’re familiar. , — there have been many variations on the class name. I’ve looked at popular implementations and compiled the kitchen sink version below. Please don’t copy this as a golden sample. It merely encompasses all I’ve seen. There are variations on the selector using pseudo-classes that allow for focus. Think “skip to main content” links, for example. What is the purpose of the class? The idea is to hide an element visually, but allow it to be discovered by assistive technology. Screen readers being the primary example. The element must be removed from layout flow. It should leave no render artefacts and have no side effects. It does this whilst trying to avoid the bugs and quirks of web browsers. If this sounds and looks just a bit hacky to you, you have a high tolerance for hacks! It’s a massive hack! How was this normalised? We’ll find out later. I’ll whittle down the properties for those unfamiliar. Absolute positioning is vital to remove the element from layout flow. Otherwise the position of surrounding elements will be affected by its presence. This crops the visible area to nothing. remains as a fallback but has long been deprecated and is obsolete. All modern browsers support . These two properties remove styles that may add layout dimensions. This group effectively gives the element zero dimensions. There are reasons for instead of and negative margin that I’ll cover later. Another property to ensure no visible pixels are drawn. I’ve seen the newer value used but what difference that makes if any is unclear. This was added to address text wrapping inside the square (I’ll explain later). So basically we have and a load of properties that attempted to make the element invisible. We cannot use or or because those remove elements from the accessibility tree. So the big question remains: why must we still ‘zero’ the dimensions? Why is not sufficient? To make sense of this mystery I went back to the beginning. It was tricky to research this topic because older articles have been corrected with modern information. I recovered many details from the archives and mailing lists with the help of those involved. They’re cited along the way. Our journey begins November 2004. A draft document titled “CSS Techniques for WCAG 2.0” edited by Wendy Chisholm and Becky Gibson includes a technique for invisible labels. While it is usually best to include visual labels for all form controls, there are situations where a visual label is not needed due to the surrounding textual description of the control and/or the content the control contains. Users of screen readers, however, need each form control to be explicitly labeled so the intent of the control is well understood when navigated to directly. Creating Invisible labels for form elements ( history ) The following CSS was provided: Could this be the original class? My research jumped through decades but eventually I found an email thread “CSS and invisible labels for forms” on the W3C WAI mailing list. This was a month prior, preluding the WCAG draft. A different technique from Bob Easton was noted: The beauty of this technique is that it enables using as much text as we feel appropriate, and the elements we feel appropriate. Imagine placing instructive text about the accessibility features of the page off left (as well as on the site’s accessibility statement). Imagine interspersing “start of…” landmarks through a page with heading tags. Or, imagine parking full lists off left, lists of access keys, for example. Screen readers can easily collect all headings and read complete lists. Now, we have a made for screen reader technique that really works! Screenreader Visibility - Bob Easton (2003) Easton attributed both Choan Gálvez and Dave Shea for their contributions. In same the thread, Gez Lemon proposed to ensure that text doesn’t bleed into the display area . Following up, Becky Gibson shared a test case covering the ideas. Lemon later published an article “Invisible Form Prompts” about the WCAG plans which attracted plenty of commenters including Bob Easton. The resulting WCAG draft guideline discussed both the and ideas. Note that instead of using the nosize style described above, you could instead use postion:absolute; and left:-200px; to position the label “offscreen”. This technique works with the screen readers as well. Only position elements offscreen in the top or left direction, if you put an item off to the right or the bottom, many browsers will add scroll bars to allow the user to reach the content. Creating Invisible labels for form elements Two options were known and considered towards the end of 2004. Why not both? Indeed, it appears Paul Bohman on the WebAIM mailing list suggested such a combination in February 2004. Bohman even discovered possibly the first zero width bug. I originally recommended setting the height and width to 0 pixels. This works with JAWS and Home Page Reader. However, this does not work with Window Eyes. If you set the height and width to 1 pixel, then the technique works with all browsers and all three of the screen readers I tested. Re: Hiding text using CSS - Paul Bohman Later in May 2004, Bohman along with Shane Anderson published a paper on this technique. Citations within included Bob Easton and Tom Gilder . Aside note: other zero width bugs have been discovered since. Manuel Matuzović noted in 2023 that links in Safari were not focusable . The zero width story continues as recently as February 2026 (last week). In browse mode in web browsers, NVDA no longer treats controls with 0 width or height as invisible. This may make it possible to access previously inaccessible “screen reader only” content on some websites. NVDA 2026.1 Beta TWO now available - NV Access News Digger further into WebAIM’s email archive uncovered a 2003 thread in which Tom Gilder shared a class for skip navigation links . I found Gilder’s blog in the web archives introducing this technique. I thought I’d put down my “skip navigation” link method down in proper writing as people seem to like it (and it gives me something to write about!). Try moving through the links on this page using the keyboard - the first link should magically appear from thin air and allow you to quickly jump to the blog tools, which modern/visual/graphical/CSS-enabled browsers (someone really needs to come up with an acronym for that) should display to the left of the content. Skip-a-dee-doo-dah - Tom Gilder Gilder’s post links to a Dave Shea post which in turn mentions the 2002 book “Building Accessible Websites” by Joe Clark . Chapter eight discusses the necessity of a “skip navigation” link due to table-based layout but advises: Keep them visible! Well-intentioned developers who already use page anchors to skip navigation will go to the trouble to set the anchor text in the tiniest possible font in the same colour as the background, rendering it invisible to graphical browsers (unless you happen to pass the mouse over it and notice the cursor shape change). Building Accessible Websites - 08. Navigation - Joe Clark Clark expressed frustration over common tricks like the invisible pixel. It’s clear no class existed when this was written. Choan Gálvez informed me that Eric Meyer would have the css-discuss mailing list. Eric kindly searched the backups but didn’t find any earlier discussion. However, Eric did find a thread on the W3C mailing list from 1999 in which Ian Jacobs (IBM) discusses the accessibility of “skip navigation” links. The desire to visually hide “skip navigation” links was likely the main precursor to the early techniques. In fact, Bob Easton said as much: As we move from tag soup to CSS governed design, we throw out the layout tables and we throw out the spacer images. Great! It feels wonderful to do that kind of house cleaning. So, what do we do with those “skip navigation” links that used to be attached to the invisible spacer images? Screenreader Visibility - Bob Easton (2003) I had originally missed that in my excitement seeing the class. I reckon we’ve reached the source of the class. At least conceptually. Technically, the class emerged from several ideas, rather than a “eureka” moment. Perhaps more can be gleaned from other CSS techniques such a the desire to improve accessibility of CSS image replacement . Bob Easton retired in 2008 after a 40 year career at IBM. I reached out to Bob who was surprised to learn this technique was still a topic today † . Bob emphasised the fact that it was always a clumsy workaround and something CSS probably wasn’t intended to accommodate . I’ll share more of Bob’s thoughts later. † I might have overdone the enthusiasm Let’s take an intermission! My contact page is where you can send corrections by the way :) The class stabilised for a period. Visit 2006 in the Wayback Machine to see WebAIM’s guide to invisible content — Paul Bohman’s version is still recommended. Moving forward to 2011, I found Jonathan Snook discussing the “clip method”. Snook leads us to Drupal developer Jeff Burnz the previous year. […] we still have the big problem of the page “jump” issue if this is applied to a focusable element, such as a link, like skip navigation links. WebAim and a few others endorse using the LEFT property instead of TOP, but this no go for Drupal because of major pain-in-the-butt issues with RTL. In early May 2010 I was getting pretty frustrated with this issue so I pulled out a big HTML reference and started scanning through it for any, and I mean ANY property I might have overlooked that could possible be used to solve this thorny issue. It was then I recalled using clip on a recent project so I looked up its values and yes, it can have 0 as a value. Using CSS clip as an Accessible Method of Hiding Content - Jeff Burnz It would seem Burnz discovered the technique independently and was probably the first to write about it. Burnz also notes a right-to-left (RTL) issue. This could explain why pushing content off-screen fell out of fashion. 2010 also saw the arrival of HTML5 Boilerplate along with issue #194 in which Jonathan Neal plays a key role in the discussion and comments: If we want to correct for every seemingly-reasonable possibility of overflow in every browser then we may want to consider [code below] This was their final decision. I’ve removed for clarity. This is very close to what we have now, no surprise since HTML5 Boilterplate was extremely popular. I’m leaning to conclude that the additional properties are really just there for the “possibility” of pixels escaping containment as much as fixing any identified problem. Thierry Koblentz covered the state of affairs in 2012 noting that: Webkit, Opera and to some extent IE do not play ball with [clip] . Koblentz prophesies: I wrote the declarations in the previous rule in a particular order because if one day clip works as everyone would expect, then we could drop all declarations after clip, and go back to the original Clip your hidden content for better accessibility - Thierry Koblentz Sound familiar? With those browsers obsolete, and if behaves itself, can the other properties be removed? Well we have 14 years of new bugs features to consider first. In 2016, J. Renée Beach published: Beware smushed off-screen accessible text . This appears to be the origin of (as demonstrated by Vispero .) Over a few sessions, Matt mentioned that the string of text “Show more reactions” was being smushed together and read as “Showmorereactions”. Beach’s class did not include the kitchen sink. The addition of became standard alongside everything else. Aside note: the origin of remains elusive. One Bootstrap issue shows it was rediscovered in 2018 to fix a browser bug. However, another HTML5 Boilterplate issue dated 2017 suggests negative margin broke reading order. Josh Comeau shared a React component in 2024 without margin. One of many examples showing that it has come in and out of fashion. We started with WCAG so let’s end there. The latest WCAG technique for “Using CSS to hide a portion of the link text” provides the following code. Circa 2020 the property was added as browser support increased and became deprecated. An obvious change I not sure warrants investigation (although someone had to be first!) That brings us back to what we have today. Are you still with me? As we’ve seen, many of the properties were thrown in for good measure. They exist to ensure absolutely no pixels are painted. They were adapted over the years to avoid various bugs, quirks, and edge cases. How many such decisions are now irrelevant? This is a classic Chesterton’s Fence scenario. Do not remove a fence until you know why it was put up in the first place. Well we kinda know why but the specifics are practically folklore at this point. Despite all that research, can we say for sure if any “why” is still relevant? Back to Ana Tudor’s suggestion. How do we know for sure? The only way is extensive testing. Unfortunately, I have neither the time nor skill to perform that adequately here. There is at least one concern with the code above, Curtis Wilcox noted that in Safari the focus ring behaves differently. Other minimum viable ideas have been presented before. Scott O’Hara proposed a different two-liner using . JAWS, Narrator, NVDA with Edge all seem to behave just fine. As do Firefox with JAWS and NVDA, and Safari on macOS with VoiceOver. Seems also fine with iOS VO+Safari and Android TalkBack with Firefox or Chrome. In none of these cases do we get the odd focus rings that have occurred with other visually hidden styles, as the content is scaled down to zero. Also because not hacked into a 1px by 1px box, there’s no text wrapping occurring, so no need to fix that issue. transform scale(0) to visually hide content - Scott O’Hara Sounds promising! It turns out Katrin Kampfrath had explored both minimum viable classes a couple of years ago, testing them against the traditional class. I am missing the experience and moreover actual user feedback, however, i prefer the screen reader read cursor to stay roughly in the document flow. There are screen reader users who can see. I suppose, a jumping read cursor is a bit like a shifting layout. Exploring the visually-hidden css - Katrin Kampfrath Kampfrath’s limited testing found the read cursor size differs for each class. The technique was favoured but caution is given. A few more years ago, Kitty Giraudel tested several ideas concluding that was still the most accessible for specific text use. This technique should only be used to mask text. In other words, there shouldn’t be any focusable element inside the hidden element. This could lead to annoying behaviours, like scrolling to an invisible element. Hiding content responsibly - Kitty Giraudel Zell Liew proposed a different idea in 2019. Many developers voiced their opinions, concerns, and experiments over at Twitter. I wanted to share with you what I consolidated and learned. A new (and easy) way to hide content accessibly - Zell Liew Liew’s idea was unfortunately torn asunder. Although there are cases like inclusively hiding checkboxes where near-zero opacity is more accessible. I’ve started to go back in time again! I’m also starting to question whether this class is a good idea. Unless we are capable and prepared to thoroughly test across every combination of browser and assistive technology — and keep that information updated — it’s impossible to recommend anything. This is impossible for developers! Why can’t browser vendors solve this natively? Once you’ve written 3000 words on a twenty year old CSS hack you start to question why it hasn’t been baked into web standards by now. Ben Myers wrote “The Web Needs a Native .visually-hidden” proposing ideas from HTML attributes to CSS properties. Scott O’Hara responded noting larger accessibility issues that are not so easily handled. O’Hara concludes: Introducing a native mechanism to save developers the trouble of having to use a wildly available CSS ruleset doesn’t solve any of those underlying issues. It just further pushes them under the rug. Visually hidden content is a hack that needs to be resolved, not enshrined - Scott O’Hara Sara Soueidan had floated the topic to the CSS working group back in 2016. Soueidan closed the issue in 2025, coming to a similar conclusion. I’ve been teaching accessibility for a little less than a decade now and if there’s one thing I learned is that developers will resort to using utility to do things that are more often than not just bad design decisions. Yes, there are valid and important use cases. But I agree with all of @scottaohara’s points, and most importantly I agree that we need to fix the underlying issues instead of standardizing a technique that is guaranteed to be overused and misused even more once it gets easier to use. csswg-drafts comment - Sara Soueidan Adrian Roselli has a blog post listing priorities for assigning an accessible name to a control. Like O’Hara and Soueidan, Roselli recognises there is no silver bullet. Hidden text is also used too casually to provide information for just screen reader users, creating overly-verbose content . For sighted screen reader users , it can be a frustrating experience to not be able to find what the screen reader is speaking, potentially causing the user to get lost on the page while visually hunting for it. My Priority of Methods for Labeling a Control - Adrian Roselli In short, many believe that a native visually-hidden would do more harm than good. The use-cases are far more nuanced and context sensitive than developers realise. It’s often a half-fix for a problem that can be avoided with better design. I’m torn on whether I agree that it’s ultimately a bad idea. A native version would give software an opportunity to understand the developer’s intent and define how “visually hidden” works in practice. It would be a pragmatic addition. The technique has persisted for over two decades and is still mentioned by WCAG. Yet it remains hacks upon hacks! How has it survived for so long? Is that a failure of developers, or a failure of the web platform? The web is overrun with inaccessible div soup . That is inexcusable. For the rest of us who care about accessibility — who try our best — I can’t help but feel the web platform has let us down. We shouldn’t be perilously navigating code hacks, conflicting advice, and half-supported standards. We need more energy money dedicated to accessibility. Not all problems can be solved with money. But what of the thousands of unpaid hours, whether volunteered or solicited, from those seeking to improve the web? I risk spiralling into a rant about browser vendors’ financial incentives, so let’s wrap up! I’ll end by quoting Bob Easton from our email conversation: From my early days in web development, I came to the belief that semantic HTML, combined with faultless keyboard navigation were the essentials for blind users. Experience with screen reader users bears that out. Where they might occasionally get tripped up is due to developers who are more interested in appearance than good structural practices. The use cases for hidden content are very few, such as hidden information about where a search field is, when an appearance-centric developer decided to present a search field with no visual label, just a cute unlabeled image of a magnifying glass. […] The people promoting hidden information are either deficient in using good structural practices, or not experienced with tools used by people they want to help. Bob ended with: You can’t go wrong with well crafted, semantically accurate structure. Ain’t that the truth. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Accessibility notice Class walkthrough Where it all began Further adaptations Minimum viable technique Native visually-hidden Zero dimensions Position off-screen

6 views
Xe Iaso 1 weeks ago

Anubis v1.25.0: Necron

I'm sure you've all been aware that things have been slowing down a little with Anubis development, and I want to apologize for that. A lot has been going on in my life lately (my blog will have a post out on Friday with more information), and as a result I haven't really had the energy to work on Anubis in publicly visible ways. There are things going on behind the scenes, but nothing is really shippable yet, sorry! I've also been feeling some burnout in the wake of perennial waves of anger directed towards me. I'm handling it, I'll be fine, I've just had a lot going on in my life and it's been rough. I've been missing the sense of wanderlust and discovery that comes with the artistic way I playfully develop software. I suspect that some of the stresses I've been through (setting up a complicated surgery in a country whose language you aren't fluent in is kind of an experience) have been sapping my energy. I'd gonna try to mess with things on my break, but realistically I'm probably just gonna be either watching Stargate SG-1 or doing unreasonable amounts of ocean fishing in Final Fantasy 14. Normally I'd love to keep the details about my medical state fairly private, but I'm more of a public figure now than I was this time last year so I don't really get the invisibility I'm used to for this. I've also had a fair amount of negativity directed at me for simply being much more visible than the anonymous threat actors running the scrapers that are ruining everything, which though understandable has not helped. Anyways, it all worked out and I'm about to be in the hospital for a week, so if things go really badly with this release please downgrade to the last version and/or upgrade to the main branch when the fix PR is inevitably merged. I hoped to have time to tame GPG and set up full release automation in the Anubis repo, but that didn't work out this time and that's okay. If I can challenge you all to do something, go out there and try to actually create something new somehow. Combine ideas you've never mixed before. Be creative, be human, make something purely for yourself to scratch an itch that you've always had yet never gotten around to actually mending. At the very least, try to be an example of how you want other people to act, even when you're in a situation where software written by someone else is configured to require a user agent to execute javascript to access a webpage. PS: if you're well-versed in FFXIV lore, the release title should give you an idea of the kind of stuff I've been going through mentally. Full Changelog : https://github.com/TecharoHQ/anubis/compare/v1.24.0...v1.25.0 Add iplist2rule tool that lets admins turn an IP address blocklist into an Anubis ruleset. Add Polish locale ( #1292 ) Fix honeypot and imprint links missing when deployed behind a path prefix ( #1402 ) Add ANEXIA Sponsor logo to docs ( #1409 ) Improve idle performance in memory storage Add HAProxy Configurations to Docs ( #1424 ) build(deps): bump the github-actions group with 4 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1355 feat(localization): add Polish language translation by @btomaev in https://github.com/TecharoHQ/anubis/pull/1363 docs(known-instances): Alphabetical order + Add Valve Corporation by @p0008874 in https://github.com/TecharoHQ/anubis/pull/1352 test: basic nginx smoke test by @Xe in https://github.com/TecharoHQ/anubis/pull/1365 build(deps): bump the github-actions group with 3 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1369 build(deps-dev): bump esbuild from 0.27.1 to 0.27.2 in the npm group by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1368 fix(test): remove interactive flag from nginx smoke test docker run c… by @JasonLovesDoggo in https://github.com/TecharoHQ/anubis/pull/1371 test(nginx): fix tests to work in GHA by @Xe in https://github.com/TecharoHQ/anubis/pull/1372 feat: iplist2rule utility command by @Xe in https://github.com/TecharoHQ/anubis/pull/1373 Update check-spelling metadata by @JasonLovesDoggo in https://github.com/TecharoHQ/anubis/pull/1379 fix: Update SSL Labs IP addresses by @majiayu000 in https://github.com/TecharoHQ/anubis/pull/1377 fix: respect Accept-Language quality factors in language detection by @majiayu000 in https://github.com/TecharoHQ/anubis/pull/1380 build(deps): bump the gomod group across 1 directory with 3 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1370 Revert "build(deps): bump the gomod group across 1 directory with 3 updates" by @JasonLovesDoggo in https://github.com/TecharoHQ/anubis/pull/1386 build(deps): bump preact from 10.28.0 to 10.28.1 in the npm group by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1387 docs: document how to import the default config by @Xe in https://github.com/TecharoHQ/anubis/pull/1392 fix sponsor (Databento) logo size by @ayoung5555 in https://github.com/TecharoHQ/anubis/pull/1395 fix: correct typos by @antonkesy in https://github.com/TecharoHQ/anubis/pull/1398 fix(web): include base prefix in generated URLs by @Xe in https://github.com/TecharoHQ/anubis/pull/1403 docs: clarify botstopper kubernetes instructions by @tarrow in https://github.com/TecharoHQ/anubis/pull/1404 Add IP mapped Perplexity user agents by @tdgroot in https://github.com/TecharoHQ/anubis/pull/1393 build(deps): bump astral-sh/setup-uv from 7.1.6 to 7.2.0 in the github-actions group by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1413 build(deps): bump preact from 10.28.1 to 10.28.2 in the npm group by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1412 chore: add comments back to Challenge struct. by @JasonLovesDoggo in https://github.com/TecharoHQ/anubis/pull/1419 performance: remove significant overhead of decaymap/memory by @brainexe in https://github.com/TecharoHQ/anubis/pull/1420 web: fix spacing/indent by @bjacquin in https://github.com/TecharoHQ/anubis/pull/1423 build(deps): bump the github-actions group with 4 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1425 Improve Dutch translations by @louwers in https://github.com/TecharoHQ/anubis/pull/1446 chore: set up commitlint, husky, and prettier by @Xe in https://github.com/TecharoHQ/anubis/pull/1451 Fix a CI warning: "The set-output command is deprecated" by @kurtmckee in https://github.com/TecharoHQ/anubis/pull/1443 feat(apps): add updown.io policy by @hyperdefined in https://github.com/TecharoHQ/anubis/pull/1444 docs: add AI coding tools policy by @Xe in https://github.com/TecharoHQ/anubis/pull/1454 feat(docs): Add ANEXIA Sponsor logo by @Earl0fPudding in https://github.com/TecharoHQ/anubis/pull/1409 chore: sync logo submissions by @Xe in https://github.com/TecharoHQ/anubis/pull/1455 build(deps): bump the github-actions group across 1 directory with 6 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1453 build(deps): bump the npm group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/TecharoHQ/anubis/pull/1452 feat(docs): Add HAProxy Configurations to Docs by @Earl0fPudding in https://github.com/TecharoHQ/anubis/pull/1424 @majiayu000 made their first contribution in https://github.com/TecharoHQ/anubis/pull/1377 @ayoung5555 made their first contribution in https://github.com/TecharoHQ/anubis/pull/1395 @antonkesy made their first contribution in https://github.com/TecharoHQ/anubis/pull/1398 @tarrow made their first contribution in https://github.com/TecharoHQ/anubis/pull/1404 @tdgroot made their first contribution in https://github.com/TecharoHQ/anubis/pull/1393 @brainexe made their first contribution in https://github.com/TecharoHQ/anubis/pull/1420 @bjacquin made their first contribution in https://github.com/TecharoHQ/anubis/pull/1423 @louwers made their first contribution in https://github.com/TecharoHQ/anubis/pull/1446 @kurtmckee made their first contribution in https://github.com/TecharoHQ/anubis/pull/1443

0 views
David Bushell 1 weeks ago

Web font choice and loading strategy

When I rebuilt my website I took great care to optimise fonts for both performance and aesthetics. Fonts account for around 50% of my website (bytes downloaded on an empty cache). I designed and set a performance budget around my font usage. I use three distinct font families and three different methods to load them. Web fonts are usually defined by the CSS rule. The property allows us some control over how fonts are loaded. The value has become somewhat of a best practice — at least the most common default. The CSS spec says: Gives the font face an extremely small block period (100ms or less is recommended in most cases) and an infinite swap period . In other words, the browser draws the text immediately with a fallback if the font face isn’t loaded, but swaps the font face in as soon as it loads. CSS Fonts Module Level 4 - W3C That small “block period”, if implemented by the browser, renders an invisible font temporarily to minimise FOUC . Personally I default to and don’t change unless there are noticeable or measurable issues. Most of the time you’ll use swap. If you don’t know which option to use, go with swap. It allows you to use custom fonts and tip your hand to accessibility. font-display for the Masses - Jeremy Wagner Google Fonts’ default to which has performance gains. In effect, this makes the font files themselves asynchronous—the browser immediately displays our fallback text before swapping to the web font whenever it arrives. This means we’re not going to leave users looking at any invisible text (FOIT), which makes for both a faster and more pleasant experience. Speed Up Google Fonts - Harry Roberts Harry further notes that a suitable fallback is important, as I’ll discover below. My three fonts in order of importance are: Ahkio for headings. Its soft brush stroke style has a unique hand-drawn quality that remains open and legible. As of writing, I load three Ahkio weights at a combined 150 KB. That is outright greed! Ahkio is core to my brand so it takes priority in my performance budget (and financial budget, for that matter!) Testing revealed the 100ms † block period was not enough to avoid FOUC, despite optimisation techniques like preload . Ahkio’s design is more condensed so any fallback can wrap headings over additional lines. This adds significant layout shift. † Chrome blog mention a zero second block period . Firefox has a config preference default of 100ms. My solution was to use instead of which extends the block period from a recommended 0–100ms up to a much longer 3000ms. Gives the font face a short block period (3s is recommended in most cases) and an infinite swap period . In other words, the browser draws “invisible” text at first if it’s not loaded, but swaps the font face in as soon as it loads. CSS Fonts Module Level 4 - W3C This change was enough to avoid ugly FOUC under most conditions. Worst case scenario is three seconds of invisible headings. With my website’s core web vitals a “slow 4G” network can beat that by half. For my audience an extended block period is an acceptable trade-off. Hosting on an edge CDN with good cache headers helps minimised the cost. Update: Richard Rutter suggested which gives more fallback control than I knew. I shall experiment and report back! Atkinson Hyperlegible Next for body copy. It’s classed as a grotesque sans-serif with interesting quirks such as a serif on the lowercase ‘i’. I chose this font for both its accessible design and technical implementation as a variable font . One file at 78 KB provides both weight and italic variable axes. This allows me to give links a subtle weight boost. For italics I just go full-lean. I currently load Atkinson Hyperlegible with out of habit but I’m strongly considering why I don’t use . Gives the font face an extremely small block period (100ms or less is recommended in most cases) and a short swap period (3s is recommended in most cases). In other words, the font face is rendered with a fallback at first if it’s not loaded, but it’s swapped in as soon as it loads. However, if too much time passes, the fallback will be used for the rest of the page’s lifetime instead. CSS Fonts Module Level 4 - W3C The browser can give up and presumably stop downloading the font. The spec actually says that and “[must/should] only be used for small pieces of text.” Although it notes that most browsers implement the default with similar strategies to . 0xProto for code snippets. If my use of Ahkio was greedy, this is gluttonous! A default would be acceptable. My justification is that controlling presentation of code on a web development site is reasonable. 0xProto is designed for legibility with a personality that compliments my design. I don’t specify 0xProto with the CSS rule. Instead I use the JavaScript font loading API to conditionally load when a element is present. Note the name change because some browsers aren’t happy with a numeric first character. Not shown is the event wrapper around this code. I also load the script with both and attributes. This tells the browser the script is non-critical and avoids render blocking. I could probably defer loading even later without readers noticing the font pop in. Update: for clarity, browsers will conditionally load but JavaScript can purposefully delay the loading further to avoid fighting for bandwidth. When JavaScript is not available the system default is fine. There we have it, three fonts, three strategies, and a few open questions and decisions to make. Those may be answered when CrUX data catches up. My new website is a little chunkier than before but its well within reasonable limits. I’ll monitor performance and keep turning the dials. Web performance is about priorities . In isolation it’s impossible to say exactly how an individual asset should be loaded. There are upper limits, of course. How do you load a one megabyte font? You don’t. Unless you’re a font studio providing a complete type specimen. But even then you could split the font and progressive load different unicode ranges. I wonder if anyone does that? Anyway I’m rambling now, bye. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Stratechery 1 weeks ago

Thin Is In

Listen to this post : There was, in the early days of computing, no debate about thick clients versus thin: When a computer was the size of a room, there were no clients: you scheduled time or submitted jobs, and got back the results when it was your turn. A few years later, however, thin clients in the form of a monitor and keyboard arrived: There is no computer in this image; rather, this is a terminal connected to a mainframe. That’s why it’s called a “thin” client: it’s just an interface, with all of the computing happening elsewhere (i.e. in another room). By the 1980s, however, “thick” clients were the dominant form of computing, in the form of the PC. All of your I/O and compute were packaged together: you typed on a keyboard connected to a PC, which output to the monitor in front of you. A decade later, and Sun Microsystems in particular tried to push the idea of a “network computer”: This was a device that didn’t really have a local operating system; you ran Java applications and Java applets from a browser that were downloaded as they were used from a central server. Sun’s pitch was that network computers would be much cheaper and easier to administer, but PCs were dropping in price so quickly that the value proposition rapidly disappeared, and Windows so dominant that it was already the only platform that network administrators wanted to deal with. Thick clients won, and won decisively. If you wanted to make a case for thin clients, you could argue that mobile devices are a hybrid; after all, the rise of mobile benefited from and drove the rise of the cloud: nearly every app on a phone connects to a server somewhere. Ultimately, however, mobile devices are themselves thick clients: they are very capable computers in their own right, that certainly benefit from being connected to a server, but are useful without it. Critically, the server component is just data: the actual interface is entirely local. You can make the same argument about SaaS apps: on one hand, yes, they operate in the cloud and are usually accessed via a browser; on the other hand, the modern browser is basically an operating system in its own right, and the innovations that made SaaS apps possible were the fact that interactive web apps could be downloaded and run locally. Granted, this isn’t far off from Sun’s vision (although the language ended up being JavaScript, not Java), but you still need a lot of local compute to make these apps work. The thick-versus-thin debate felt, for many years, like a relic; that’s how decisive was the thick client victory. One of the things that is fascinating about AI, however, is that the thin client concept is not just back, it’s dominant. The clearest example of this is the interface that most people use to interact with AI: chat. There is no UI that matters other than a text field and a submit button; when you click that button the text is sent to a data center, where all of the computation happens, and an answer is sent back to you. The quality of the answer or of the experience as a whole is largely independent of the device you are using: it could be a browser on a PC, an app on a high-end smartphone, or the cheapest Android device you can find. The device could be a car, or glasses, or just an earpiece. The local compute that matters is not processing power, but rather connectivity. This interaction paradigm actually looks a lot like the interaction paradigm for mainframe computers: type text into a terminal, send it to the computer, and get a response back. Unlike mainframe terminals, however, the user doesn’t need to know a deterministic set of commands; you just say what you want in plain language and the computer understands. There is no pressure for local compute capability to drive a user interface that makes the computer easier to use, because a more complex user interface would artificially constrain the AI’s capabilities. Nicolas Bustamante, in an X Article about the prospects for vertical software in an AI world , explained why this is threatening: When the interface is a natural language conversation, years of muscle memory become worthless. The switching cost that justified $25K per seat per year dissolves. For many vertical software companies, the interface was most of the value. The underlying data was licensed, public, or semi-commoditized. What justified premium pricing was the workflow built on top of that data. That’s over. Bustamante’s post is about much more than chat interfaces, but I think the user interface point is profound: it’s less that AI user interfaces are different, and more that, for many use cases, they basically don’t exist. This is even clearer when you consider the next big wave of AI: agents. The point of an agent is not to use the computer for you; it’s to accomplish a specific task. Everything between the request and the result, at least in theory, should be invisible to the user. This is the concept of a thin client taken to the absolute extreme: it’s not just that you don’t need any local compute to get an answer from a chatbot; you don’t need any local compute to accomplish real work. The AI on the server does it all. Of course most agentic workflows that work tread a golden path, but stumble with more complex situations or edge cases. That, though, is changing rapidly, as models become better and the capabilities of the chips running them increase, particularly in terms of memory. When it comes to inference, memory isn’t just important for holding the model weights, but also retaining context about the task at hand. To date most of the memory that matters has been high-bandwidth memory attached to the GPUs, but future architectures will offload context to flash storage . At the same time, managing agents is best suited to CPUs , which themselves need large amounts of DRAM. In short, both the amount of compute we have, and the capability of that compute, still isn’t good enough; once it crosses that threshold, though, demand will only get that much stronger. This combination of factors will only accentuate the dominance of the thin client paradigm: Yes, you can run large language models locally, but you are limited in the size of the model, the size of the context window, and speed. Meanwhile, the superior models with superior context windows and faster speeds don’t require a trip to the computer lab; just connect to the Internet from anywhere. Note that this reality applies even to incredible new local tools like OpenClaw: OpenClaw is an orchestration layer that runs locally, but the actual AI inference is, by default and in practice for most users, done by models in the cloud. To put it another way, to be competitive, local inference would need some combination of smaller-yet-sufficiently-capable models, a breakthrough in context management, and critically, lots and lots of memory. It’s that last one that might be the biggest problem of all. From Bloomberg : A growing procession of tech industry leaders including Elon Musk and Tim Cook are warning about a global crisis in the making: A shortage of memory chips is beginning to hammer profits, derail corporate plans and inflate price tags on everything from laptops and smartphones to automobiles and data centers — and the crunch is only going to get worse… Sony Group Corp. is now considering pushing back the debut of its next PlayStation console to 2028 or even 2029, according to people familiar with the company’s thinking. That would be a major upset to a carefully orchestrated strategy to sustain user engagement between hardware generations. Close rival Nintendo Co., which contributed to the surplus demand in 2025 after its new Switch 2 console drove storage card purchases, is also contemplating raising the price of that device in 2026, people familiar with its plans said. Sony and Nintendo representatives didn’t respond to requests for comment. A manager at a laptop maker said Samsung Electronics has recently begun reviewing its memory supply contracts every quarter or so, versus generally on an annual basis. Chinese smartphone makers including Xiaomi Corp., Oppo and Shenzhen Transsion Holdings Co. are trimming shipment targets for 2026, with Oppo cutting its forecast by as much as 20%, Chinese media outlet Jiemian reported. The companies did not respond to requests for comment. The memory shortage has been looming for a while, and is arguably the place where consumers will truly feel the impact of AI; I wrote in January in the context of Nvidia’s keynote at CES: CES stands for “Consumer Electronics Show”, and while Nvidia’s gaming GPUs received some updates, they weren’t a part of [Nvidia CEO Jensen] Huang’s keynote, which was focused on that Vera Rubin AI system and self-driving cars. In other words, there wasn’t really anything for the consumer, despite the location, because AI took center stage. This is fine as far as Nvidia goes: both the Vera Rubin announcement and its new Alpamayo self-driving system are big deals. It is, however, symbolic of the impact AI is having on technology broadly, and that impact is set to impact consumer electronics in a major way. Specifically, not only is all of the energy and investment in the tech sector going towards AI, but so is the supply chain. A big story over the last few months has been the dramatically escalating cost of memory as the major memory manufacturers shift their focus to high-bandwidth memory for AI chips in particular. What that means is that everything else is going to get a lot more expensive: memory is one of the most expensive components in nearly everything tech-related, and given the competitive and commoditized nature of the industry those costs will almost certainly be passed on to the end users. This AI crowd-out dynamic arguably started with the hyperscalers, who diverted ever increasing parts of their budget to GPUs in place of CPU purchases, but now it’s coming for everything from grid power to turbines and now to components, and it’s only going to increase and become more impactful to end users. In other words, Nvidia may not have talked about consumer electronics at the Consumer Electronics Show, but they are having the biggest impact on the industry by far. The downsides of this crowd-out effect are obvious; I pity anyone trying to build their own PC, for example, but soon their pain will be everyone’s pain as prices inevitably rise on everything that needs RAM. At the same time, I think the reported PlayStation delay is telling: apparently the PS5 is “good enough” for Sony to wait for more memory capacity to come online, and they’re probably right! Thick clients — of which consoles like the PS5 are the ultimate example — have long since reached the point of diminishing returns when it comes to hardware improvements. I think you could make the same case for PCs and phones as well: what we already have is already more than sufficient for almost any task we want to do. Moreover, the plateau in thick client capability is happening at the same time that the need for any capability at all is disappearing, thanks to these entirely new AI workflows that happen in the cloud. Yes, it sucks that AI is making memory scarce and personal computers of all kinds — from PCs to phones to consoles — more expensive; it’s also making them less important than ever. Of course thick clients could make a comeback, particularly since local inference is “free” (i.e. the user pays for their own electricity). As I noted above, however, I’m skeptical about local inference in the near term for performance reasons, and the memory crunch is going to make it uneconomical for the foreseeable future. And, by the time local inference is a viable alternative, path dependency downstream of these few years may have already led to many workflows moving to this new paradigm. It will, to be clear, be a transition: UI isn’t just about how to use a computer, it also, as Benedict Evans noted on a recent Interview , embeds critical aspects of how a business works. Open-ended text prompts in particular are a terrible replacement for a well-considered UI button that both prompts the right action and ensures the right thing happens. That’s why it’s the agent space that will be the one to watch: what workflows will transition from UI to AI, and thus from a thick client architecture to a thin one? Current workflows are TBD; future workflows seem inevitable. First, if compute isn’t yet good enough, then workloads will flow to wherever compute is the best, which is going to be in large data centers. Second, if larger models and more context makes for better results, then workloads will flow to wherever there is the most memory available. Third, the expense of furnishing this level of compute means that it will be far more economical to share the cost of that compute amongst millions of users; guaranteeing high utilization and maximizing leverage on your up-front costs.

0 views
Simon Willison 1 weeks ago

Two new Showboat tools: Chartroom and datasette-showboat

I introduced Showboat a week ago - my CLI tool that helps coding agents create Markdown documents that demonstrate the code that they have created. I've been finding new ways to use it on a daily basis, and I've just released two new tools to help get the best out of the Showboat pattern. Chartroom is a CLI charting tool that works well with Showboat, and datasette-showboat lets Showboat's new remote publishing feature incrementally push documents to a Datasette instance. I normally use Showboat in Claude Code for web (see note from this morning ). I've used it in several different projects in the past few days, each of them with a prompt that looks something like this: Here's the resulting document . Just telling Claude Code to run is enough for it to learn how to use the tool - the help text is designed to work as a sort of ad-hoc Skill document. The one catch with this approach is that I can't see the new Showboat document until it's finished. I have to wait for Claude to commit the document plus embedded screenshots and push that to a branch in my GitHub repo - then I can view it through the GitHub interface. For a while I've been thinking it would be neat to have a remote web server of my own which Claude instances can submit updates to while they are working. Then this morning I realized Showboat might be the ideal mechanism to set that up... Showboat v0.6.0 adds a new "remote" feature. It's almost invisible to users of the tool itself, instead being configured by an environment variable. Set a variable like this: And every time you run a or or or command the resulting document fragments will be POSTed to that API endpoint, in addition to the Showboat Markdown file itself being updated. There are full details in the Showboat README - it's a very simple API format, using regular POST form variables or a multipart form upload for the image attached to . It's simple enough to build a webapp to receive these updates from Showboat, but I needed one that I could easily deploy and would work well with the rest of my personal ecosystem. So I had Claude Code write me a Datasette plugin that could act as a Showboat remote endpoint. I actually had this building at the same time as the Showboat remote feature, a neat example of running parallel agents . datasette-showboat is a Datasette plugin that adds a endpoint to Datasette for viewing documents and a endpoint for receiving updates from Showboat. Here's a very quick way to try it out: Click on the sign in as root link that shows up in the console, then navigate to http://127.0.0.1:8001/-/showboat to see the interface. Now set your environment variable to point to this instance: And run Showboat like this: Refresh that page and you should see this: Click through to the document, then start Claude Code or Codex or your agent of choice and prompt: The command assigns a UUID and title and sends those up to Datasette. The best part of this is that it works in Claude Code for web. Run the plugin on a server somewhere (an exercise left up to the reader - I use Fly.io to host mine) and set that environment variable in your Claude environment, then any time you tell it to use Showboat the document it creates will be transmitted to your server and viewable in real time. I built Rodney , a CLI browser automation tool, specifically to work with Showboat. It makes it easy to have a Showboat document load up web pages, interact with them via clicks or injected JavaScript and captures screenshots to embed in the Showboat document and show the effects. This is wildly useful for hacking on web interfaces using Claude Code for web, especially when coupled with the new remote publishing feature. I only got this stuff working this morning and I've already had several sessions where Claude Code has published screenshots of its work in progress, which I've then been able to provide feedback on directly in the Claude session while it's still working. A few days ago I had another idea for a way to extend the Showboat ecosystem: what if Showboat documents could easily include charts? I sometimes fire up Claude Code for data analysis tasks, often telling it to download a SQLite database and then run queries against it to figure out interesting things from the data. With a simple CLI tool that produced PNG images I could have Claude use Showboat to build a document with embedded charts to help illustrate its findings. Chartroom is exactly that. It's effectively a thin wrapper around the excellent matplotlib Python library, designed to be used by coding agents to create charts that can be embedded in Showboat documents. Here's how to render a simple bar chart: It can also do line charts, bar charts, scatter charts, and histograms - as seen in this demo document that was built using Showboat. Chartroom can also generate alt text. If you add to the above it will output the alt text for the chart instead of the image: Or you can use or to get the image tag with alt text directly: I added support for Markdown images with alt text to Showboat in v0.5.0 , to complement this feature of Chartroom. Finally, Chartroom has support for different matplotlib styles . I had Claude build a Showboat document to demonstrate these all in one place - you can see that at demo/styles.md . I started the Chartroom repository with my click-app cookiecutter template, then told a fresh Claude Code for web session: We are building a Python CLI tool which uses matplotlib to generate a PNG image containing a chart. It will have multiple sub commands for different chart types, controlled by command line options. Everything you need to know to use it will be available in the single "chartroom --help" output. It will accept data from files or standard input as CSV or TSV or JSON, similar to how sqlite-utils accepts data - clone simonw/sqlite-utils to /tmp for reference there. Clone matplotlib/matplotlib for reference as well It will also accept data from --sql path/to/sqlite.db "select ..." which runs in read-only mode Start by asking clarifying questions - do not use the ask user tool though it is broken - and generate a spec for me to approve Once approved proceed using red/green TDD running tests with "uv run pytest" Also while building maintain a demo/README.md document using the "uvx showboat --help" tool - each time you get a new chart type working commit the tests, implementation, root level README update and a new version of that demo/README.md document with an inline image demo of the new chart type (which should be a UUID image filename managed by the showboat image command and should be stored in the demo/ folder Make sure "uv build" runs cleanly without complaining about extra directories but also ensure dist/ and uv.lock are in gitignore This got most of the work done. You can see the rest in the PRs that followed. The Showboat family of tools now consists of Showboat itself, Rodney for browser automation, Chartroom for charting and datasette-showboat for streaming remote Showboat documents to Datasette. I'm enjoying how these tools can operate together based on a very loose set of conventions. If a tool can output a path to an image Showboat can include that image in a document. Any tool that can output text can be used with Showboat. I'll almost certainly be building more tools that fit this pattern. They're very quick to knock out! The environment variable mechanism for Showboat's remote streaming is a fun hack too - so far I'm just using it to stream documents somewhere else, but it's effectively a webhook extension mechanism that could likely be used for all sorts of things I haven't thought of yet. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Showboat remote publishing datasette-showboat How I built Chartroom The burgeoning Showboat ecosystem

0 views
Jimmy Miller 1 weeks ago

Untapped Way to Learn a Codebase: Build a Visualizer

The biggest shock of my early career was just how much code I needed to read that others wrote. I had never dealt with this. I had a hard enough time understanding my own code. The idea of understanding hundreds of thousands or even millions of lines of code written by countless other people scared me. What I quickly learned is that you don't have to understand a codebase in its entirety to be effective in it. But just saying that is not super helpful. So rather than tell, I want to show. In this post, I'm going to walk you through how I learn an unfamiliar codebase. But I'll admit, this isn't precisely how I would do it today. After years of working on codebases, I've learned quite a lot of shortcuts. Things that come with experience that just don't translate for other people. So what I'm going to present is a reconstruction. I want to show bits and parts of how I go from knowing very little to gaining knowledge and ultimately, asking the right questions. To do this, I will use just a few techniques: I want to do this on a real codebase, so I've chosen one whose purpose and scope I'm generally familiar with. But one that I've never contributed to or read, Next.js . But I've chosen to be a bit more particular than that. I'm particularly interested in learning more about the Rust bundler setup (turbopack) that Next.js has been building out. So that's where we will concentrate our time. Trying to learn a codebase is distinctly different from trying to simply fix a bug or add a feature. In post, we may use bugs, talk about features, make changes, etc. But we are not trying to contribute to the codebase, yet. Instead, we are trying to get our mind around how the codebase generally works. We aren't concerned with things like coding standards, common practices, or the development roadmap. We aren't even concerned with correctness. The changes we make are about seeing how the codebase responds so we can make sense of it. I find starting at to be almost always completely unhelpful. From main, yes, we have a single entry point, but now we are asking ourselves to understand the whole. But things actually get worse when dealing with a large codebase like this. There isn't even one main. Which main would we choose? So instead, let's start by figuring out what our library even consists of. A couple of things to note. We have packages, crates, turbo, and turbopack. Crates are relevant here because we know we are interested in some of the Rust code, but we are also interested in turbopack in particular. A quick look at these shows that turbo, packages, and crates are probably not our target. Why do I say that? Because turbopack has its own crates folder. So there are 54 crates under turbopack.... This is beginning to feel a bit daunting. So why don't we take a step back and find a better starting point? One starting point I find particularly useful is a bug report . I found this by simply looking at recently opened issues. When I first found it, it had no comments on it. In fact, I find bug reports with only reproducing instructions to be the most useful. Remember, we are trying to learn, not fix a bug. So I spent a little time looking at the bug report. It is fairly clear. It does indeed reproduce. But it has a lot of code. So, as is often the case, it is useful to reduce it to the minimal case. So that's what I did. Here is the important code and the problem we are using to learn from. MyEnum here is dead code. It should not show up in our final bundle. But when we do and look for it, we get: If we instead do The code is completely gone from our build. So now we have our bug. But remember. Our goal here is not to fix the bug. But to understand the code. So our goal is going to be to use this little mini problem to understand what code is involved in this bug. To understand the different ways we could fix this bug. To understand why this bug happened in the first place. To understand some small slice of the turbopack codebase. So at each junction, we are going to resist the urge to simply find the offending code. We are going to take detours. We are going to ask questions. We hope that from the start of this process to the end, we no longer think of the code involved in this action as a black box. But we will intentionally leave ourselves with open questions. As I write these words, I have no idea where this will take us. I have not prepared this ahead of time. I am not telling you a fake tale from a codebase I already know. Yes, I will simplify and skip parts. But you will come along with me. The first step for understanding any project is getting some part of it running. Well, I say that, but in my day job, I've been at companies where this is a multi-day or week-long effort. Sometimes, because of a lack of access, sometimes from unclear instructions, if you find yourself in that situation, you now have a new task, understand it well enough to get it to build. Well, unfortunately, that is the scenario we find ourselves in. I can't think of a single one of these endeavors I've gone on to learn a codebase that didn't involve a completely undesirable, momentum-stopping side quest. For this one, it was as soon as I tried to make changes to the turbopack Rust code and get it working in my test project. There are instructions on how to do this . In fact, we even get an explanation on why it is a bit weird. Since Turbopack doesn't support symlinks when pointing outside of the workspace directory, it can be difficult to develop against a local Next.js version. Neither nor imports quite cut it. An alternative is to pack the Next.js version you want to test into a tarball and add it to the pnpm overrides of your test application. The following script will do it for you: Okay, straightforward enough. I start by finding somewhere in the turbopack repo that I think will be called more than once, and I add the following: Yes. Very scientific, I know. But I've found this to be a rather effective method of making sure my changes are picked up. So I do that, make sure I've built and done the necessary things. I run Then that script tells me to add some overrides and dependencies to my test project. I go to build my project and HERERE!!!!!!! does not show up at all... I will save you the fun details here of looking through this system. But I think it's important to mention a few things. First, being a dependency immediately stood out to me. In my day job, I maintain a fork of swc (WHY???) for some custom stuff. I definitely won't pretend to be an expert on swc, but I know it's written in Rust. I know it's a native dependency. The changes I'm making are native dependencies. But I see no mention at all of turbopack. In fact, if I search in my test project, I get the following: So I have a sneaking suspicion my turbopack code should be in that tar. So let's look at the tar. Ummm. That seems a bit small... Let's look at what's inside. Okay, I think we found our problem. There's really nothing in this at all. Definitely no native code. After lots of searching, the culprit came down to: In our case, the input came from this file and f was . Unfortunately, this little set + regex setup causes to be filtered out. Why? Because it doesn't match the regex. This regex is looking for a with characters after it. We have none. So since we are already in the set (we just added ourselves), we filter ourselves out. How do we solve this problem? There are countless answers, really. I had Claude whip me up one without regex. But my gut says the sorting lets us do this much simpler. But after this fix, let's look at the tar now: Much better. After this change, we can finally see HERERE!!!!!!! a lot. Update : As I wrote this article, someone fixed this in a bit of a different way . Keeping the regex and just changing to . Fairly practical decision. Okay, we now have something we can test. But where do we even begin? This is one reason we chose this bug. It gives a few avenues to go down. First, the report says that these enums are not being "tree-shaken." Is that the right term? One thing I've learned from experience is to never assume that the end user is using terms in the same manner as the codebase. So this can be a starting point, but it might be wrong. With some searching around, we can actually see that there is a configuration for turning turbopackTreeShaking on or off. It was actually a bit hard to find exactly where the default for this was. It isn't actually documented. So let's just enable it and see what we get. Well, I think we figured out that the default is off. So one option is that we never "tree shake" anything. But that seems wrong. At this point, I looked into tree shaking a bit in the codebase, and while I started to understand a few things, I've been at this point before. Sometimes it is good to go deep. But how much of this codebase do I really understand? If tree shaking is our culprit (seeming unlikely at this point), it might be good to know how code gets there. Here, we of course found a bug. But it is an experimental feature. Maybe we can come back and fix it? Maybe we can file a bug? Maybe this code just isn't at all ready for primetime. It's hard to know as an outsider. Our "search around the codebase" strategy failed. So now we try a different tactic. We know a couple of things. We now have two points we can use to try to trace what happens. Let's start with parsing. Luckily, here it is straightforward: . When we look at this code, we can see that swc does the heavy lifting. First, it parses it into a TypeScript AST, then applies transforms to turn it into JavaScript. At this point, we don't write to a string, but if you edit the code and use an emitter, you see this: Now, to find where we write the chunks. In most programs, this would be pretty easy. Typically, there is a linear flow somewhere that just shows you the steps. Or if you can't piece one together, you can simply breakpoint and follow the flow. But Turbopack is a rather advanced system involving async Rust (more on this later). So, in keeping with the tradition of not trying to do things that rely too heavily on my knowledge, I have done the tried and true, log random things until they look relevant. And what I found made me realize that logging was not going to be enough. It was time to do my tried and true learning technique, visualization. Ever since my first job , I have been building custom tools to visualize codebases. Perhaps this is due to my aphantasia. I'm not really sure. Some of these visualizers make their way into general use for me. But more often than not, they are a means of understanding. When I applied for a job at Shopify working on YJIT, I built a simple visualizer but never got around to making it more useful than a learning tool. The same thing is true here, but this time, thanks to AI, it looks a bit more professional. This time, we want to give a bit more structure than what we'd be able to do with a simple print. 1 We are trying to get events out that have a bunch of information. Mostly, we are interested in files and their contents over time. Looking through the codebase, we find that one key abstract is an ident; this will help us identify files. We will simply find points that seem interesting, make a corresponding event, make sure it has idents associated with it, and send that event over a WebSocket. Then, with that raw information, we can have our visualizer stitch together what exactly happens. If we take a look, we can see our code step through the process. And ultimately end up in the bundle despite not being used. If you notice, though, between steps 3 and 4, our code changed a bit. We lost this PURE annotation. Why? Luckily, because we tried to capture as much context as we could. We can see that a boolean "Scope Hoisting" has been enabled. Could that be related? If we turn it off, we instead see: Our pure annotation is kept around, and as a result, our code is eliminated. If we take a step back, this can make sense. Something during the parse step is creating a closure around our enum code, but when it does so, it is marking that as a "pure" closure, meaning it has no side effects. Later, because this annotation is dropped, the minifier doesn't know that it can just get rid of this closure. As I've been trying to find time to write this up, it seems that people on the bug report have found this on their own as well. So we've found the behavior of the bug. Now we need to understand why it is happening. Remember, we are fixing a bug as a means of understanding the software. Not just to fix a bug. So what exactly is going on? Well, we are trying to stitch together two libraries. Software bugs are way more likely to occur on these seams. In this case, after reading the code for a while, the problem becomes apparent. SWC parses our code and turns it into an AST. But if you take a look at an AST , comments are not a part of the AST. So instead, SWC stores comments off in a hashmap where we can look them up by byte pos. So for each node in the AST, it can see if there is a comment attached. But for the PURE comment, it doesn't actually need to look this comment up. It is not a unique comment that was in the source code. It is a pre-known meta comment. So rather than store each instance in the map, it makes a special value. Now, this encoding scheme causes some problems for turbopack. Turbopack does not act on a single file; it acts across many files. In fact, for scope hoisting, we are trying to take files across modules and condense them into a single scope. So now, when we encounter one of these bytepos encodings, how do we know what module it belongs to? The obvious answer to many might be to simply make a tuple like , and while that certainly works, it does come with tradeoffs. One of these is memory footprint. I didn't find an exact reason. But given the focus on performance on turbopack, I'd imagine this is one of the main motivations. Instead, we get a fairly clever encoding of module and bytepos into a single BytePos, aka a u32. I won't get into the details of the representation here; it involves some condition stuff. But needless to say, now that we are going from something focusing on one file to focusing on multiple and trying to smuggle in this module_id into our BytePos, we ended up missing one detail, PURE. Now our pure value is being interpreted as some module at some very high position instead of the proper bytes. To fix this bug, I found the minimal fix was simply the following: With this our enum properly is marked as PURE and disappears from the output! Now remember, we aren't trying to make a bug fix. We are trying to understand the codebase. Is this the right fix? I'm not sure. I looked around the codebase, and there are a number of other swc sentinel values that I think need to also be handled (PLACEHOLDER and SYNTHESIZED). There is also the decoding path. For dummy, the decoding path panics. Should we do the same? Should we be handling pure at a higher level, where it never even goes through the encoder? Update : As I was writing this, someone else proposed a fix . As I was writing the article, I did see that others had started to figure out the things I had determined from my investigation. But I was not confident enough that it was the right fix yet. In fact, the PR differs a bit from my local fix. It does handle the other sentinel, but at a different layer. It also chooses to decode with module 0. Which felt a bit wrong to me. But again, these are decisions that people who work on this codebase long-term are better equipped to decide than me. I must admit that simply fixing this bug didn't quite help me understand the codebase. Not just because it is a fairly good size. But because I couldn't see this fundamental unit that everything was composed of. In some of the code snippets above, you will see types that mention Vc. This stands for ValueCell. There are a number of ways to try to understand these; you can check out the docs for turbo engine for some details. Or you can read the high-level overview that skips the implementation details for the most part. You can think of these cells like the cells in a spreadsheet. They provide a level of incremental computation. When the value of some cell updates, we can invalidate stuff. Unlike a spreadsheet, the turbo engine is lazy. I've worked with these kinds of systems before. Some are very explicitly modeled after spreadsheets. Others are based on rete networks or propagators. I am also immediately reminded of salsa from the Rust analyzer team. I've also worked with big, complicated non-computational graphs. But even with that background, I know myself, I've never been able to really understand these things until I can visualize them. And while a general network visualizer can be useful (and might actually be quite useful if I used the aggregate graph), I've found that for my understanding, I vastly prefer starting small and exploring out the edges of the graph. So that's what I did. But before we get to that visualization, I want to highlight something fantastic in the implementation: a central place for controlling a ton of the decisions that go into this system. The backend here lets us decide so many things about how the execution of our tasks will run. Because of this, we have one place we can insert a ton of tooling and begin to understand how this system works. As before, we are going to send things on a WebSocket. But unlike last time, our communication will actually be two-way. We are going to be controlling how the tasks run. In my little test project, I edited a file, and my visualizer displayed the following. Admittedly, it is a bit janky, but there are some nice features. First, on the left, we can see all the pending tasks. In this case, something has marked our file read as dirty, so we are trying to read the file. We can see the contents of a cell that this task has. And we can see the dependents of this task. Here is what it looks like once we release that task to run. We can now see 3 parse tasks have kicked off. Why 3? I'll be honest, I haven't looked into it. But a good visualization is about provoking questions, not only answering them. Did I get my visualization wrong because I misunderstood something about the system? Are there three different subsystems that want to parse, and we want to do them in parallel? Have we just accidentally triggered more parses than we should be? This is precisely what we want out of a visualizer. Is it perfect? No, would I ship this as a general visualizer? No. Am I happy with the style? Not in the least. But already it enables a look into the project I couldn't see before. Here we can actually watch the graph unfold as I execute more steps. What a fascinating view of a once opaque project. With this visualizer, I was able to make changes to my project and watch values as they flow through the systems. I made simple views for looking at code. If I extended this, I can imagine it being incredibly useful for debugging general issues, for seeing the ways in which things are scheduled, and for finding redundancies in the graph. Once I was able to visualize this, I really started to understand the codebase better. I was able to see all the values that didn't need to be recomputed when we made changes. The whole thing clicked. This was an exercise in exploring a new codebase that is a bit different of a process than I see others take. It isn't an easy process, it isn't quick. But I've found myself repeating this process over and over again. For the turborepo codebase, this is just the beginning. This exploration was done over a few weekends in the limited spare time I could find. But already I can start to put the big picture together. I can start to see how I could shape my tools to help me answer more questions. If you've never used tool building as a way to learn a codebase, I highly recommend it. One thing I always realize as I go through this process is just how hard it is to work interactively with our current software. Our languages, our tools, our processes are all written without ways to live code, without ways to inspect their inner workings. It is also incredibly hard to find a productive UI environment for this kind of live exploration. The running state of the visualizer contains all the valuable information. Any system that needs you to retrace your steps to get the UI back to the state it was once in to visualize more is incredibly lacking. So I always find myself in the browser, but immediately, I am having to worry about performance. We have made massive strides in so many aspects of software development. I hope that we will fix this one as well. Setting a goal Editing randomly Fixing things I find that are broken Reading to answer questions Making a visualizer Our utilities.ts file is read and parsed. It ends up in a file under a "chunks" directory.

0 views
matduggan.com 1 weeks ago

I Sold Out for $20 a Month and All I Got Was This Perfectly Generated Terraform

Until recently the LLM tools I’ve tried have been, to be frank, worthless. Copilot was best at writing extremely verbose comments. Gemini would turn a 200 line script into a 700 line collection of gibberish. It was easy for me to, more or less, ignore LLMs for being the same over-hyped nonsense as the Metaverse and NFTs. This is great for me because I understand that LLMs represent a massive shift in power from an already weakened worker class to an increasingly monarch-level wealthy class. By stealing all human knowledge and paying nothing for it, then selling the output of that knowledge, LLMs are an impossibly unethical tool. So if the energy wasting tool of the tech executive class is also a terrible tool, easy choice. Like boycotting Tesla for being owned by an evil person and also being crappy overpriced cars, or not shopping at Hobby Lobby and just buying directly from their Chinese suppliers, the best boycotts are ones where you aren’t really losing much. Google can continue to choke out independent websites with their AI results that aren’t very good and I get to feel superior doing what I was going to do anyway by not using Google search. This logic was all super straight forward right up until I tried Claude Code. Then it all got much more complicated. Let’s just get this out of the way right off the bat. I didn't want to like Claude Code. I got a subscription with the purpose of writing a review on it where I would find that it was just as terrible as Gemini and Copilot. Except that's not what happened. Instead it was like discovering the 2AM kebab place might actually make the best pizza in town. I kept asking Claude to do annoying tasks where it was easy for me to tell if it had made a mistake and it kept doing them correctly. It felt impossible but the proof was right in front of me. I’ve written tens of thousands of lines of Terraform in my life. It is a miserable chore to endlessly flip back and forth between the provider documentation and Vim, adding all the required parameters. I don’t learn anything by doing it, it’s just a grind I have to push through to get back to the meaningful work. The amount of time I have wasted on this precious time on Earth importing all of a companies DNS records into Terraform, then taking the autogenerated names and organizing them so that they make sense for the business is difficult to express. It's like if the only way I knew how to make a hamburger bun was to carefully put every sesame seed by hand on the top only to stumble upon an 8 pack of buns for $4 at the grocery store after years of using tiny tweezers to put the seeds in exactly the right spot. I feel the same way about writing robust READMEs, k8s YAML and reorganizing the file structure of projects. Setting up more GitHub Actions is as much fun as doing my taxes. If I never had to write another regex for the rest of my life, that would be a better life by every conceivable measure. These are tasks that sap my enthusiasm for this type of work, not feed it. I’m not sad to offload them and switch to mostly reviewing its PRs. But the tool being useful doesn’t remove what’s bad about it. This is where a lot of pro-LLM people start to delude themselves. In no particular order are the arguments I keep seeing about LLMs from people who want to keep using them for why their use is fine. This is the most common one I see and the worst. It can be condensed down to “because most things on the internet originally existed to find pornography and/or pirate movies, stealing all content on the internet is actually fine because programmers don’t care about copyright”. You also can’t have it both ways. OpenAI can’t decide to enforce NDAs and trademarks and then also declare law is meaningless. If I don’t get to launch a webmail service named Gmail+ then Google doesn’t get to steal all the books in human existence. The argument basically boils down to: because we all pirated music in 2004, intellectual property is a fiction when it stands in the way of technology. By this logic I shoplifted a Snickers bar when I was 12 so property rights don't exist and I should be allowed to live in your house. I have an internet friend I met years ago playing EVE Online that is a brutally pragmatic person. To someone like him, code craftsmanship is a joke. For those of you who are unaware, EVE Online is the spaceship videogame where sociopaths spend months plotting against each other. His approach to development is 80% refining requirements and getting feedback. He doesn’t care at all about DRY, he uses Node because then he can focus on just JavaScript, he doesn’t invest a second into optimization until the application hits a hard wall that absolutely requires it. His biggest source of clients? Creating fast full stacks because internal teams are missing deadlines. And he is booked up for at least 12 months out all the time because he hits deadlines. When he started freelancing I thought he was crazy. Who was going to hire this band of Eastern European programmers who chain smoke during calls and whose motto is basically "we never miss a deadline". As it turns out, a lot of people. Why doesn't he care? Why doesn't he care about these things? He believes that programmers fundamentally don't understand the business they are in. "Code is perishable" is something he says a lot and he means it. Most of the things we all associate with quality (full test coverage, dependency management, etc) are programmers not understanding the rate of churn a project undergoes over its lifespan. The job of a programmer, according to him, is delivering features that people will use. How pleasant and well-organized that code is to work with is not really a thing that matters in the long term. He doesn't see LLM-generated code as a problem because he's not building software with a vision that it will still be used in 10 years. Most of the stuff typically associated with quality he, more or less, throws in the trash. He built a pretty large stack for a automotive company and my jaw must have hit the table when he revealed they're deploying m6g.4xlarge for a NodeJS full-stack application. "That seems large to me for that type of application" was my response. He was like "yeah but all I care about are whether the user metrics show high success rate and high performance for the clients". It's $7000 a year for the servers, with two behind a load balancer. That's absolutely nothing when compared with the costs of what having a team of engineers tune it would cost and it means he can run laps around the internal teams who are, basically, his greatest competition. To be clear, he is very technically competent. He simply rejects a lot of the conventional wisdom out there about what one has to do in order to make stuff. He focuses on features, then securing endpoints and more or less gives up on the rest of it. For someone like this, LLMs are a logical choice for him. The annoying thing about my friend is that his bank account suggests he's right. But I can't get there. If I'm writing a simple script or something as a one-off, it can sometimes feel like we're all wasting the companies time when we have a long back and forth on the PR discussing comments or the linting or whatever. So it's not that this idea is entirely wrong . But the problem with programming is you never know what is going to be "the core" of your work life for the next 5 years. Sometimes I write a feature, we push it out, it explodes in popularity and then I'm a little bit in trouble because I built a MVP and now it's a load-bearing revenue generating thing that has to be retooled. I also just have trouble with the idea that this is my career and the thing I spend my limited time on earth doing and the quality of it doesn't matter. I delight in craftsmanship when I encounter it in almost any discipline. I love it when you walk into an old house and see all the hand crafted details everywhere that don't make economic sense but still look beautiful. I adore when someone has carefully selected the perfect font to match something. Every programmer has that library or tool that they aspire to. That code base where you delight at looking at it because it proves perfection is possible even if you have never come close to reaching that level. For me its always been looking through the source code of SQLite that restores my confidence. I might not know what I'm doing but it's good to be reminded that someone out there does. Not everything I make is that great, but the concept of "well great doesn't matter at all" effectively boils down to "don't take pride in your work" which is probably the better economic argument but feels super bad to me. In a world full of cheap crap, it feels bad to make more of it and then stick my name on it. The best argument for why programmers should be using LLMs is because it's going to be increasingly difficult to compete for jobs and promotions against people who are using them. In my experience Claude Code allows me to do two tasks at once. That's a pretty hard advantage to overcome. Last Tuesday I had Claude Code write a GitHub Action for me while I worked on something else. When it was done, I reviewed it, approved it, and merged it. It was fine. It was better than fine, actually — it was exactly what I would have written, minus the forty-five minutes of resentment. I sat there for a moment, staring at the merged PR, feeling the way I imagine people feel when they hire a cleaning service for the first time: relieved, and then immediately guilty about the relief, and then annoyed at myself for feeling guilty about something that is, by any rational measure, a completely reasonable thing to do. Except it isn't reasonable. Or maybe it is. I genuinely don't know anymore, and that's the part that bothers me the most — not that the tool works, but that I've lost the clean certainty that it shouldn't. So now I'm paying $20 a month to a company that scraped the collective knowledge of humanity without asking so that I can avoid writing Kubernetes YAML. I know what that makes me. I just haven't figured out a word for it yet that I can live with. When I asked my EVE friend about it on a recent TeamSpeak session, he was quiet for awhile. I thought that maybe my moral dilemma had shocked him into silence. Then he said, "You know what the difference is between you and me? I know I'm a mercenary. You thought you were an artist. We're both guys who type for money." I couldn't think of a clever response to that. I still can't.

0 views
iDiallo 1 weeks ago

Programming is free

A college student on his spring break contacted me for a meeting. At the time, I had my own startup and was navigating the world of startup school with Y Combinator and the publicity from TechCrunch. This student wanted to meet with me to gain insight on the project he was working on. We met in a cafe, and he went straight to business. He opened his MacBook Pro, and I glimpsed at the website he and his partner had created. It was a marketplace for college students. You could sell your items to other students in your dorm. I figured this was a real problem he'd experienced and wanted to solve. But after his presentation, I only had one question in mind, about something he had casually dropped into his pitch without missing a beat. He was paying $200 a month for a website with little to no functionality. To add to it, the website was slow. In fact, it was so slow that he reassured me the performance problems should disappear once they upgraded to the next tier. Let's back up for a minute. When I was getting started, I bought a laptop for $60. A defective PowerBook G4 that was destined for the landfill. I downloaded BBEdit, installed MAMP, and in little to no time I had clients on Craigslist. That laptop paid for itself at least 500 times over. Then a friend gave me her old laptop, a Dell Inspiron e1505. That one paved the way to a professional career that landed me jobs in Fortune 10 companies. I owe it all not only to the cheap devices I used to propel my career and make a living, but also to the free tools that were available. My IDE was Vim. My language was PHP, a language that ran on almost every server for the price of a shared hosting plan that cost less than a pizza. My cloud was a folder on that server. My AI pair programmer was a search engine and a hope that someone, somewhere, had the same problem I did and had posted the solution on a forum. The only barrier to entry was the desire to learn. Fast forward to today, every beginner is buying equipment that can simulate the universe. Before they start their first line of code, they have subscriptions to multiple paid services. It's not because the free tools have vanished, but because the entire narrative around how to get started is now dominated by paid tools and a new kind of gatekeeper: the influencer. When you get started with programming today, the question is "which tool do I need to buy?" The simple LAMP stack (Linux, Apache, MySQL, PHP) that launched my career and that of thousands of developers is now considered quaint. Now, beginners start with AWS. Some get the certification before they write a single line of code. Every class and bootcamp sells them on the cloud. It's AWS, it's Vercel, it's a dozen other platforms with complex pricing models designed for scale, not for someone building their first "Hello, World!" app. Want to build something modern? You'll need an API key for this service, a paid tier for that database, and a hosting plan that charges by the request. Even the code editor, once a simple download, is now often a SaaS product with a subscription. Are you going to use an IDE without an AI assistant? Are you a dinosaur? To be a productive programmer, you need a subscription to an AI. It may be a fruitless attempt, but I'll say it anyway. You don't need any paid tools to start learning programming and building your first side project. You never did. The free tools are still there. Git, VS Code (which is still free and excellent!), Python, JavaScript, Node.js, a million static site generators. They are all still completely, utterly free. New developers are not gravitating towards paid tools by accident. Other than code bootcamps selling them on the idea, the main culprit is their medium of learning. The attention economy. As a beginner, you're probably lost. When I was lost, I read documentation until my eyes bled. It was slow, frustrating, and boring. But it was active. I was engaging with the code, wrestling with it line by line. Today, when a learner is lost, they go to YouTube. A question I am often asked is: Do you know [YouTuber Name]? He makes some pretty good videos. And they're right. The YouTuber is great. They're charismatic, they break down complex topics, and they make it look easy. In between, they promote Hostinger or whichever paid tool is sponsoring them today. But the medium is the message, and the message of YouTube is passive consumption . You watch, you nod along, you feel like you're learning. And then the video ends. An algorithm, designed to keep you watching, instantly serves you the next shiny tutorial . You click. You watch. You never actually practice. Now instead of just paying money for the recommended tool, you are also paying an invisible cost. You are paying with your time and your focus. You're trading the deep, frustrating, but essential work of building for the shallow, easy dopamine hit of watching someone else build. The influencer's goal is to keep you watching. The platform's goal is to keep you scrolling. Your goal should be to stop watching and start typing. These goals are at odds. I told that student he was paying a high cost for his hobby project. A website with a dozen products and images shouldn't cost more than a $30 Shopify subscription. If you feel more daring and want to do the work yourself, a $5 VPS is a good start. You can install MySQL, Rails, Postgres, PHP, Python, Node, or whatever you want on your server. If your project gains popularity, scaling it wouldn't be too bad. If it fails, the financial cost is a drop in a bucket. His story stuck with me because it wasn't unique. It's the default path now: spend first, learn second. But it doesn't have to be. You don't need an AI subscription. You don't need a YouTuber. You need a text editor (free), a language runtime (free), and a problem you want to solve. You need to get bored enough to open a terminal and start tinkering. The greatest gift you can give yourself as a new programmer isn't a $20/month AI tool or a library of tutorial playlists. It's the willingness to stare at a blinking cursor and a cryptic error message until you figure it out yourself. Remember, my $60 defective laptop launched a career. That student's $200/month website taught him to wait for someone else to fix his problems. The only difference between us was our approach. The tools for learning are, and have always been, free. Don't let anyone convince you otherwise.

0 views
JSLegendDev 1 weeks ago

I Tried 3 Web Game Frameworks (So You Don’t Have To)

I went on a quest to test the 3 most popular 2D web game frameworks according to their NPM download numbers. To achieve this, I set out to build a Sonic infinite runner game in each framework. Today, I’d like to share my findings. Below are links to playable demos along with the source code for each version of the game. I recommend browsing the code of each version to get a better feel of the differences between them. KAPLAY version - ( live demo ) - ( source code ) Phaser version - ( live demo ) - ( source code ) Excalibur version - ( live demo ) - ( source code ) Note that even though KAPLAY markets itself as a library and Excalibur as a game engine they still feel like frameworks and occupy the same space as Phaser. That’s why I took the liberty to refer to them as frameworks. I will not beat around the bush. If you want stability and performance, you should pick Phaser. That said, overall, KAPLAY is the easiest one to learn but the least scalable of the 3. This makes it a good choice to get into game development as a beginner and to prototype ideas quickly but you’ll end up facing performance issues eventually. I’d like to note that there have been recent performance improvements. However, KAPLAY’s performance still doesn’t come anywhere close to the one offered by Phaser. Additionally, Phaser has an AI advantage. For example, if you ask ChatGPT a question related to Phaser, more likely than not, the answer provided will be right. As for KAPLAY and Excalibur, you’ll get a lot more hallucinations. This AI advantage lowers the barrier of learning for Phaser which, in contrast, makes the easiness of KAPLAY less compelling. While Phaser’s API is verbose, you get over it especially when you get performance and stability in exchange. Finally, Phaser is significantly more popular than the other 2 which means it will be easier to find help when needed. If you’re looking to target mobile, Phaser is also your best bet between the 3 as the performance on Safari is also quite good and the best. This is relevant because Safari uses the Webkit web engine which is what’s forced on every browser and webview on iOS. Finally, for multiplayer games, Phaser offers the ability to run it on the server in headless mode. This makes it much easier to handle state in a multiplayer game. To my knowledge, neither KAPLAY nor Excalibur offers something like this. As mentioned previously, KAPLAY might still be worth using in the context of prototyping or for game jams, as its component based approach allows you to truly iterate quickly on game design. Its simplicity also makes it an ideal candidate for teaching game development to beginners. Some have used it to teach children. Phaser and Excalibur felt very similar when using them as they both relied on the object oriented paradigm. Yet, I felt that Excalibur sometimes required more work. A notable example of this is when coming back to a scene after having switched away from it. The state of that scene would be left as is before transitioning over. This meant you had to write extra logic to reset the scene’s state, something you didn’t have to do in Phaser. That said, I still felt that Excalibur’s API was on average a bit more concise than Phaser’s. In conclusion, the issue with Excalibur is that it didn’t feel different enough from Phaser to warrant switching over to it, considering Phaser is more performant, more featureful, and more popular. At the same time, I was impressed with how Excalibur handled its debug mode. Instead of having it within the framework, it was instead available as a browser extension. Debug mode would therefore appear in the browser devtools and could be toggled and configured from there. It was a nice experience. Regardless of the web framework you end up using, if you want to bring your web game to Steam, the easiest tool for achieving this is GemShell. It allows you to create executables for Windows, Mac and Linux in what amounts to a single click. It also offers an integration with Steam so you can easily have access to features like Steam achievements and now also the Steam overlay. (The latter might not be available yet at the time of publishing this post.) (Video by the developer showcasing the use of Steam overlay on top of a game packaged with GemShell) This tool will save you hours that would have been wasted configuring tools like Electron and Steamworks.js. Time which could have been spent working on your game instead. To have a consistent experience across operating systems, GemShell now makes each executable Chromium based so that your game renders the same way on all platforms. Additionally, The developer is also considering implementing the ability to export for mobile platforms as well. Due to how useful this tool is, I have decided to partner with its developer to offer you a 15% discount if you use the following link : https://l0om.itch.io/jslegend To be completely transparent, if you end up purchasing GemShell using the link above, I’ll get a cut of that sale. That said, a free lite version is available, if you want to get a feel for the tool. I hope you enjoyed this post, I recommend subscribing to not miss out on future releases. Subscribe now In the meantime, you can read the following : I went on a quest to test the 3 most popular 2D web game frameworks according to their NPM download numbers. To achieve this, I set out to build a Sonic infinite runner game in each framework. Today, I’d like to share my findings. Below are links to playable demos along with the source code for each version of the game. I recommend browsing the code of each version to get a better feel of the differences between them. KAPLAY version - ( live demo ) - ( source code ) Phaser version - ( live demo ) - ( source code ) Excalibur version - ( live demo ) - ( source code )

0 views
Dominik Weber 1 weeks ago

Lighthouse update February 16th

## Website to feed The past week had first and foremost one improvement, website to feed conversion. It enables users to subscribe to websites that don't provide an RSS feed. This feature consists of multiple areas. The backbone is extracting items from a website based on CSS selectors, and then putting those items through the same pipeline as items of an RSS feed. Meaning extracting full content, calculating reading time, creating a summary, creating an about sentence, interpreting language and topic, and so on. Additional areas are all about making it easier to use. Showing the website and letting users select items simplifies the feature, for many websites it's not necessary to even know about the selectors. This also required some heuristics about which elements to select and how to find the repeating items from just one selection. The user experience can always be improved, but I think as it is right now it's already quite decent. The next step for this feature is to automatically find the relevant items, without the user having to select anything. ## Next steps An ongoing thing is the first user experience. It's not where I want it to be, but honestly it's difficult to know or imagine how it should be. One issue that came up repeatedly is the premium trial, and that users don't want to provide their credit card just to start the trial. That's fair. Though Paddle, the payment system Lighthouse uses, doesn't provide another option. They have it as private beta, but I didn't get invited to that unfortunately. So I'm going to bite the bullet and implement this myself. Won't be as great as if Paddle does it, but at least users will get the premium experience for 2 weeks after signup. An improvement I had my eyes on for some time is using the HTML of RSS feed items for the preview. Lighthouse attempts to parse the full content for all items, but that's not always possible. If websites disallow it via robots.txt, or block via bot protection, Lighthouse doesn't get the content. In these cases it shows that access was blocked. But if the feed contains some content, that could be displayed. Feeds usually don't contain the full content, but it's at least something. One more thing I wanted to do for a long time, and can finally make time for, is creating collections of feeds for specific topics. For example "Frontier AI labs", "Company engineering blogs", "JS ecosystem", and so on. The [blogroll editor](https://lighthouseapp.io/tools/blogroll-editor) is the basis for that. It lets you create a collection of websites and feeds, and export OPML from that. I'm going to improve its UX a bit and then start creating these collections.

0 views