├── .gitignore ├── blog ├── a-cleaner-node-modules-ecosystem │ ├── contributions.png │ ├── index.md │ ├── nodemodules.png │ └── which.png ├── barrel-files-a-case-study │ ├── 74-modules.png │ ├── barrel-begone-1.png │ ├── barrel-begone-2.png │ ├── barrel-file.png │ ├── graphql-barrel.png │ ├── index.md │ ├── initiator.png │ └── interceptors.png ├── events-are-the-shit │ └── index.md ├── i-overengineered-my-blog │ └── index.md ├── practical-barrel-file-guide-for-library-authors │ └── index.md ├── rustify-your-js-tooling │ └── index.md ├── self-unregistering-service-workers │ ├── happy-scenario.jpg │ ├── index.md │ └── unhappy-scenario.jpg ├── service-worker-templating-language-(swtl) │ └── index.md └── the-cost-of-convenience │ └── index.md ├── env.js ├── md-to-html.js ├── netlify.toml ├── netlify └── functions │ └── blog │ └── blog.mjs ├── package.json ├── public ├── sw.js └── sw.js.map ├── router.js ├── src ├── Html.js └── utils.js ├── sw.js └── thoughts ├── on-bun └── index.md └── on-web-components └── index.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Local Netlify folder 2 | .netlify 3 | package-lock.json 4 | node_modules/ 5 | public/output 6 | .DS_Store -------------------------------------------------------------------------------- /blog/a-cleaner-node-modules-ecosystem/contributions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/a-cleaner-node-modules-ecosystem/contributions.png -------------------------------------------------------------------------------- /blog/a-cleaner-node-modules-ecosystem/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A cleaner node modules ecosystem 3 | description: And how to fix it 4 | updated: 2024-07-25 5 | --- 6 | 7 | # A cleaner `node_modules` ecosystem 8 | 9 | There are many jokes in the software development ecosystem about the size of `node_modules` and JavaScript dependencies in general, like for example dependencies like `is-even`, or `is-odd`, or packages adding a ton of dependencies to support ridiculously low versions of Node.js. Fortunately, there are many individuals who are working on improving this situation, but there are also individuals actively adding more and more unnecessary dependencies to popular projects that get millions of downloads, leading to bloated `node_modules` folders with tons of dependencies. 10 | 11 | ![Heaviest objects in the world ranked from lightest to heaviest starting with the sun a neutron star a black hole and finally node modules](https://imgur.com/E0zycbs.png) 12 | 13 | A lot of people have spoken up about this recently, and this has caused a great initiative to form in the shape of the [e18e community](https://e18e.dev/), where a bunch of talented, like-minded people are actively working on creating a cleaner, healthier, and more performant ecosystem. I should also note that the e18e initiative is **not only** about countering package bloat, there's a lot of important work going on to improve runtime performance of packages as well, but in this blog I'll be mainly addressing the `node_modules` situation. 14 | 15 | ## How do we fix this situation the ecosystem is in? 16 | 17 | It starts with awareness. I've always been a firm believer of speaking up about things that you disagree with, because it might resonate with others and actually lead to change. (Definitely) not always, but sometimes it does. 18 | 19 | Package bloat can be a very tricky thing to keep track of; you install a dependency and never think about it again. But what dependencies does your dependency have? Do you religiously review your `package-lock.json` on PRs? Do you inspect your JavaScript bundles to see if anything weird got bundled? For a large amount of people the answer is likely "no". A [smart guy](https://x.com/kettanaito) once pointed out to me, in a discussion about barrel files and the performance implications of using them, that: 20 | 21 | > "Changes like this cannot really happen in a sustainable fashion unless there's tooling around them." 22 | 23 | And I wholeheartedly agree with that. If we're to counter package bloat, we're gonna need tooling; nobody wants to do these things manually, we need automation. 24 | 25 | Fortunately, there are many projects being worked on to achieve this. The list at [e18e - resources](https://e18e.dev/guide/resources.html) is a good overview of tools that you can use in your projects, but in this blog I'd like to discuss a couple I've personally been involved with and worked on. 26 | 27 | ## Module Replacements 28 | 29 | [`es-tooling/module-replacements`](https://github.com/es-tooling/module-replacements) is a project that was started by [James Garbutt](https://x.com/43081j) who started up a lot of great work on the ecosystem cleanup. Module replacements provides several "manifests" that list modules, and leaner, lighter replacements for them; which can either be alternative packages with fewer dependencies, or API's that have now been built-in natively. 30 | 31 | What's nice about having these manifests available as machine readable documents, is that we can use them for automation, like for example [`eslint-plugin-depend`](https://www.npmjs.com/package/eslint-plugin-depend). `eslint-plugin-depend` helps suggest alternatives to various dependencies based on `es-tooling/module-replacements`, so you can easily discover potentially problematic dependencies in your projects, and find more modern/lightweight solutions for them. 32 | 33 | ## Module Replacements Codemods 34 | 35 | If we have these module replacements and alternatives for them available, we can even take things further and automatically replace them via codemods. 36 | 37 | For those of you who are unsure what codemods are, codemods are automatic transformations that run on your codebase programmatically. What that means is that you give a codemod some input source code, and it will output changed code, like for example cleanup of dependencies. For example, a codemod for `is-even` would result in: 38 | 39 | Before: 40 | ```js 41 | const isEven = require('is-even'); 42 | 43 | console.log(isEven(0)); 44 | ``` 45 | 46 | After: 47 | ```js 48 | console.log((0 % 2 === 0)); 49 | ``` 50 | 51 | To achieve this, we created the repository [`es-tooling/module-replacements-codemods`](https://github.com/es-tooling/module-replacements-codemods), which aims to provide automatic codemods for the module listed in `es-tooling/module-replacements`, so we can automatically replace them. Implementing codemods _for all these packages_ is a herculean effort; there are simply so many of them, which is why I've been spamming Twitter hoping to find more like-minded people and contributors. 52 | 53 | Fortunately (and a huge shoutout to [codemod.studio](https://codemod.com/studio) here, which is an amazing tool to start building codemods — even if you've never built one before!), the bulk of them were mostly fairly straightforward to implement, and we got many, many great contributions from people to implement codemods. What's nice about this is that for some of those people, it was their first time implementing a codemod! Being able to create codemods is a super valuable skill to have as a developer, so it's been really nice to see people taking on the challenge, learning something new in the process, and contributing to a healthier ecosystem. 54 | 55 | ![list of 20 contributors who contributed to the project](https://imgur.com/FvQBKMs.png) 56 | 57 | I also want to give a special shoutout to [Matvey](https://github.com/ronanru) here who implemented a _huge_ amount of codemods single handedly. 58 | 59 | We've currently implemented over 90% of all codemods, with the following module replacements left to implement: 60 | 61 | ![A list of codemods left to be implemented](https://imgur.com/eIVDnUx.png) 62 | 63 | If you're interested in helping out, you can find some instructions on how to get started [here](https://github.com/es-tooling/module-replacements-codemods?tab=readme-ov-file#contributing). If you've never built a codemod before, I challenge you to try it! Take a look at [codemod.studio](https://codemod.com/studio), and I'm sure you'll be up and running in no time. If you're unsure how to get started or get stuck, please feel free to shoot me a [DM](https://x.com/passle_), and we can take a look together. 64 | 65 | ## So what do we do with these codemods? 66 | 67 | For me, the goal of creating these codemods was to simplify the replacement of these packages on a large scale; I want people to be able to run these codemods on their own projects, but also I want everyone to be able to create pull requests to _other_ projects that may be using these dependencies. If people notice any of their dependencies leading to bloated `node_modules`, I want to enable them to fork and clone that project, run the codemods, and create a PR, hopefully speeding up the ecosystem cleanup by a lot. 68 | 69 | In the near future, we'll be looking into implementing a CLI to help with this, so we can easily run these codemods on our projects; I'll create a separate blogpost about that when that happens. 70 | 71 | In conclusion, it's been really great to see the amount of awareness that's been created around these issues, and also to see the amount of people it has resonated with and spurred into action. But we're not there yet! There are still, many, many, _many_ projects that cause bloated `node_modules` folders. I hope this blog finds you, I hope you agree, and I hope you'll do something about it with the tools being created. 72 | -------------------------------------------------------------------------------- /blog/a-cleaner-node-modules-ecosystem/nodemodules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/a-cleaner-node-modules-ecosystem/nodemodules.png -------------------------------------------------------------------------------- /blog/a-cleaner-node-modules-ecosystem/which.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/a-cleaner-node-modules-ecosystem/which.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/74-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/74-modules.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/barrel-begone-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/barrel-begone-1.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/barrel-begone-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/barrel-begone-2.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/barrel-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/barrel-file.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/graphql-barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/graphql-barrel.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Barrel files a case study 3 | description: Untangling MSW 4 | updated: 2024-01-30 5 | --- 6 | 7 | # Barrel files - a case study 8 | 9 | > Let me preface this blogpost with 2 things: 10 | > 11 | > **1:** This blogs intention is not to shame MSW, I'm a maintainer of MSW myself and it's a great project that has to consider lots of different usecases and environments, which is not always easy to do. The reason I'm highlighting MSW in this blog is because I encountered all this while working on it, and so it makes for a good illustrative case study of sorts. 12 | > 13 | > **2:** This is also not an "all barrel files are evil" kind of blog; consider your project, _how_ people may consume it, and apply some critical thinking. Although I will say that I personally find, more often than not, barrel files (and most notably some barrel-file related practices) to be a code smell. This blog is also not a criticism against anyone, it's just me going down a rabbit hole and taking you along for the ride 14 | 15 | A couple of weeks ago I was working on one of our features, and I noticed that an enormous amount of JavaScript files were getting loaded in the browser during local development. 16 | 17 | I set out to investigate, and noticed that out of 184 requests on the page, 179 were caused by `MSW`. For our local development environment, and also for running our unit tests, we don't bundle; we use buildless development, and we get a lot of benefits from this approach. 18 | 19 | Now, it may be easy to jump to: "Just use a bundler", but there are many usecases and environments where its not common to use a bundler, like for example: 20 | 21 | - When loading the library from a CDN 22 | - During local development 23 | - Test runners 24 | - in the browser 25 | - in node processes 26 | - In JavaScript server-side runtime environments 27 | 28 | These are all scenarios and examples of cases where we typically don't (or can't) bundle code, and we don't benefit from treeshaking. Additionally, `MSW` is a (mostly? only?) dev-time library, that we import and consume in our unit test code, not our source code directly; we also don't typically bundle our _unit test code_ before running our unit tests. 29 | 30 | Additionally, barrel files also cause bundlers to slow down, and barrel files also often contain code like `export * from` or `import * as foo`, which bundlers often can't treeshake correctly. 31 | 32 | ## Untangling MSW's module graph 33 | 34 | Knowing a little bit about `MSW`'s internals, and what it does, it felt like 179 modules being loaded was _way_ too much for what it does, so I started looking at what kind of modules are actually being loaded. I noticed pretty quickly that `GraphQL` is a large source of the requests being made, which struck me as curious, because while `MSW` does support mocking `GraphQL` requests, my project doesn't. So why am I still loading all this `GraphQL` related code that's going unused, and slows my development down? 35 | 36 | Thankfully, the browser's network tab has a nifty initiator tab that shows you the import chain for a given import: 37 | 38 | ![initiator](https://i.imgur.com/xmfLY6P.png) 39 | 40 | Starting at the root of the import chain, I pretty quickly discovered the culprit: 41 | 42 | ![barrel-file](https://i.imgur.com/AE6TWPI.png) 43 | 44 | `MSW` ships as a _barrel file_. A barrel file is essentially just an `index` file that re-exports everything else from the package. Which means that if you import _only one thing_ from that barrel file, you end up loaded _everything_ in its module graph. 45 | 46 | So even though in my code I'm only importing: 47 | ```js 48 | import { http } from 'msw'; 49 | import { setupWorker } from 'msw/browser'; 50 | ``` 51 | I still end up loading all the other modules, and the entire module graph, which itself may also contain _other_ barrel files, like for example `GraphQL`: 52 | 53 | ![graphql-barrel](https://i.imgur.com/fbiYG7W.png) 54 | 55 | ## Automating it 56 | 57 | I brought this up in `MSW`'s maintainer channel on their Discord, and had a good discussion about it, and opened an issue on the MSW github repository. The creator of `MSW` and I also had a private chat on Discord and discussed how it would be nice to have some tooling around this, to make it easier for package authors and create more awareness around what their module graph may look like, and highlight some potential issues. 58 | 59 | So I set out to coding, and created [`barrel-begone`](https://www.npmjs.com/package/barrel-begone) and [`eslint-plugin-barrel-files`](https://www.npmjs.com/package/eslint-plugin-barrel-files) which is a little eslint plugin that helps detect some barrel file-related issues during development. 60 | 61 | The `barrel-begone` package does a couple things: 62 | 63 | It scans your packages entrypoints, either via the `"module"`, `"main"` or `"exports"` field in your `package.json`, and then it analyzes your module graph to detect the amount of modules that will be loaded in total by importing this entrypoint, and it also does some analysis on the modules that will be imported by this entrypoint, for example: 64 | 65 | - Detecting barrel files 66 | - Detecting `import *` style imports 67 | - Detecting `export *` style exports 68 | 69 | And pointing them out. Here's what that looks like on the `MSW` project: 70 | 71 | ![barrel-begone-1](https://i.imgur.com/xAYirY2.png) 72 | 73 | From this information, we see that importing from module specifier `'msw'` causes a total of **179 modules** to be loaded, which is pretty much in line with what I noticed earlier on. We also see some other information like: 74 | 75 | - The entrypoint itself is a barrel file 76 | - The entrypoint uses some `export *` style exports 77 | - The entrypoint leads to other modules that import from a barrel file 78 | 79 | There's a lot to unpack, but since `GraphQL` makes up such a significant portion of all modules loaded, I figured I'd hunt that one down first. I found that there's actually only _one_ import for `'graphql'`, so I changed that import from: 80 | 81 | ```js 82 | import { parse } from 'graphql'; 83 | ``` 84 | 85 | To: 86 | 87 | ```js 88 | import { parse } from 'graphql/language/parser.mjs'; 89 | ``` 90 | 91 | And ran `npx barrel-begone` again: 92 | 93 | ![74-modules](https://i.imgur.com/6aUCSH6.png) 94 | 95 | We're down from **179 modules** to **74 modules**. That seems like a pretty significant change already! 96 | 97 | > I didn't think I'd have to spell this out, but Twitter proved otherwise; loading less modules in the browser is _better_ and more _performant_, and _speeds up_ local development. The less you load, the less the browser has to do. 98 | 99 | However, we're still importing `GraphQL`'s parser while we don't even use it. So lets take another step, and create some separate entrypoints. Looking at the `'msw'` barrel file, there's a lot of stuff in there, but it seems reasonable to have separate entrypoints for (likely) the two most commonly imported things: the `http` and `graphql` handlers, so we'll be able to import them like: 100 | 101 | ```js 102 | import { http } from 'msw/http'; 103 | ``` 104 | 105 | Splitting up the entrypoints like this reduces the amount of modules loaded by a lot, because we're no longer importing anything from `GraphQL` which goes unused; the `msw/http` entrypoint leads to a module graph of **32 modules**. However, we're also importing the `msw/browser` entrypoint (because you can't use the one without the other), which itself leads to a module graph of **21 modules**, so in total, we're down from **179 modules** to **53 modules**. 106 | 107 | Next up, `barrel-begone` told me there were a couple of other imports to another barrel file, namely in `@mswjs/interceptors`, lets see what that looks like: 108 | 109 | ![interceptors](https://i.imgur.com/QbYhEL0.png) 110 | 111 | Yep, looks like another barrel file, with only one locally declared function. And it turns out that that in the `msw/http` entrypoint, that one function is the only thing that we need from the `@mswjs/interceptors` package. If we were to remove that from the barrel file as well, that brings us down from **32 modules** to **24 modules**, in total: 112 | 113 | Down from **179 modules** to **45 modules**. That's a pretty significant difference! I didn't run any objective benchmarks, but on my personal macbook this improved the performance of the page load by **67%**. If you're interested in more numbers, I encourage you to take a look at some of your own projects and setups, finding the barrel files in your projects and/or libraries that you use with `barrel-begone` (or other tools), and see how much benefit you get from removing them. 114 | 115 | ## Conclusion 116 | 117 | The github discussion on barrel files is still on-going, and there are some pull requests, but not everything in this blogpost has been implemented yet; all of this was done locally. Some things, like changing the `GraphQL` import may be tricky to do, because `MSW` uses a dual CJS/ESM setup. Hopefully, we can still make a lot of the changes highlighted in this blogpost in the near future though. 118 | 119 | Despite that, I really wanted to make this blogpost to highlight a couple of things. Firstly, when authoring a package it's importing to consider how people _consume_ your package. As mentioned before, this is not always easy (like in the case of `MSW`'s dual CJS/ESM setup), but as a package author you should be conscious of this. _Where_ does your package get used? Does it make sense to ship only a barrel file? Or should you create some more granular, grouped entrypoints? 120 | 121 | Secondly, a lot of the changes highlighted in this blogpost are not rocket science. Many of them were changing one import to another, or simply creating an additional entrypoint. Hopefully the `barrel-begone` tool will prove useful for other people as well to take a look at their entrypoints and give insight in _what_ they are actually shipping to their users. 122 | 123 | And finally, I think this is important because _not_ doing this means death by a thousand cuts. In this case study, I've looked _exclusively_ at `MSW`, and I've done all this testing with just an empty `index.html` that imports `MSW`, but in an actual project you might be loading additional libraries, like testing utilities/helpers and other things as well, do they also have barrel files? All of these things combined add up. 124 | -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/initiator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/initiator.png -------------------------------------------------------------------------------- /blog/barrel-files-a-case-study/interceptors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/barrel-files-a-case-study/interceptors.png -------------------------------------------------------------------------------- /blog/events-are-the-shit/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events are the shit 3 | description: You may not need to reach for a library — give native events a try 4 | updated: 2023-07-26 5 | --- 6 | 7 | # Events are the shit 8 | 9 | Pardon my profanity, there's just no better way to say it. Events are just great. In this blog I'll showcase some cool things that you can achieve with just plain old events. You might not need an expensive or heavy library! Try an event. 10 | 11 | ## Use `EventTarget` 12 | 13 | Did you know you can instantiate `EventTarget`s? 14 | 15 | ```js 16 | const target = new EventTarget(); 17 | 18 | target.dispatchEvent(new Event('foo')); 19 | target.addEventListener('foo', (event) => {}); 20 | ``` 21 | 22 | ## Extend `Event`! 23 | 24 | Did you know you can extend `Event`? 25 | 26 | Instead of creating `CustomEvent`s, you can just extend the `Event` class, and assign data to it, or even implement other methods on it: 27 | 28 | ```js 29 | // Create the event: 30 | class MyEvent extends Event { 31 | constructor(data) { 32 | super('my-event', { bubbles: true, composed: true }); 33 | this.data = data; 34 | } 35 | } 36 | 37 | const target = new EventTarget(); 38 | 39 | // Fire the event: 40 | target.dispatchEvent(new MyEvent({foo: 'bar'})); 41 | 42 | // Catch the event: 43 | target.addEventListener('my-event', ({data}) => { 44 | console.log(data); // { foo: 'bar' } 45 | }); 46 | ``` 47 | 48 | ## Extend `EventTarget`! 49 | 50 | Did you know you can also _extend_ `EventTarget`? 51 | 52 | Here's how you can create a super minimal state manager using events: 53 | 54 | ```js 55 | class StateEvent extends Event { 56 | constructor(state = {}) { 57 | super('state-changed'); 58 | this.state = state; 59 | } 60 | } 61 | 62 | export class State extends EventTarget { 63 | #state; 64 | 65 | constructor(initialState) { 66 | super(); 67 | this.#state = initialState; 68 | } 69 | 70 | setState(state) { 71 | this.#state = typeof state === 'function' ? state(this.#state) : structuredClone(state); 72 | this.dispatchEvent(new StateEvent(this.#state)); 73 | } 74 | 75 | getState() { 76 | return this.#state; 77 | } 78 | } 79 | 80 | export const state = new State({}); 81 | ``` 82 | 83 | And then you can use it like: 84 | 85 | ```js 86 | state.setState({foo: 'bar'}); // #state === {foo: 'bar'} 87 | state.setState((old) => ({...old, bar: 'baz'})); // #state === {foo: 'bar', bar: 'baz'} 88 | 89 | state.addEventListener('state-changed', ({state}) => { 90 | // Assign state, trigger a render, whatever 91 | }); 92 | 93 | state.getState(); // {foo: 'bar', bar: 'baz'}; 94 | ``` 95 | 96 | I use this in my [`@thepassle/app-tools`](https://github.com/thepassle/app-tools/blob/master/state/index.js) library, and it's often all the state management I need. Super tiny, but powerful state manager. 97 | 98 | ## Events are sync 99 | 100 | Did you know events execute synchronously? 101 | 102 | ```js 103 | const target = new EventTarget(); 104 | 105 | console.log('first'); 106 | 107 | target.addEventListener('foo', ({data}) => { 108 | console.log('second'); 109 | }); 110 | 111 | target.dispatchEvent(new Event('foo')); 112 | 113 | console.log('third'); 114 | ``` 115 | 116 | Outputs: 117 | ``` 118 | // first 119 | // second 120 | // third 121 | ``` 122 | 123 | ## Context-like patterns 124 | 125 | It's a common scenario to pass down properties to child components. However, sometimes you end up in a situation known as "prop drilling", where you need to get some property down to a deeply nested child component, and along the way you're passing the property through components that really don't need to know about the property in the first place. In this case, it can sometimes be easier for the child component to request the property from a parent higher up the tree. This is also known as the context pattern. Since events execute synchronously, we can just use the following pattern: 126 | 127 | ```js 128 | class MyParent extends HTMLElement { 129 | theme = 'dark'; 130 | 131 | constructor() { 132 | super(); 133 | /** 134 | * The provider: 135 | */ 136 | this.addEventListener('theme-context', (event) => { 137 | event.theme = this.theme; 138 | }); 139 | } 140 | } 141 | 142 | export class MyChild extends HTMLElement { 143 | connectedCallback() { 144 | const event = new Event('theme-context', { 145 | bubbles: true, 146 | composed: true, 147 | }); 148 | this.dispatchEvent(event); 149 | 150 | /** 151 | * Because events execute synchronously, the callback for `'theme-context'` 152 | * event executes first, and assigns the `theme` to the `event`, which we 153 | * can then access in the child component 154 | */ 155 | console.log(event.theme); // 'dark'; 156 | } 157 | } 158 | ``` 159 | 160 | ## Promise-carrying events 161 | 162 | Did you know events can also carry promises? A great showcase of this pattern is the [`Pending Task Protocol`](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/pending-task.md) by the Web Components Community Group. Now, "Pending Task Protocol" sounds very fancy, but really, it's just an event that carries a promise. 163 | 164 | Consider the following example, we create a new `PendingTaskEvent` class: 165 | 166 | ```js 167 | class PendingTaskEvent extends Event { 168 | constructor(complete) { 169 | super('pending-task', {bubbles: true, composed: true}); 170 | this.complete = complete; 171 | } 172 | } 173 | ``` 174 | 175 | And then in a child component, whenever we do some asynchronous work, we can send a `new PendingTaskEvent` to signal to any parents that a task is pending: 176 | 177 | ```js 178 | class ChildElement extends HTMLElement { 179 | async doWork() { /* ... */ } 180 | 181 | startWork() { 182 | const workComplete = this.doWork(); 183 | this.dispatchEvent(new PendingTaskEvent(workComplete)); 184 | } 185 | } 186 | ``` 187 | 188 | In our parent component we can then catch the event, and show/hide a loading state: 189 | 190 | ```js 191 | class ParentElement extends HTMLElement { 192 | #pendingTaskCount = 0; 193 | 194 | constructor() { 195 | super(); 196 | this.addEventListener('pending-task', async (e) => { 197 | e.stopPropagation(); 198 | if (++this.#pendingTaskCount === 1) { 199 | this.showSpinner(); 200 | } 201 | await e.complete; 202 | if (--this.#pendingTaskCount === 0) { 203 | this.hideSpinner(); 204 | } 205 | }); 206 | } 207 | } 208 | ``` -------------------------------------------------------------------------------- /blog/i-overengineered-my-blog/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: I overengineered my blog 3 | description: I almost titled this "My blog is better than yours" 4 | updated: 2024-01-14 5 | --- 6 | 7 | # I overengineered my blog 8 | 9 | > Alright, so at the time of writing, its not _entirely_ finished. I didn't setup the rss feed yet, I need to do some reworking for the OG stuff, but I can't be bothered to do that right now, as well as some other small things that need doing. 10 | 11 | > You can find the source code [here](https://github.com/thepassle/blog) 12 | 13 | I've been interested in Service Worker templating for a while now, largely inspired by [Jeff Posnick](https://jeffy.info/)'s work. Last year, I explored **[SWSR](/definitions#swsr)** and created a library called **[SWTL](https://github.com/thepassle/swtl)**, which this blog is built with. 14 | 15 | **SWTL** is a Service Worker Templating Language for component-like templating in service workers. With it, you can stream templates to the browser as they're being parsed, and it can be used on the client side (in a, well, service worker), in Node(/Bun/Deno) processes, as well as other serverless, edge, or otherwise worker-like environments. You can read more about **SWTL** [here](/blog/service-worker-templating-language-(swtl)/). 16 | 17 | I strongly believe that when you build a library or tool, you have to eat your own dogfood and actually build some projects with it yourself. I've used **SWTL** for a couple of small projects, as well as an internal project at work, and I've been wanting to build a personal blog for a while now as opposed to hosting all my content on [dev.to](https://dev.to/thepassle) only, so it seemed as good a time as any. 18 | 19 | ## How does it work 20 | 21 | This blog uses isomorphic rendering; on initial request the blog will be served by a Netlify function. Once the page is loaded, the service worker will be installed, and once activated will take care of all other requests. The code that runs on the Netlify function is the same source code that runs in the service worker. 22 | 23 | Here's a simplified example: 24 | 25 | ```js 26 | import { html, Router } from 'swtl'; 27 | import { Html } from './src/Html.js'; 28 | 29 | export const router = new Router({ 30 | routes: [ 31 | { 32 | path: '/', 33 | render: ({url, params, query, request}) => { 34 | const overview = fetch(url.origin + '/output/overview.html'); 35 | 36 | return html` 37 | <${Html} title="Home"> 38 |

Overview

39 | ${overview} 40 | 41 | ` 42 | } 43 | }, 44 | ] 45 | }); 46 | ``` 47 | 48 | ## The content 49 | 50 | My blog content is written using markdown files. I have a node script that turns those markdown files into HTML, and takes care of some other things like highlighting code snippets, and creating an overview of blogs/thoughts. 51 | 52 | Those markdown files are eventually output as static HTML files, which I can then `fetch` in my templates inside the Netlify function/service worker: 53 | 54 | ```js 55 | { 56 | path: '/blog/:title', 57 | render: ({url, params, query, request}) => { 58 | const blog = fetch(url.origin + '/output/blog/' + params.title + '/index.html'); 59 | 60 | return html` 61 | <${Html} title="${title(params.title)}"> 62 |
63 | ${blog} 64 |
65 | 66 | ` 67 | } 68 | }, 69 | ``` 70 | 71 | Very annoyingly, most of the time I spent working on my blog was spent on the `md-to-html.js` script. For rendering my pages, I only have one dependency; **SWTL**. But for preprocessing my markdown files and turning them into HTML, I needed a bunch of dependencies, which I'm not a huge fan of. 72 | 73 | Once I had the script setup, working with it was fairly straightforward. I'm already a big fan of [Astro](https://astro.build/), but writing all this md-to-html logic myself definitely has given me a new appreciation for it. _But_ blogs are to be overengineered, so here we are. 74 | 75 | What's also kind of cool, if not a bit overkill, is that not only am I using **SWTL** on the server and in the service worker, but also in the `md-to-html.js` script itself. 76 | 77 | ```js 78 | writeFileSync('./public/output/overview.html', await renderToString(html``)); 79 | ``` 80 | 81 | ## Future work 82 | 83 | As mentioned, there are a bunch of things to be improved yet, like handling OG information, the RSS feed, and some other small things, but at this point I just want to put something out there and share what I have. Is a blog ever really finished anyway? 84 | 85 | There are also some other fun things to explore, like caching of the blogs. Currently they are preprocessed HTML pages that are `fetch`ed on demand, but since we're operating in a service worker, I should probably make use of caching. Since **SWTL** supports out-of-order streaming, one of the things I played around with is the following: 86 | 87 | ```js 88 | { 89 | path: '/blog/:title', 90 | render: ({url, params}) => { 91 | const blog = fetch(url.origin + '/output/' + params.title + '/index.html'); 92 | 93 | const cachedPromise = ENV === 'worker' 94 | ? caches.open('blogs').then(cache => blog.then(response => cache.put(url, response.clone()))) 95 | : Promise.resolve(); 96 | 97 | return html` 98 | <${Html}> 99 |
${blog}
100 | 101 | ${ENV === 'worker' ? html` 102 | <${Await} promise=${() => cachePromise}> 103 | ${({pending, error, succes}) => html` 104 | ${when(pending, () => html`

Caching this blog...

`)} 105 | ${when(error, () => html`

Failed to cache blog.

`)} 106 | ${when(success, () => html`

Blog cached for offline reading pleasure.

`)} 107 | `} 108 | 109 | `} 110 | 111 | ` 112 | } 113 | } 114 | ``` 115 | 116 | This is cool, because it'll show pending/error/success states on the page without using any client-side JS (other than the service worker itself, of course) 117 | 118 | Anyways, lots more to build and explore with. If you're interested in seeing how this blog was set up, you can take a look at the repository [here](https://github.com/thepassle/blog). -------------------------------------------------------------------------------- /blog/practical-barrel-file-guide-for-library-authors/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Practical barrel file guide for library authors 3 | description: What to do, what not to do 4 | updated: 2024-06-01 5 | --- 6 | 7 | # Practical barrel file guide for library authors 8 | 9 | Over the past couple of months I've been working on a lot on tooling against barrel files. A barrel file is essentially just a big **index.js** file that re-exports everything else from the package. Which means that if you import only one thing from that barrel file, you end up loading everything in its module graph. This has lots of downsides, from slowing down runtime performance to slowing down bundler performance. You can find a good introduction on the topic [here](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/) or a deep dive of the effects of barrel files on a popular real world library [here](/blog/barrel-files-a-case-study). 10 | 11 | In this post, I'll give some practical advice for library authors on how to deal with barrel files in your packages. 12 | 13 | ## Use automated tooling 14 | 15 | First of all, automated tooling can help a lot here. For example, if you want to check if your project has been setup in a barrel-file-free way, you can use [barrel-begone](https://www.npmjs.com/package/barrel-begone): 16 | 17 | ``` 18 | npx barrel-begone 19 | ``` 20 | 21 | `barrel-begone` will analyze **your packages entrypoints**, and analyze your code and warn for various different things: 22 | 23 | - The total amount of modules loaded by importing the entrypoint 24 | - Whether a file is a barrel or not 25 | - Whether export * is used, which leads to poor or no treeshaking 26 | - Whether import * is used, which leads to poor or no treeshaking 27 | - Whether an entrypoint leads to a barrel file somewhere down in your module graph 28 | 29 | 30 | ## Lint against barrel files 31 | 32 | Additionally, you can use linters against barrel files, like for example [eslint-plugin-barrel-files](https://www.npmjs.com/package/eslint-plugin-barrel-files) if you're using ESLint. If you're using tools like `Oxlint` or `Biome`, these rules are already built-in. This eslint plugin warns you against authoring barrel files, but also warns against importing from other barrel files, and helps you avoid using barrel files all around. 33 | 34 | ## Avoid authoring barrel files 35 | 36 | For example, if you author the following file, the eslint plugin will give a warning: 37 | 38 | ```js 39 | // The eslint rule will detect this file as being a barrel file, and warn against it 40 | // It will also provide additional warnings, for example again using namespace re-exports: 41 | export * from './foo.js'; // Error: Avoid namespace re-exports 42 | export { foo, bar, baz } from './bar.js'; 43 | export * from './baz.js'; // Error: Avoid namespace re-exports 44 | ``` 45 | 46 | ## Avoid importing from barrel files 47 | It will also warn you if you, as a library author, are importing something from a barrel file. For example, maybe your library makes use of an external library: 48 | 49 | ```js 50 | import { thing } from 'external-dep-thats-a-barrel-file'; 51 | ``` 52 | 53 | This will give the following warning: 54 | 55 | ``` 56 | The imported module is a barrel file, which leads to importing a module graph of modules 57 | ``` 58 | 59 | If you run into this, you can instead try to find a more specific entrypoint to import `thing` from. If the project has a package exports map, you can consult that to see if there's a more specific import for the thing you're trying to import. Here's an example: 60 | 61 | ```json 62 | { 63 | "name": "external-dep-thats-a-barrel-file", 64 | "exports": { 65 | ".": "./barrel-file.js", // 🚨 66 | "./thing.js": "./lib/thing.js" // ✅ 67 | } 68 | } 69 | ``` 70 | 71 | In this case, we can optimize our module graph size by a lot by just importing form `"external-dep-thats-a-barrel-file/thing.js"` instead! 72 | 73 | If a project does _not_ have a package exports map, that essentially means anything is fair game, and you can try to find the more specific import on the filesystem instead. 74 | 75 | ## Don't expose a barrel file as the entrypoint of your package 76 | 77 | Avoid exposing a barrel file as the entrypoint of your package. Imagine we have a project with some utilities, called `"my-utils-lib"`. You might have authored an `index.js` file that looks like this: 78 | 79 | ```js 80 | export * from './math.js'; 81 | export { debounce, throttle } from './timing.js'; 82 | export * from './analytics.js'; 83 | ``` 84 | 85 | Now if I, as a consumer of your package, only import: 86 | 87 | ```js 88 | import { debounce } from 'my-utils-lib'; 89 | ``` 90 | 91 | I'm _still_ importing everything from the `index.js` file, and everything that comes with it down the line; maybe `analytics.js` makes use of a hefty analytics library that itself imports a bunch of modules. And all I wanted to do was use a `debounce`! Very wasteful. 92 | 93 | ## On treeshaking 94 | 95 | > "But Pascal, wont everything else just get treeshaken by my bundler??" 96 | 97 | First of all, you've incorrectly assumed that everybody uses a bundler for every step of their development or testing workflow. It could also be the case that your consumer is using your library from a CDN, where treeshaking doesn't apply. Additionally, some patterns treeshake poorly, or may not get treeshaken as you might expect them to; because of sideeffects. There may be things in your code that are seen as sideeffectul by bundlers, which will cause things to _not_ get treeshaken. Like for example, did you know that: 98 | 99 | ```js 100 | Math.random().toString().slice(1); 101 | ``` 102 | 103 | Is seen as sideeffectful and might mess with your treeshaking? 104 | 105 | ## Create granular entrypoints for your library 106 | 107 | Instead, provide **granular entrypoints** for your library, with a sensible grouping of functionality. Given our `"my-utils-lib"` library: 108 | 109 | ```js 110 | export * from './math.js'; 111 | export { debounce, throttle } from './timing.js'; 112 | export * from './analytics.js'; 113 | ``` 114 | 115 | We can see that there are separate kinds of functionality exposed: some math-related helpers, some timing-related helpers, and analytics. In this case, we might create the following entrypoints: 116 | 117 | - `my-utils-lib/math.js` 118 | - `my-utils-lib/timing.js` 119 | - `my-utils-lib/analytics.js` 120 | 121 | Now, I, as a consumer of `"my-utils-lib"`, will have to update my import from: 122 | 123 | ```js 124 | import { debounce } from 'my-utils-lib'; 125 | ``` 126 | 127 | to: 128 | ```js 129 | import { debounce } from 'my-utils-lib/timing.js'; 130 | ``` 131 | 132 | Small effort, fully automatable via codemods, and big improvement all around! 133 | 134 | ## How do I add granular entrypoints? 135 | 136 | If you're using package exports for your project, and your main entrypoint is a barrel file, it probably looks something like this: 137 | 138 | ```json 139 | { 140 | "name": "my-utils-lib", 141 | "exports": { 142 | ".": "./index.js" // 🚨 143 | } 144 | } 145 | ``` 146 | 147 | Instead, create entrypoints that look like: 148 | 149 | ```json 150 | { 151 | "name": "my-utils-lib", 152 | "exports": { 153 | "./math.js": "./lib/math.js", // ✅ 154 | "./timing.js": "./lib/timing.js", // ✅ 155 | "./analytics.js": "./lib/analytics.js" // ✅ 156 | } 157 | } 158 | ``` 159 | 160 | ## Alternative: Subpath exports 161 | 162 | Alternatively, you can add subpath exports for your package, something like: 163 | 164 | ```json 165 | { 166 | "name": "my-utils-lib", 167 | "exports": { 168 | "./lib/*": "./lib/*" 169 | } 170 | } 171 | ``` 172 | 173 | This will make anything under `./lib/*` importable for consumers of your package. 174 | 175 | ## A real-life example 176 | 177 | Lets [again](https://thepassle.netlify.app/blog/barrel-files-a-case-study) take a look at the `msw` library as a case study. `msw` exposes a barrel file as the main entrypoint, and it looks something like this: 178 | 179 | > Note: I've omitted the type exports for brevity 180 | 181 | ```js 182 | export { SetupApi } from './SetupApi' 183 | 184 | /* Request handlers */ 185 | export { RequestHandler } from './handlers/RequestHandler' 186 | export { http } from './http' 187 | export { HttpHandler, HttpMethods } from './handlers/HttpHandler' 188 | export { graphql } from './graphql' 189 | export { GraphQLHandler } from './handlers/GraphQLHandler' 190 | 191 | /* Utils */ 192 | export { matchRequestUrl } from './utils/matching/matchRequestUrl' 193 | export * from './utils/handleRequest' 194 | export { getResponse } from './getResponse' 195 | export { cleanUrl } from './utils/url/cleanUrl' 196 | 197 | export * from './HttpResponse' 198 | export * from './delay' 199 | export { bypass } from './bypass' 200 | export { passthrough } from './passthrough' 201 | ``` 202 | 203 | If I'm a consumer of `msw`, I might use `msw` to mock api calls with the `http` function, or `graphql` function, or both. Let's imagine my project doesn't use GraphQL, so I'm only using the `http` function: 204 | 205 | ```js 206 | import { http } from 'msw'; 207 | ``` 208 | 209 | Just importing this `http` function, will import the entire barrel file, including all of the `graphql` project as well, which adds a hefty **123 modules** to our module graph! 210 | 211 | > Note: `msw` has since my last blogpost on the case study also added granular entrypoints for `msw/core/http` and `msw/core/graphql`, but they still expose a barrel file as main entrypoint, which [most](https://github.com/search?q=import+%7B+http+%7D+from+%27msw%27&type=code) of its users actually use. 212 | 213 | Instead of shipping this barrel file, we could **group** certain kinds of functionalities, like for example: 214 | 215 | **http.js**: 216 | ```js 217 | export { HttpHandler, http }; 218 | ``` 219 | 220 | **graphql.js**: 221 | ```js 222 | export { GraphQLHandler, graphql }; 223 | ``` 224 | 225 | **builtins.js**: 226 | ```js 227 | export { bypass, passthrough }; 228 | ``` 229 | 230 | **utils.js**: 231 | ```js 232 | export { cleanUrl, getResponse, matchRequestUrl }; 233 | ``` 234 | 235 | That leaves us with a ratatouille of the following exports, that frankly I'm not sure what to name, so I'll just name those **todo.js** for now (I also think these are actually just types, but they weren't imported via a `type` import): 236 | ```js 237 | export { HttpMethods, RequestHandler, SetupApi }; 238 | ``` 239 | 240 | Our package exports could look something like: 241 | 242 | ```json 243 | { 244 | "name": "msw", 245 | "exports": { 246 | "./http.js": "./http.js", 247 | "./graphql.js": "./graphql.js", 248 | "./builtins.js": "./builtins.js", 249 | "./utils.js": "./utils.js", 250 | "./TODO.js": "./TODO.js" 251 | } 252 | } 253 | ``` 254 | 255 | Now, I, as a consumer of MSW, only have to update my import from: 256 | 257 | ```js 258 | import { http } from 'msw'; 259 | ``` 260 | 261 | to: 262 | 263 | ```js 264 | import { http } from 'msw/http.js'; 265 | ``` 266 | 267 | Very small change, and fully automatable via codemods, but results in a big improvement all around, and no longer imports all of the graphql parser when I'm not even using graphql in my project! 268 | 269 | Now, ofcourse it could be that users are not _only_ importing the `http` function, but also other things from `'msw'`. In this case the user may have to add an additional import or two, but that seems hardly a problem. Additionally, the [evidence](https://github.com/search?q=import+%7B+http+%7D+from+%27msw%27&type=code) seems to suggest that _most_ people will mainly be importing `http` or `graphql` anyway. 270 | 271 | ## Conclusion 272 | 273 | Hopefully this blogpost provides some helpful examples and guidelines for avoiding barrel files in your project. Here's a list of some helpful links for you to explore to learn more: 274 | 275 | - [barrel-begone](https://www.npmjs.com/package/barrel-begone) 276 | - [eslint-plugin-barrel-files](https://www.npmjs.com/package/eslint-plugin-barrel-files) 277 | - [oxlint no-barrel-file](https://oxc-project.github.io/docs/guide/usage/linter/rules.html#:~:text=oxc-,no%2Dbarrel%2Dfile,-oxc) 278 | - [biome noBarrelFile](https://biomejs.dev/linter/rules/no-barrel-file/) 279 | - [Barrel files a case study](/blog/barrel-files-a-case-study) 280 | - [Speeding up the JavaScript ecosystem - The barrel file debacle](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/) -------------------------------------------------------------------------------- /blog/rustify-your-js-tooling/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rustify your js tooling 3 | description: A little overview on how to start using Rust code in your existing JS codebases 4 | updated: 2024-05-21 5 | --- 6 | 7 | # Rustify your js tooling 8 | 9 | A big part of my work revolves around JavaScript tooling, and as such it's important to keep an eye on the ecosystem and see where things are going. It's no secret that recently lots of projects are native-ying (??) parts of their codebase, or even rewriting them to native languages altogether. [Esbuild](https://esbuild.github.io/) is one of the first popular and successful examples of this, which was written in Go. Other examples are [Rspack](https://www.rspack.dev/) and [Turbopack](https://turbo.build/pack), which are both Rust-based alternatives to Webpack, powered by [SWC](https://swc.rs/) ("Speedy Web Compiler"). There's also [Rolldown](https://rolldown.rs/), a Rust-based alternative to Rollup powered by [OXC](https://oxc-project.github.io/) ("The JavaScript Oxidation Compiler"), but [Rollup](https://rollupjs.org/) itself is also native-ying (??) parts of their codebase and recently started using SWC for parts of their codebase. And finally, there are [Oxlint](https://oxc-project.github.io/docs/guide/usage/linter.html) (powered by OXC) and [Biome](https://biomejs.dev/) as Rust-based alternatives for [Eslint](https://eslint.org/) and [Prettier](https://prettier.io/) respectively. 10 | 11 | Having said all that, there definitely seems to be a big push to move lots of JavaScript tooling to Rust, and as mentioned above, as someone working on JavaScript tooling it's good to stick with the times a bit, so I've been keeping a close eye on these developments and learning bits of Rust here and there in my spare time. While I've built a couple of hobby projects with Rust, for a long time I haven't really been able to really apply my Rust knowledge at work, or other tooling projects. One of the big reasons for that was that a lot of the necessary building blocks either simply weren't available yet, or not user-friendly (for a mostly mainly JS dev like your boy) enough. 12 | 13 | That has changed. 14 | 15 | Notably by projects like OXC and [Napi-rs](https://napi.rs/), and these projects combined make for an absolute powerhouse for tooling. A lot of the tooling I work on have to do with some kind of analysis, AST parsing, module graph crawling, codemodding, and other dev tooling related stuff; but a lot of very AST-heavy stuff. OXC provides some really great projects to help with this, and I'll namedrop a few of them here. 16 | 17 | ## Lil bit of namedropping 18 | 19 | Starting off with [**oxc_module_lexer**](https://crates.io/crates/oxc_module_lexer). Admittedly not an actual lexer; it actually does do a full parse of the code, but achieves the same result as the popular `es-module-lexer`, but made very easy to use in Rust. If you're not a dummy like me, you're probably able to get `es-module-lexer` up and running in Rust. For me, it takes days of fiddling around, not knowing what I'm doing, and getting frustrated. I just want to install a crate that works and be on my way and write some code. There is also a fork of `es-module-lexer` made by the creator of Parcel, but it's not actually published on crates.io, and so you have to install it via a Github link, which makes me a bit squeemish in terms of versioning/breakage, so just being able to use `oxc_module_lexer` is really great. Very useful for _lots_ of tooling. Here's a small example: 20 | 21 | ```rust 22 | let allocator = Allocator::default(); 23 | let ret = Parser::new(&allocator, &source, SourceType::default()).parse(); 24 | let ModuleLexer { exports, imports, .. } = ModuleLexer::new().build(&ret.program); 25 | ``` 26 | 27 | Next up there's [**oxc_resolver**](https://crates.io/crates/oxc_resolver) which implements node module resolution in Rust, super useful to have available in Rust: 28 | 29 | ```rust 30 | let options = ResolveOptions { 31 | condition_names: vec!["node".into(), "import".into()], 32 | main_fields: vec!["module".into(), "main".into()], 33 | ..ResolveOptions::default() 34 | }; 35 | 36 | let resolver = Resolver::new(options); 37 | let resolved = resolver.resolve(&importer, importee).unwrap(); 38 | ``` 39 | 40 | And finally [**oxc_parser**](https://crates.io/crates/oxc_parser), which parses JS/TS code and gives you the AST so you can do some AST analysis: 41 | 42 | ```rust 43 | let ret = Parser::new( 44 | Allocator::default(), 45 | &source_code, 46 | SourceType::default() 47 | ).parse(); 48 | 49 | let mut variable_declarations = 0; 50 | for declaration in ret.program.body { 51 | match declaration { 52 | Statement::VariableDeclaration(variable) => { 53 | variable_declarations += variable.declarations.len(); 54 | } 55 | _ => {} 56 | } 57 | } 58 | ``` 59 | 60 | With these things combined, you can already build some pretty powerful (and fast) tooling. However, we still need a way to be able to consume this Rust code on the JavaScript side in Node. That's where Napi-rs comes in. 61 | 62 | ## Using your Rust code in Node 63 | 64 | > "NAPI-RS is a framework for building pre-compiled Node.js addons in Rust." 65 | 66 | Or for dummy's: rust code go brrrr 67 | 68 | Conveniently, Napi-rs provides a starter project that you can find [here](https://github.com/napi-rs/package-template/tree/main), which makes getting setup very easy. I will say however, that the starter project comes with quite a lot of bells and whistles; and the first thing I did was cut a lot of the stuff I didn't absolutely need out. When I'm starting out with a new tool or technology I like to keep things very simple and minimal. 69 | 70 | Alright, let's get to some code. Consider the previous example where we used the **oxc_parser** to count all the top-level variable declarations in a file, our Rust code would look something like this: 71 | 72 | ```rust 73 | use napi::{Env}; 74 | use napi_derive::napi; 75 | use oxc_allocator::Allocator; 76 | use oxc_ast::ast::Statement; 77 | use oxc_parser::Parser; 78 | use oxc_span::SourceType; 79 | 80 | #[napi] 81 | pub fn count_variables(env: Env, source_code: String) -> Result { 82 | let ret = Parser::new( 83 | Allocator::default(), 84 | &source_code, 85 | SourceType::default() 86 | ).parse(); 87 | 88 | let mut variable_declarations = 0; 89 | for declaration in ret.program.body { 90 | match declaration { 91 | Statement::VariableDeclaration(variable) => { 92 | variable_declarations += variable.declarations.len(); 93 | } 94 | _ => {} 95 | } 96 | } 97 | Ok(variable_declarations) 98 | } 99 | ``` 100 | 101 | Even if you've never written Rust, you can probably still tell what this code does and if not, that's okay too because I'm still gonna explain it anyway. 102 | 103 | In this Rust code we create a function that takes some `source_code`, which is just some text. We then create a new `Parser` instance, pass it the `source_code`, and have it parse it. Then, we loop through the AST nodes in the program, and for every `VariableDeclaration`, we add the number of declarations (a variable declaration can have multiple declarations, e.g.: `let foo, bar;`) to `variable_declarations`, and finally we return an `Ok` result. 104 | 105 | And what's cool about Napi is that you don't have to communicate _just_ via strings only; you can pass [many different data types](https://napi.rs/docs/concepts/values) back and forth between Rust and JavaScript, and you can even pass callbacks from JS. Consider the following example: 106 | 107 | ```rust 108 | #[napi] 109 | pub fn foo(env: Env, callback: JsFunction) { 110 | // We call the callback from JavaScript on the Rust side, and pass it a string 111 | let result = callback.call1::( 112 | env.create_string("Hello world".to_str().unwrap())?, 113 | )?; 114 | 115 | // The result of this callback can either be a string or a boolean 116 | match &result.get_type()? { 117 | napi::ValueType::String => { 118 | let js_string: JsString = result.coerce_to_string()?; 119 | // do something with the string 120 | } 121 | napi::ValueType::Boolean => { 122 | let js_bool: JsBoolean = result.coerce_to_bool()?; 123 | // do something with the boolean 124 | } 125 | _ => { 126 | println!("Expected a string or a boolean"); 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | Which we can then use on the JavaScript side like this: 133 | 134 | ```js 135 | import { foo } from './index.js'; 136 | 137 | foo((greeting) => { 138 | console.log(greeting); // "Hello world" 139 | return "Good evening"; 140 | }); 141 | ``` 142 | 143 | This is really cool, because it means you'll be able to create plugin systems for your Rust-based programs with the flexibility of JS/callbacks, which previously seemed like a big hurdle for Rust-based tooling. 144 | 145 | Alright, back to our `count_variables` function. Now that we have the Rust code, we'll want to smurf it into something that we can actually consume on the JavaScript side of things. Since I'm using the napi-rs starter project, I can run the `npm run build` script, and it'll compile the Rust code, and provide me with an `index.d.ts` file that has the types for my `count_variables` function, which looks something like this: 146 | 147 | ```ts 148 | /* tslint:disable */ 149 | /* eslint-disable */ 150 | 151 | /* auto-generated by NAPI-RS */ 152 | 153 | export function countVariables(sourceCode: string): number 154 | ``` 155 | 156 | As well as a generated `index.js` file which actually loads the bindings and exposes the function. The file is pretty long, but at the end of it you'll see this: 157 | 158 | ```ts 159 | // ... etc, other code 160 | 161 | const { countVariables } = nativeBinding 162 | 163 | module.exports.countVariables = countVariables 164 | ``` 165 | 166 | Next up, you can simply create a `run.js` file: 167 | 168 | ```js 169 | import { countVariables } from 'index.js'; 170 | 171 | console.log(countVariables(`let foo, bar;`)); // 2 172 | ``` 173 | 174 | And it Just Works™️. Super easy to write some rust code, and just consume it in your existing JS projects. 175 | 176 | ## Publishing 177 | 178 | Publishing with the Napi-rs starter admittedly wasn't super clear to me, and took some fiddling with to get it working. For starters, you _have_ to use Yarn instead of NPM, otherwise Github Action the starter repo comes with will fail because it's unable to find a yarn.lock file. Im sure it's not rocket science to refactor the Github Action to use NPM instead, but it's a fairly big (~450 loc) `.yml` file and I just want to ship some code. Additionally, if you want to actually _publish_ the code, the commit message has to conform to: `grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"`. This was a bit frustrating to find out because the CI job took about half an hour (although I suspect something was up at Github Actions today making it slower than it actually should be) to reach the `"Publish"` stage, only for me to find out it wouldn't actually publish. 179 | 180 | ## In conclusion 181 | 182 | I think all of these developments are just really cool, which is why I wanted to do a quick blog on it, but the point I wanted to get across here is; Go try this out, clone the Napi-rs starter template, write some Rust, and see if you can integrate something in your existing Node projects, I think you'd be surprised at how well all this works. If I can do it, so can you. Having said that, there are definitely rough edges, and I think there are definitely some things that can be improved to make getting started with this a bit easier, but I'm sure those will come to pass in time; hopefully this blogpost will help someone figure out how to get up and running and play with this as well. 183 | 184 | And finally, a big shoutout to the maintainers of Napi-rs and OXC, they're really cool and friendly people and their projects are super cool. -------------------------------------------------------------------------------- /blog/self-unregistering-service-workers/happy-scenario.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/self-unregistering-service-workers/happy-scenario.jpg -------------------------------------------------------------------------------- /blog/self-unregistering-service-workers/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Self unregistering service workers 3 | description: Pong 4 | updated: 2024-03-26 5 | --- 6 | 7 | # Self unregistering Service Workers 8 | 9 | ## The Problem 10 | 11 | At work, we use microfrontends for our frontend features. These features get deployed to a CDN, and the url for those features look something like this: 12 | 13 | ``` 14 | https://cdn.ing.com/ing-web/2.44.0/es-modules/button.js 15 | ``` 16 | 17 | The way this works is that features have full autonomy of their project and their dependencies when they are deployed. This has really helped us scale our already very large monolithic application by decentralizing the release cycle of our feature teams, while at the same time reducing the friction of the application's release process. 18 | 19 | Imports to dependencies in a features source code get rewritten to a versioned URL of the dependency. Consider the following example: 20 | 21 | ``` 22 | feature-a@1.0.0 23 | ├─ ing-web@2.44.0 24 | ├─ ing-lib-ow@4.0.0 25 | ``` 26 | 27 | **Feature-a** is the feature teams project, and it has two dependencies: `ing-web@2.44.0` and **`ing-lib-ow@4.0.0`**. At buildtime, the imports for those dependencies will get rewritten so that they'll get loaded from: 28 | 29 | ``` 30 | https://cdn.ing.com/ing-web/2.44.0/es-modules/index.js 31 | https://cdn.ing.com/ing-lib-ow/4.0.0/es-modules/index.js 32 | ``` 33 | 34 | This is nice, because **feature-a** is totally in control of their project and dependencies. However, this leads to a problem when features come together in apps. Imagine if we have many different features in the app, **feature-a**, **feature-b**, **feature-c**, and they all depend on a different version of ing-web: 35 | 36 | ``` 37 | feature-a@1.0.0 38 | ├─ ing-web@2.40.0 39 | ├─ ing-lib-ow@4.0.0 40 | 41 | feature-b@1.0.0 42 | ├─ ing-web@2.41.0 43 | 44 | feature-c@1.0.0 45 | ├─ ing-web@2.41.1 46 | ``` 47 | 48 | The problem here is that the user visiting the app will download ing-web three times: version 2.40.0, version 2.41.0 and version 2.41.1. You can probably see why this is an issue; This is terrible for performance. 49 | 50 | ## A potential solution 51 | 52 | To combat this, I was tinkering with a service worker that simply rewrites the URL whenever a request is done to the CDN. Given the following request: 53 | 54 | ``` 55 | https://cdn.ing.com/ing-web/2.41.0/es-modules/button.js 56 | ``` 57 | 58 | It will get rewritten to: 59 | 60 | ``` 61 | https://cdn.ing.com/ing-web/2.44.0/es-modules/button.js 62 | ``` 63 | 64 | Given that we use semver, we can expect there not to be breaking changes in the entrypoints of those projects. This does however mean that teams lose a bit of autonomy, as common, shared dependencies (like the design system) will now get deduped on the application level, and the application dictates which version of the design system is supported. I think this is a fair trade-off to make, given the performance implications. 65 | 66 | The service worker I implemented only does one thing; 67 | - If a CDN request comes in 68 | - If that request is for one of the packages we want to dedupe 69 | - Rewrite the request url to the version dictated by the app 70 | 71 | Pretty straightforward. Here's some code: 72 | 73 | ```js 74 | const packages = { 75 | 'ing-web': { 76 | '2': '2.44.0', 77 | }, 78 | }; 79 | 80 | self.addEventListener('fetch', event => { 81 | const url = new URL(event.request.url); 82 | 83 | if (url.host.includes('cdn.ing.com')) { 84 | const [pkg, requestedVersion, _, file] = url.pathname.split('/').filter(Boolean); 85 | const [requestedMajor] = requestedVersion.split('.'); 86 | 87 | for (const [packageName, versions] of Object.entries(packages)) { 88 | if (packageName === pkg) { 89 | for (const [major, rewriteTo] of Object.entries(versions)) { 90 | if (major === requestedMajor && requestedVersion !== rewriteTo) { 91 | url.pathname = url.pathname.replace(requestedVersion, rewriteTo); 92 | return event.respondWith(fetch(url)); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | }); 99 | 100 | self.addEventListener('install', () => { 101 | self.skipWaiting(); 102 | }); 103 | 104 | self.addEventListener('activate', event => { 105 | event.waitUntil(clients.claim()); 106 | }); 107 | 108 | ``` 109 | 110 | And in the `index.html` of our app, we register the service worker: 111 | ```html 112 | 115 | ``` 116 | 117 | This is nice, because the service worker is essentially a performance improving progressive enhancement; if we'd remove it, we just fall back to the default behavior, and everything still works. 118 | 119 | ## So what are you getting at? 120 | 121 | So far so good. So what's actually the problem here? Well, my colleague [Kristjan Oddsson](https://twitter.com/koddsson) mentioned this thing to me once: 122 | 123 | > It's always good to have an exit strategy for any code or library that you add 124 | 125 | And so I was considering the impact of the service worker. I've used service workers quite a bit and I'd say I'm fairly well-versed with them, but in a large production application it can be a bit scary. What if something goes wrong? What if the service worker is buggy? If we ever want to get rid of the service worker, **what is our exit strategy**? 126 | 127 | Doing some homework (googling and asking chatgpt), quickly leads to people recommending to just deploy a noop service worker, something like: 128 | 129 | ```js 130 | self.addEventListener('install', () => { 131 | self.skipWaiting(); 132 | }); 133 | 134 | self.addEventListener('activate', () => { 135 | self.clients.matchAll({ 136 | type: 'window' 137 | }).then(windowClients => { 138 | windowClients.forEach((windowClient) => { 139 | windowClient.navigate(windowClient.url); 140 | }); 141 | }); 142 | }); 143 | ``` 144 | 145 | Another way of doing this, is just by calling the unregister method: 146 | ```html 147 | 150 | ``` 151 | 152 | These options work well for the following scenario: 153 | 154 | ![happy scenario](https://i.imgur.com/8M0yXCf.jpeg) 155 | 156 | In this scenario, version 1.0.0 of the app deploys with a `navigator.serviceWorker.register('./sw.js')` call. The user visits version 1.0.0 of the app, so the service worker gets installed. Then in version 2.0.0 of the app, the service worker is removed via `navigator.serviceWorker.unregister()`. The user visits the app again when the app is on version 2.2.0, which contains the unregistration of the service worker, and so the user no longer has the service worker; the service worker is removed successfully. 157 | 158 | However, consider the following scenario: 159 | 160 | ![unhappy scenario](https://i.imgur.com/eRxQpOy.jpeg) 161 | 162 | In this scenario, the user visits version 1.1.0 of the app, which contains the service worker registration; the service worker gets installed. In version 2.0.0 the app removes the service worker registration, and starts calling `unregister()`. The user does not visit the app during this time. Then in version 3.0.0, the call to `navigator.serviceWorker.unregister()` is removed from the code. The user only visits the app again on version 3.1.0, which no longer has the unregister call in the code, meaning that the user will still have the service worker they got installed on version 1.1.0; the service worker is not removed successfully. 163 | 164 | So the question here is: when can you ever get rid of that code? When can you ever be sure that all your users have the noop service worker installed? And... then what happens to the noop service worker? Do they just have that installed forever? One way to achieve this is by basing it on analytics, but I wondered if there wasn't a different way of achieving this. For example, what if the service worker _itself_ would have some kind of keepalive check built-in? 165 | 166 | Here's the gist of it: 167 | - On every `fetch` request, the service worker sends a debounced message to the `index.html`; a keepalive check 168 | - If the `index.html` responds to that message, the service worker will stay alive 169 | - If the `index.html` does _not_ respond to that message, the service worker will unregister itself 170 | 171 | Here's an example: 172 | ```html 173 | 180 | ``` 181 | 182 | ```js 183 | function checkPong(pong, interval = 500, maxInterval = 4000) { 184 | setTimeout(() => { 185 | if (!pong()) { 186 | if (interval < maxInterval) { 187 | console.log('[Deduping SW]: Pong not received. Checking again in', interval * 2, 'ms.'); 188 | checkPong(pong, interval * 2); 189 | } else { 190 | console.log('[Deduping SW]: Unregistering.'); 191 | self.registration.unregister(); 192 | } 193 | } else { 194 | console.log('[Deduping SW]: Pong received.'); 195 | } 196 | }, interval); 197 | } 198 | 199 | async function _keepaliveCheck(clientId) { 200 | const channel = new MessageChannel(); 201 | let pong = false; 202 | 203 | channel.port1.onmessage = event => { 204 | if (event.data === 'pong') { 205 | pong = true; 206 | } 207 | }; 208 | 209 | const client = await clients.get(clientId); 210 | if (client) { 211 | client.postMessage('ping', [channel.port2]); 212 | console.log('[Deduping SW]: Ping.'); 213 | 214 | checkPong(() => pong); 215 | } 216 | } 217 | 218 | const keepaliveCheck = debounce(_keepaliveCheck, 2000); 219 | 220 | self.addEventListener('fetch', event => { 221 | keepaliveCheck(event.clientId); 222 | // other SW-ey code 223 | }); 224 | ``` 225 | 226 | This way, if we ever want to get rid of the service worker, we can just remove the following code from our `index.html`: 227 | 228 | ```html 229 | 236 | ``` 237 | 238 | Then, on the next keepalive check from the service worker, it will no longer get an answer from the `index.html`, and unregister itself; effectively ensuring that eventually every service worker will get unregistered, and we don't have any lingering code in our codebase because someone might still have a service worker installed. 239 | 240 | Is this crazy? Maybe! Let me know on [twitter](https://twitter.com/passle_) or [mastodon](https://mastodon.social/@passle), because I'd love to hear some other thoughts about this! -------------------------------------------------------------------------------- /blog/self-unregistering-service-workers/unhappy-scenario.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thepassle/blog/71934df99320a093491e19c6a701bb57af029ada/blog/self-unregistering-service-workers/unhappy-scenario.jpg -------------------------------------------------------------------------------- /blog/service-worker-templating-language-(swtl)/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Service Worker Templating Language (SWTL) 3 | description: Component-like templating in service workers, or other worker-like environments 4 | updated: 2023-08-19 5 | --- 6 | 7 | # Service Worker Templating Language (SWTL) 8 | 9 | > Check out the starter project [here](https://github.com/thepassle/swtl-starter). 10 | 11 | I've previously written about Service Worker Side Rendering (**SWSR**) in this [blog](https://dev.to/thepassle/service-worker-side-rendering-swsr-cb1), when I was exploring running [Astro](https://github.com/withastro/astro) in a Service Worker. 12 | 13 | I recently had a usecase for a small app at work and I just kind of defaulted to a SPA. At some point I realized I needed a Service Worker for my app, and I figured, why not have the entire app rendered by the Service Worker? All I need to do was fetch some data, some light interactivity that I don't need a library or framework for, and stitch some html partials together based on that data. If I did that in a Service Worker, I could stream in the html as well. 14 | 15 | While I was able to achieve this fairly easily, the developer experience of manually stitching strings together wasnt great. Being myself a fan of buildless libraries, such as [htm](https://github.com/developit/htm) and [lit-html](https://github.com/lit/lit), I figured I'd try to take a stab at implementing a DSL for component-like templating in Service Workers myself, called [Service Worker Templating Language](https://github.com/thepassle/swtl) (**SWTL**), here's what it looks like: 16 | 17 | ```js 18 | import { html, Router } from 'swtl'; 19 | import { BreadCrumbs } from './BreadCrumbs.js' 20 | 21 | function HtmlPage({children, title}) { 22 | return html`${title}${children}`; 23 | } 24 | 25 | function Footer() { 26 | return html`
Copyright
`; 27 | } 28 | 29 | const router = new Router({ 30 | routes: [ 31 | { 32 | path: '/', 33 | render: ({params, query, request}) => html` 34 | <${HtmlPage} title="Home"> 35 |

Home

36 | 39 | ${fetch('./some-partial.html')} 40 | ${caches.match('./another-partial.html')} 41 |
    42 | ${['foo', 'bar', 'baz'].map(i => html`
  • ${i}
  • `)} 43 |
44 | <${Footer}/> 45 | 46 | ` 47 | }, 48 | ] 49 | }); 50 | 51 | self.addEventListener('fetch', (event) => { 52 | if (event.request.mode === 'navigate') { 53 | event.respondWith(router.handleRequest(event.request)); 54 | } 55 | }); 56 | ``` 57 | 58 | ## `html` 59 | 60 | To create this DSL, I'm using Tagged Template Literals. For those of you who are not familiar with them, here's what they look like: 61 | 62 | ```js 63 | function html(statics, ...dynamics) { 64 | console.log(statics); 65 | console.log(dynamics); 66 | } 67 | 68 | html`hello ${1} world`; 69 | 70 | // ["hello ", " world"]; 71 | // [1] 72 | ``` 73 | 74 | A Tagged Template Literal gets passed an array of static values (string), and an array of dynamic values (expressions). Based on those strings and expressions, I can parse the result and add support for reusable components. 75 | 76 | I figured that since I'm doing this in a Service Worker, I'm only creating html responses and not doing any kind of diffing, I should be able to just return a stitched-together array of values, and components. Based on `preact/htm`'s component syntax, I built something like this: 77 | 78 | ```js 79 | function Foo() { 80 | return html`

foo

`; 81 | } 82 | 83 | const world = 'world'; 84 | 85 | const template = html`

Hello ${world}

<${Foo}/>`; 86 | 87 | // ['

Hello ', 'world', '

', { fn: Foo, children: [], properties: []}] 88 | ``` 89 | 90 | I can then create a `render` function to serialize the results and stream the html to the browser: 91 | 92 | ```js 93 | /** 94 | * `render` is also a generator function that takes care of stringifying values 95 | * and actually calling the component functions so their html gets rendered too 96 | */ 97 | const iterator = render(html`hello ${1} world`); 98 | const encoder = new TextEncoder(); 99 | 100 | const stream = new ReadableStream({ 101 | async pull(controller) { 102 | const { value, done } = await iterator.next(); 103 | if (done) { 104 | controller.close(); 105 | } else { 106 | controller.enqueue(encoder.encode(value)); 107 | } 108 | } 109 | }); 110 | 111 | /** 112 | * Will stream the response to the browser as results are coming 113 | * in from our iterable 114 | */ 115 | new Response(stream); 116 | ``` 117 | 118 | However, I then realized that since I'm streaming the html anyways, instead of waiting for a template to be parsed entirely and return an array, why not stream the templates _as they are being parsed_? Consider the following example: 119 | 120 | ```js 121 | function* html(statics, ...dynamics) { 122 | for(let i = 0; i < statics.length; i++) { 123 | yield statics[i]; 124 | if (dynamics[i]) { 125 | yield dynamics[i]; 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | Using a generator function, we can yield results as we encounter them, and stream those results to the browser immediately. We can then iterate over the template results: 132 | 133 | ```js 134 | const template = html`hello ${1} world`; 135 | 136 | for (const chunk of template) { 137 | console.log(chunk); 138 | } 139 | 140 | // "hello " 141 | // 1 142 | // " world" 143 | ``` 144 | 145 | 146 | What makes this even cooler is that we can provide first class support for other streamable things, like iterables: 147 | 148 | ```js 149 | function* generator() { 150 | yield* html`
  • 1
  • `; 151 | yield* html`
  • 2
  • `; 152 | } 153 | 154 | html`
      ${generator()}
    `; 155 | ``` 156 | 157 | Or other streams, or `Response`s: 158 | ```js 159 | html` 160 | ${fetch('./some-html.html')} 161 | ${caches.match('./some-html.html')} 162 | `; 163 | ``` 164 | 165 | ### Why not do this at build time? 166 | 167 | The following template: 168 | ```js 169 | const template = html`

    hi

    <${Foo} prop=${1}>bar` 170 | ``` 171 | 172 | Would compile to something like: 173 | ```js 174 | const template = ['

    hi

    ', {fn: Foo, properties: [{name: 'prop', value: 1}], children: ['bar']}]; 175 | ``` 176 | 177 | While admittedly that would save a little runtime overhead, it would increase the bundlesize of the service worker itself. Considering the fact that templates are streamed _while_ they are being parsed, I'm not convinced pre-compiling templates would actually result in a noticeable difference. 178 | 179 | Also I'm a big fan of [buildless development](https://dev.to/thepassle/the-cost-of-convenience-kco), and libraries like [lit-html](https://github.com/lit/lit) and [preact/htm](https://github.com/developit/htm), and the bundlesize for the `html` function itself is small enough: 180 | 181 | ![Minified code for the html function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0bvub78wvu99e2p27oi.png) 182 | 183 | ## Isomorphic rendering 184 | 185 | While I'm using this library in a Service Worker only, similar to a SPA approach, you can also use this library for isomorphic rendering in worker-like environments, or even just on any node-like JS runtime, _and_ the browser! The following code will work in any kind of environment: 186 | 187 | ```js 188 | function Foo() { 189 | return html`

    hi

    `; 190 | } 191 | 192 | const template = html`
    <${Foo}/>
    `; 193 | 194 | const result = await renderToString(template); 195 | //

    hi

    196 | ``` 197 | 198 | Hurray for agnostic libraries! 199 | 200 | ## Router 201 | 202 | I also implemented a simple router based on [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) so you can easily configure your apps routes: 203 | 204 | ```js 205 | import { Router, html } from 'swtl'; 206 | 207 | const router = new Router({ 208 | routes: [ 209 | { 210 | path: '/', 211 | render: () => html`<${HtmlPage}>

    Home

    ` 212 | }, 213 | { 214 | path: '/users/:id', 215 | render: ({params}) => html`<${HtmlPage}>

    User: ${params.id}

    ` 216 | }, 217 | { 218 | path: '/foo', 219 | render: ({params, query, request}) => html`<${HtmlPage}>

    ${request.url.pathname}

    ` 220 | }, 221 | ] 222 | }); 223 | 224 | self.addEventListener('fetch', (event) => { 225 | if (event.request.mode === 'navigate') { 226 | event.respondWith(router.handleRequest(event.request)); 227 | } 228 | }); 229 | ``` 230 | 231 | ## Out of order streaming 232 | 233 | I also wanted to try and take a stab at out of order streaming, for cases where you may need to fetch some data. While you could do something like this: 234 | 235 | ```js 236 | async function SomeComponent() { 237 | try { 238 | const data = await fetch('/api/foo').then(r => r.json()); 239 | return html` 240 |
      241 | ${data.map(user => html` 242 |
    • ${user.name}
    • 243 | `)} 244 |
    245 | `; 246 | } catch { 247 | return html`Failed to fetch data.`; 248 | } 249 | } 250 | ``` 251 | 252 | This would make the api call blocking and stop streaming html until the api call resolves, and we can't really show a loading state. Instead, we ship a special `<${Await}/>` component that takes an asynchronous `promise` function to enable out of order streaming. 253 | 254 | ```js 255 | import { Await, when, html } from 'swtl'; 256 | 257 | html` 258 | <${Await} promise=${() => fetch('/api/foo').then(r => r.json())}> 259 | ${({pending, error, success}, data, error) => html` 260 |

    Fetching data

    261 | ${when(pending, () => html`<${Spinner}/>`)} 262 | ${when(error, () => html`Failed to fetch data.`)} 263 | ${when(success, () => html` 264 |
      265 | ${data.map(user => html` 266 |
    • ${user.name}
    • 267 | `)} 268 |
    269 | `)} 270 | `} 271 | 272 | `; 273 | ``` 274 | 275 | When an `Await` component is encountered, it kicks off the `promise` that is provided to it, and immediately stream/render the `pending` state, and continues streaming the rest of the document. When the rest of the document is has finished streaming to the browser, we await all the promises in order of resolution (the promise that resolves first gets handled first), and replace the `pending` result with either the `error` or `success` template, based on the result of the `promise`. 276 | 277 | So considering the following code: 278 | 279 | ```js 280 | html` 281 | <${HtmlPage}> 282 |

    home

    283 |
      284 |
    • 285 | <${Await} promise=${() => new Promise(r => setTimeout(() => r({foo:'foo'}), 3000))}> 286 | ${({pending, error, success}, data) => html` 287 | ${when(pending, () => html`[PENDING] slow`)} 288 | ${when(error, () => html`[ERROR] slow`)} 289 | ${when(success, () => html`[RESOLVED] slow`)} 290 | `} 291 | 292 |
    • 293 |
    • 294 | <${Await} promise=${() => new Promise(r => setTimeout(() => r({bar:'bar'}), 1500))}> 295 | ${({pending, error, success}, data) => html` 296 | ${when(pending, () => html`[PENDING] fast`)} 297 | ${when(error, () => html`[ERROR] fast`)} 298 | ${when(success, () => html`[RESOLVED] fast`)} 299 | `} 300 | 301 |
    • 302 |
    303 |

    footer

    304 | 305 | `; 306 | ``` 307 | 308 | This is the result: 309 | 310 | ![loading states are rendered first, while continuing streaming the rest of the document. Once the promises resolve, the content is updated in-place with the success state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xdynf41uqwfymb4b0rki.gif) 311 | 312 | We can see that the entire document is streamed, initially displaying loading states. Then, once the promises resolve, the content is updated in-place to display the success state. 313 | 314 | ## Wrapping up 315 | 316 | I've published an initial version of [`swtl`](https://github.com/thepassle/swtl) to NPM, and so far it seems to hold up pretty well for my app, but please let me know if you run into any bugs or issues! Lets make it better together 🙂 317 | 318 | You can also check out the starter project [here](https://github.com/thepassle/swtl-starter). 319 | 320 | ## Acknowledgements 321 | 322 | - [lit-html](https://github.com/lit/lit) 323 | - [preact/htm](https://github.com/developit/htm) 324 | - [Astro](https://github.com/withastro/astro) and [Matthew Philips](https://twitter.com/matthewcp) - For doing the hard work of implementing the rendering logic back when I [requested](https://github.com/withastro/astro/pull/4832) this in astro 325 | - [Artem Zakharchenko](https://twitter.com/kettanaito) - For helping with the handling of first-resolve-first-serve promises 326 | - [Alvar Lagerlöf](https://twitter.com/alvarlagerlof) - For a cool demo of out of order streaming which largely influenced my implementation 327 | 328 | And it's also good to mention that, while working/tweeting about some of the work in progress updates of this project, it seems like many other people had similar ideas and even similar implementations as well! It's always cool to see different people converging on the same idea 🙂 329 | -------------------------------------------------------------------------------- /blog/the-cost-of-convenience/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The cost of convenience 3 | description: Developer conveniences and long term maintainability 4 | updated: 2023-05-04 5 | --- 6 | 7 | # The cost of convenience 8 | 9 | Something I see happen time and time again in frontend development is developers reaching for _conveniences_. It may be the hot new tool in town, it may be existing tooling, but developers just love to take shortcuts. And this is fine! We all love to spend our time being productive, right? We'll just add this one little convenience that will surely make our lives better, never think about it again and move on. 10 | 11 | ## The conveniences 12 | 13 | When we talk about _conveniences_, we generally talk about non-standardisms or any kind of magical behavior. A useful rule of thumb is to ask: "Does it run natively in a browser?". A good example of this are imports, which often get subjected to magical behavior and transformed into non-standardisms, like the following examples: 14 | 15 | ```js 16 | import icon from './icon.svg'; 17 | import data from './data.json'; 18 | import styles from './styles.css'; 19 | import whatever from '~/whatever.js'; 20 | import transformed from 'transform:whatever.js'; 21 | ``` 22 | 23 | None of these examples will actually work in a browser, because they are _non-standard_. Some of you might have correctly spotted that a browser standard exists for two of the imports pointed out in the example, namely the [Import Attributes proposal](https://github.com/tc39/proposal-import-attributes) (previously known as Import Assertions), but these imports in their current shape will not work natively in a browser. Many of these non-standard imports exist for good reason; they are very convenient. 24 | 25 | Other conveniences may include any kind of build-time transformations; like for example JSX. Nobody wants to write `createElement` calls by hand, but JSX will not run in a browser as is. The same goes for TypeScript. Developers evidently are very happy writing types in their source code (and will get [very upset](https://dev.to/thepassle/using-typescript-without-compilation-3ko4) when you tell them you can use TypeScript _without_ compiling your code), but alas, TypeScript source code does not run natively in the browser. Not until the [Type Annotations proposal](https://tc39.es/proposal-type-annotations/) lands anyway, which is only stage 1 at the time of writing. 26 | 27 | Using Node.js globals like `process.env` to enable special development-time logging is another convenience often added by libraries, but will also cause runtime errors in the browser when not specifically handled by adding additional tooling. 28 | 29 | It is important to note that this is not to say that anything that doesn't run natively in the browser is _wrong_ or _bad_. Exactly the opposite is true; It is _convenient_. Keep reading. It's also important to note that these are only _some_ examples of conveniences. 30 | 31 | 32 | ## The cost 33 | 34 | The problem with conveniences is that they come at a cost. How easy is it to add this one little convenience that will surely make our lives better, never think about it again and move on. 35 | 36 | It should come as no surprise to anybody that the frontend tooling ecosystem is complex, and in large part stems from the conveniences developers insist upon. When we apply a convenience to one tool, we have to now also make sure all of our _other_ tooling plays nice with it, and enforce everything to also support anything. Whenever _new_ tooling comes about, it is then also pressured into supporting these conveniences for it to ever be able to catch on, and to ease migrations to it. 37 | 38 | But more concerningly, we become so accustomed to our toolchains and conveniences, that it often leads to _assumptions_ of other people using the same toolchain as you do. Which then leads to packages _containing_ conveniences to be published to the NPM registry. This, as you might have guessed, forces other projects to also adopt additional tooling to be able to support these packages that they want to use, and cause a never-ending spiral of imposed conveniences. 39 | 40 | It is disappointing to see that we have still not learned from the mistakes made by the recently sunsetted starter kit of one of the most popular JavaScript frameworks over the past decade, when contemporary popular development tools are making the exact same mistakes as the one before it by enabling conveniences _out of the box_. 41 | 42 | Where you apply conveniences, you apply lock in. What happens when the next, faster tool comes out? Will it support all of your conveniences? 43 | 44 | ## The conclusion 45 | 46 | Is this all to say that conveniences are bad? Are they not helpful, and not valuable? Should our tooling not be allowed to support conveniences? Should they be shunned, or should tooling not support them? Are they simply _wrong_? The answer is no. Conveniences have their merits and are undeniably valuable. But they come at a cost, and tools should absolutely not enable them _by default_. 47 | 48 | Additionally, when you build for the lowest common denominator and don't rely on conveniences, your code will work anywhere. It ensures compatibility with new tools and web standards, eliminating conflicts. This practice is sometimes referred to as _buildless development_. While it may seem unnecessary to prioritize portability, the importance of this approach becomes evident when circumstances change; you might be right, until you're not. 49 | 50 | And finally, you might actually find it _refreshing_ to try frontend development without all the bells and whistles. Maybe you'll find that buildless development can get you pretty far, you'll often really only need a dev server with node resolution to resolve bare module specifiers, if at all. Maybe try it out some time. -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | export const ENV = !!globalThis?.Netlify ? 'server' : 'worker'; -------------------------------------------------------------------------------- /md-to-html.js: -------------------------------------------------------------------------------- 1 | import { Marked } from 'marked'; 2 | import puppeteer from 'puppeteer'; 3 | import { markedHighlight } from 'marked-highlight'; 4 | import { getHighlighter } from 'shikiji' 5 | import fm from 'front-matter'; 6 | import { html, renderToString } from 'swtl'; 7 | import { 8 | readFileSync, 9 | writeFileSync, 10 | readdirSync, 11 | lstatSync, 12 | mkdirSync, 13 | existsSync 14 | } from 'fs'; 15 | 16 | /** 17 | * @TODO 18 | * - OG image: https://og-playground.vercel.app/ 19 | */ 20 | 21 | const highlighter = await getHighlighter({ 22 | themes: ['nord'], 23 | langs: ['javascript', 'json', 'html', 'rust', 'ts'], 24 | }); 25 | 26 | const allPosts = []; 27 | 28 | function createParser(overview, kind) { 29 | const marked = new Marked( 30 | /** 31 | * Code snippet syntax highlighting 32 | */ 33 | markedHighlight({ 34 | async: true, 35 | highlight(code, lang) { 36 | const language = lang === 'js' ? 'javascript' : lang; 37 | 38 | return highlighter.codeToHtml(code, { 39 | lang: language, 40 | theme: 'nord', 41 | }); 42 | } 43 | }) 44 | ); 45 | 46 | 47 | marked.use({ 48 | renderer: { 49 | /** 50 | * Turn headings into anchor links 51 | */ 52 | heading(text, level) { 53 | const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); 54 | 55 | return ` 56 | 57 | ${text} 58 | 59 | `; 60 | } 61 | }, 62 | hooks: { 63 | /** 64 | * Handle frontmatter 65 | */ 66 | preprocess: (markdown) => { 67 | const { attributes: { title, description, updated }, body } = fm(markdown); 68 | 69 | allPosts.push({title, description, updated, kind}); 70 | overview.push({title, description, updated, kind}); 71 | 72 | return body; 73 | } 74 | } 75 | }); 76 | 77 | 78 | return marked; 79 | } 80 | 81 | for (const dir of [ 82 | './public/output', 83 | './public/output/og', 84 | './public/output/blog', 85 | './public/output/thoughts' 86 | ]) { 87 | if (!existsSync(dir)) { 88 | mkdirSync(dir); 89 | } 90 | } 91 | 92 | const posts = { 93 | thoughts: [], 94 | blog: [], 95 | } 96 | 97 | for (const kind of ['blog', 'thoughts']) { 98 | const parser = createParser(posts[kind], kind); 99 | const dirs = readdirSync(kind); 100 | 101 | for (const dir of dirs) { 102 | const path = `./${kind}/${dir}`; 103 | 104 | if (lstatSync(path).isDirectory()) { 105 | const path = `./${kind}/${dir}/index.md`; 106 | const blog = readFileSync(path, 'utf8'); 107 | const blogAsHtml = await parser.parse(blog); 108 | 109 | const blogOutputDir = `./public/output/${kind}/${dir}`; 110 | if (!existsSync(blogOutputDir)) { 111 | mkdirSync(blogOutputDir); 112 | } 113 | 114 | writeFileSync(`./public/output/${kind}/${dir}/index.html`, blogAsHtml); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Build overview list 121 | */ 122 | 123 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); 124 | 125 | function createHtml({title, description, updated, kind}) { 126 | const date = new Date(updated); 127 | return html` 128 |
  • 129 | 130 |
    131 |

    ${title}

    132 |

    ${date.toISOString().split('T')[0]}, ${capitalize(kind === 'thoughts' ? 'thought' : 'blog')}

    133 |

    ${description}

    134 |
    135 |
    136 |
  • 137 | `; 138 | } 139 | 140 | const sort = (a, b) => new Date(b.updated) - new Date(a.updated); 141 | 142 | const overview = [...posts.thoughts, ...posts.blog] 143 | .sort(sort) 144 | .map(createHtml); 145 | const blogsOverview = posts.blog 146 | .sort(sort) 147 | .map(createHtml); 148 | const thoughtsOverview = posts.thoughts 149 | .sort(sort) 150 | .map(createHtml); 151 | 152 | writeFileSync('./public/output/overview.html', await renderToString(html`
      ${overview}
    `)); 153 | writeFileSync('./public/output/blog/overview.html', await renderToString(html`
      ${blogsOverview}
    `)); 154 | writeFileSync('./public/output/thoughts/overview.html', await renderToString(html`
      ${thoughtsOverview}
    `)); 155 | 156 | /** 157 | * Create OG images 158 | */ 159 | 160 | const browser = await puppeteer.launch({ 161 | headless: true, 162 | }); 163 | 164 | for (const post of [...posts.thoughts, ...posts.blog]) { 165 | const page = await browser.newPage(); 166 | await page.setViewport({ 167 | width: 1200, 168 | height: 630, 169 | }); 170 | 171 | await page.setContent(await renderToString(html` 172 | 173 | 174 | 217 | 218 | 219 |
    220 |

    PASSLE

    221 |

    ${post.title}

    222 |

    ${post.description}

    223 |
    224 | 225 | 226 | `), { 227 | waitUntil: 'networkidle0', 228 | }); 229 | 230 | const screenshotBuffer = await page.screenshot({ 231 | fullPage: false, 232 | type: 'png', 233 | path: `./public/output/og/${post.title.toLowerCase().split(' ').join('-')}.png` 234 | }); 235 | 236 | await page.close(); 237 | } 238 | await browser.close(); 239 | 240 | /** 241 | * @TODO 242 | * - Create rss feed 243 | */ -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | PUBLISH = "public" -------------------------------------------------------------------------------- /netlify/functions/blog/blog.mjs: -------------------------------------------------------------------------------- 1 | import { router } from '../../../router.js'; 2 | 3 | export default async (request, context) => { 4 | return router.handleRequest(request); 5 | }; 6 | 7 | export const config = { 8 | path: ['/*', '/foo', '/blog/*', '/thoughts/*', '/definitions'], 9 | preferStatic: true 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passle-blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "npm run build && netlify dev --dir=public", 9 | "dev": "netlify dev", 10 | "build": "npm run md-to-html && npm run bundle-sw", 11 | "deploy": "npm run build && netlify deploy --build --dir=public --prod", 12 | "md-to-html": "node md-to-html.js", 13 | "bundle-sw": "esbuild sw.js --bundle --outfile=public/sw.js --minify --sourcemap --format=iife" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "swtl": "^0.2.1" 20 | }, 21 | "devDependencies": { 22 | "esbuild": "0.19.11", 23 | "front-matter": "4.0.2", 24 | "marked": "11.1.1", 25 | "marked-highlight": "2.1.0", 26 | "netlify-cli": "17.15.1", 27 | "puppeteer": "21.7.0", 28 | "shikiji": "0.9.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | (()=>{var W=Object.freeze,V=Object.defineProperty;var k=(e,t)=>W(V(e,"raw",{value:W(t||e.slice())}));var P=Symbol("component"),T=Symbol("await"),_=Symbol("slot");var y="TEXT",q="COMPONENT",f="NONE",b="PROP",v="CHILDREN",w="SET_PROP",B="PROP_VAL";function*p(e,...t){if(!t.length)yield*e;else if(!t.some(n=>typeof n=="function"))yield*e.reduce((n,i,a)=>[...n,i,...t.length>a?[t[a]]:[]],[]);else{let n=y,i=f,a=f,r=[];for(let o=0;o"&&e[o][l]!=='"'&&e[o][l]!=="'"&&e[o][l]!==" "&&d!=="...";)d+=e[o][l],l++;if(e[o][l]==="=")a=B;else if(e[o][l]==="/"&&i===b){i=f,a=f;let m=r.pop();r.length||(s="",yield m)}else e[o][l]===">"&&i===b&&(i=v,a=f);d==="..."?h.properties.push(...Object.entries(t[o]).map(([m,Y])=>({name:m,value:Y}))):d&&h.properties.push({name:d,value:!0})}else if(a===B){if(e[o][l]==='"'||e[o][l]==="'"){let d=e[o][l];if(!e[o][l+1])x.value=t[o],a=w;else{let m="";for(l++;e[o][l]!==d;)m+=e[o][l],l++;x.value=m||"",a=w}}else if(e[o][l-1]){let d="";for(;e[o][l]!==" "&&e[o][l]!=="/"&&e[o][l]!==">";)d+=e[o][l],l++;if(x.value=d||"",a=w,e[o][l]==="/"){let m=r.pop();r.length||(yield m)}}else if(x.value=t[o-1],a=w,e[o][l]===">")a=f,i=v;else if(e[o][l]==="/"){let d=r.pop();r.length||(a=f,i=f,n=y,l++,yield d)}}}else if(i===v){let h=r[r.length-1];if(e[o][l]==="<"&&e[o][l+1]==="/"&&e[o][l+2]==="/"){s&&(h.children.push(s),s=""),l+=3;let x=r.pop();r.length||(n=y,i=f,yield x)}else e[o][l]==="<"&&!e[o][l+1]&&typeof t[o]=="function"?(s&&(h.children.push(s),s=""),i=b,a=w,g.fn=t[o],r.push(g)):e[o][l+1]?s+=e[o][l]:s&&h&&(s+=e[o][l],h.children.push(s))}else if(c===">")i=v;else if(c===" ")i=b,a=w;else if(c==="/"&&e[o][l+1]===">"){n=y,i=f;let h=r.pop();r.length||(s="",yield h),l++}else s+=c;else s+=c}i===v&&t.length>o&&r[r.length-1].children.push(t[o]),s&&i!==v&&(yield s),r.length>1&&g.fn&&r[r.length-2].children.push(g),t.length>o&&n!==q&&(yield t[o])}}}function E({promise:e,children:t}){return{promise:e,template:t.find(n=>typeof n=="function")}}E.kind=T;var $=(e,t)=>e?t():"";function F(e){return typeof e.getReader=="function"}async function*z(e){let t=e.getReader(),n=new TextDecoder("utf-8");try{for(;;){let{done:i,value:a}=await t.read();if(i)return;yield n.decode(a)}}finally{t.releaseLock()}}async function*H(e){if(F(e))for await(let t of z(e))yield t;else for await(let t of e)yield t}async function*R(e,t){if(typeof e=="string")yield e;else if(typeof e=="function")yield*R(e(),t);else if(Array.isArray(e))yield*L(e,t);else if(typeof e?.then=="function"){let n=await e;yield*R(n,t)}else if(e instanceof Response&&e.body)yield*H(e.body);else if(e?.[Symbol.asyncIterator]||e?.[Symbol.iterator])yield*L(e,t);else if(e?.fn?.kind===T){let{promise:n,template:i}=e.fn({...e.properties.reduce((r,o)=>({...r,[o.name]:o.value}),{}),children:e.children}),a=t.length;t.push(n().then(r=>({id:a,template:i({pending:!1,error:!1,success:!0},r,null)})).catch(r=>(console.error(r.stack),{id:a,template:i({pending:!1,error:!0,success:!1},null,r)}))),yield*L(p`${i({pending:!0,error:!1,success:!1},null,null)}`,t)}else if(e?.kind===P){let n=[],i={};for(let a of e.children)if(a?.fn?.kind===_){let r=a.properties.find(o=>o.name==="name")?.value||"default";i[r]=a.children}else n.push(a);yield*R(await e.fn({...e.properties.reduce((a,r)=>({...a,[r.name]:r.value}),{}),children:n,slots:i}),t)}else{let n=e?.toString();n==="[object Object]"?yield JSON.stringify(e):yield n}}async function*L(e,t){for await(let n of e)yield*R(n,t)}var D;async function*N(e){let t=[];for(yield*L(e,t),t=t.map(n=>{let i=n.then(a=>(t.splice(t.indexOf(i),1),a));return i});t.length>0;){let n=await Promise.race(t),{id:i,template:a}=n;yield*N(p(D||(D=k([` 2 | 3 | \n `)\n }\n}\n\nexport async function renderToString(renderResult) {\n let result = \"\";\n\n for await (const chunk of render(renderResult)) {\n result += chunk;\n }\n\n return result;\n}\n", "import { render } from './render.js';\n\nexport class Router {\n constructor({ \n routes, \n fallback, \n plugins = [], \n baseHref = '' \n }) {\n this.plugins = plugins;\n this.fallback = {\n render: fallback,\n params: {}\n };\n this.routes = routes.map(route => ({\n ...route,\n urlPattern: new URLPattern({\n pathname: `${baseHref}${route.path}`,\n search: '*',\n hash: '*',\n })\n }));\n }\n\n _getPlugins(route) {\n return [\n ...(this.plugins ?? []), \n ...(route?.plugins ?? []),\n ]\n }\n\n async handleRequest(request) {\n const url = new URL(request.url);\n let matchedRoute;\n\n for (const route of this.routes) {\n\n const match = route.urlPattern.exec(url);\n if(match) {\n matchedRoute = {\n options: route.options,\n render: route.render,\n params: match?.pathname?.groups ?? {},\n plugins: route.plugins,\n };\n break;\n }\n }\n\n const route = matchedRoute?.render ?? this?.fallback?.render;\n if (route) {\n const url = new URL(request.url);\n const query = Object.fromEntries(new URLSearchParams(url.search));\n const params = matchedRoute?.params;\n\n const plugins = this._getPlugins(matchedRoute);\n for (const plugin of plugins) {\n try {\n const result = await plugin?.beforeResponse({url, query, params, request});\n if (result) {\n return result;\n }\n } catch(e) {\n console.log(`Plugin \"${plugin.name}\" error on beforeResponse hook`, e);\n throw e;\n }\n }\n\n return new HtmlResponse(await route({url, query, params, request}), matchedRoute?.options ?? {});\n }\n }\n}\n\nexport class HtmlResponse {\n constructor(template, options = {}) {\n const iterator = render(template);\n const encoder = new TextEncoder();\n const stream = new ReadableStream({\n async pull(controller) {\n try {\n const { value, done } = await iterator.next();\n if (done) {\n controller.close();\n } else {\n controller.enqueue(encoder.encode(value));\n }\n } catch(e) {\n console.error(e.stack);\n throw e;\n }\n }\n });\n\n return new Response(stream, { \n status: 200,\n headers: { \n 'Content-Type': 'text/html', \n 'Transfer-Encoding': 'chunked', \n ...(options?.headers ?? {})\n },\n ...options\n });\n }\n}", "import { SLOT_SYMBOL } from './symbol.js';\n\nfunction Slot() {}\n\nSlot.kind = SLOT_SYMBOL;\n\nexport { Slot };", "export const ENV = !!globalThis?.Netlify ? 'server' : 'worker';", "import { html } from 'swtl';\nimport { ENV } from '../env.js';\n\nexport function Html({title, children, slots}) {\n return html`\n \n \n \n \n \n \n ${title ?? ''}\n\n ${slots?.head ?? ''}\n \n \n \n
    \n

    PASSLE

    \n \n
    \n
    \n ${children}\n
    \n
    \n \n
    \n

    This blog was rendered ${ENV === 'server' ? 'on the server' : 'in a service worker'}

    \n

    The source for this blog can be found here, please feel free to steal it and use it for your own projects

    \n
    \n
    \n \n \n \n `\n}", "export function title(title) {\n const str = title.toLowerCase().split('-').join(' ');\n return str.charAt(0).toUpperCase() + str.slice(1)\n}\n", "import { html, Router, Slot, Await, when } from 'swtl';\nimport { Html } from './src/Html.js';\nimport { title } from './src/utils.js';\nimport { ENV } from './env.js';\n\nfunction RenderPost({promise}) {\n return html`\n <${Await} promise=${() => promise.then(b => b.text())}>\n ${(status, data, error) => html`\n ${when(status.pending, () => html`\n
    \n
    \n ${Array.from({ length: Math.floor(Math.random() * 11) + 5 }, () => Math.random()).map(i => html`\n
    \n `)}\n
    \n `)}\n ${when(status.error, () => html`

    Failed to fetch blog.

    `)}\n ${when(status.success, () => data)}\n `}\n \n `\n}\n\nexport const router = new Router({\n routes: [\n {\n path: '/',\n render: ({url, params, query, request}) => { \n const overview = fetch(url.origin + '/output/overview.html');\n\n return html`\n <${Html} title=\"Passle\">\n

    Overview

    \n ${overview}\n \n `\n }\n },\n {\n path: '/blog',\n render: ({url, params, query, request}) => { \n const overview = fetch(url.origin + '/output/blog/overview.html');\n\n return html`\n <${Html} title=\"Blog\">\n

    Blogs

    \n ${overview}\n \n `\n }\n },\n {\n path: '/blog/:title',\n render: ({url, params, query, request}) => {\n const blog = fetch(url.origin + '/output/blog/' + params.title + '/index.html');\n\n const blogTitle = title(params.title);\n const blogUrl = `${url.origin}/blog/${params.title}`;\n\n return html`\n <${Html} title=\"${\"Passle blog - \" + blogTitle}\"> \n\n <${Slot} name=\"head\">\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n\n \n\n
    \n <${RenderPost} promise=${blog}/>\n
    \n \n `\n }\n },\n {\n path: '/thoughts',\n render: ({url, params, query, request}) => { \n const overview = fetch(url.origin + '/output/thoughts/overview.html');\n\n return html`\n <${Html} title=\"Thoughts\">\n

    Thoughts

    \n

    \n Not quite blogs, not quite tweets. Something in between. Likely opinionated, potentially wrong. Subject to change over time.\n

    \n ${overview}\n \n `\n }\n },\n {\n path: '/definitions',\n render: ({url, params, query, request}) => { \n\n return html`\n <${Html} title=\"Definitions\">\n

    Definitions

    \n
    \n
    \n
    Buildless development
    \n
    Local development using native ESM and web standards; code that you write runs in the browser without any transformation. Note that this does not include Vite; Vite does a bunch of non-standard transformations and (pre-)bundling out of the box.
    \n \n
    SWSR
    \n
    Service Worker Side Rendering. SSR, but in a Service Worker.
    \n \n
    SWTL
    \n
    Service Worker Templating Language.
    \n
    \n
    \n \n `\n }\n },\n {\n path: '/thoughts/:title',\n render: ({url, params, query, request}) => {\n const thought = fetch(url.origin + '/output/thoughts/' + params.title + '/index.html');\n\n const thoughtTitle = title(params.title);\n const thoughtUrl = `${url.origin}/thoughts/${params.title}`;\n\n return html`\n <${Html} title=\"${\"Passle blog - \" + thoughtTitle}\">\n <${Slot} name=\"head\">\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n\n
    \n <${RenderPost} promise=${thought}/>\n
    \n \n `\n }\n },\n {\n path: '/foo',\n render: ({url, params, query, request}) => {\n return html`\n <${Html} title=\"Foo\">\n

    Foo

    \n \n `\n }\n },\n ],\n});", "import { router } from './router.js';\n\nself.addEventListener('install', () => {\n self.skipWaiting();\n});\n\nself.addEventListener('activate', (event) => {\n event.waitUntil(\n clients.claim().then(() => {\n self.clients.matchAll().then((clients) => {\n clients.forEach((client) =>\n client.postMessage({ type: 'SW_ACTIVATED' })\n );\n });\n })\n );\n});\n\nself.addEventListener('fetch', (event) => {\n if (event.request.mode === 'navigate') {\n event.respondWith(router.handleRequest(event.request));\n }\n});"], 5 | "mappings": "qGAAO,IAAMA,EAAmB,OAAO,WAAW,EACrCC,EAAe,OAAO,OAAO,EAC7BC,EAAc,OAAO,MAAM,ECAxC,IAAMC,EAAO,OACPC,EAAY,YAEZC,EAAO,OACPC,EAAO,OACPC,EAAW,WAEXC,EAAW,WACXC,EAAW,WAEV,SAAUC,EAAKC,KAAYC,EAAU,CAI1C,GAAI,CAACA,EAAS,OACZ,MAAOD,UAIE,CAACC,EAAS,KAAKC,GAAK,OAAOA,GAAM,UAAU,EACpD,MAAOF,EAAQ,OAAO,CAACG,EAAKC,EAAGC,IAAM,CAAC,GAAGF,EAAKC,EAAG,GAAIH,EAAS,OAASI,EAAI,CAACJ,EAASI,CAAC,CAAC,EAAI,CAAC,CAAE,EAAG,CAAC,CAAC,MAC9F,CACL,IAAIC,EAAOd,EACPe,EAAiBb,EACjBc,EAAYd,EAEVe,EAAiB,CAAC,EAQxB,QAASJ,EAAI,EAAGA,EAAIL,EAAQ,OAAQK,IAAK,CACvC,IAAIK,EAAS,GACPC,EAAY,CAChB,KAAMC,EACN,MAAO,CAAC,EACR,WAAY,CAAC,EACb,SAAU,CAAC,EACX,GAAI,MACN,EAWA,QAASC,EAAI,EAAGA,EAAIb,EAAQK,CAAC,EAAE,OAAQQ,IAAK,CAC1C,IAAI,EAAIb,EAAQK,CAAC,EAAEQ,CAAC,EACpB,GAAIP,IAASd,EAET,IAAM,KAKN,CAACQ,EAAQK,CAAC,EAAEQ,EAAI,CAAC,GAAK,OAAOZ,EAASI,CAAC,GAAM,YAE7CC,EAAOb,EACPkB,EAAU,GAAKV,EAASI,CAAC,EACzBI,EAAe,KAAKE,CAAS,GAE7BD,GAAU,UAEHJ,IAASb,EAClB,GAAIc,IAAmBZ,EAAM,CAC3B,IAAMgB,EAAYF,EAAeA,EAAe,OAAS,CAAC,EACpDK,EAAWH,GAAW,WAAWA,EAAU,WAAW,OAAS,CAAC,EACtE,GAAIH,IAAcX,EAAU,CAC1B,IAAIiB,EAAW,GACf,KACEd,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBC,IAAa,OAEbA,GAAYd,EAAQK,CAAC,EAAEQ,CAAC,EACxBA,IAOF,GAAIb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,IACpBL,EAAYV,UAKHE,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAAON,IAAmBZ,EAAM,CAC3DY,EAAiBb,EACjBc,EAAYd,EACZ,IAAMiB,EAAYF,EAAe,IAAI,EAChCA,EAAe,SAClBC,EAAS,GACT,MAAMC,EAMV,MAAWX,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAAON,IAAmBZ,IACrDY,EAAiBX,EACjBY,EAAYd,GAGVoB,IAAa,MACfH,EAAU,WAAW,KAAK,GAAG,OAAO,QAAQV,EAASI,CAAC,CAAC,EAAE,IAAI,CAAC,CAACU,EAAKC,CAAK,KAAM,CAAC,KAAAD,EAAM,MAAAC,CAAK,EAAE,CAAC,EACrFF,GACTH,EAAU,WAAW,KAAK,CAAC,KAAMG,EAAU,MAAO,EAAI,CAAC,CAE3D,SAAWN,IAAcV,GAWvB,GAAIE,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAAOb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,IAAK,CAClD,IAAMI,EAAQjB,EAAQK,CAAC,EAAEQ,CAAC,EAK1B,GAAI,CAACb,EAAQK,CAAC,EAAEQ,EAAI,CAAC,EACnBC,EAAS,MAAQb,EAASI,CAAC,EAC3BG,EAAYX,MACP,CAOL,IAAIqB,EAAM,GAEV,IADAL,IACMb,EAAQK,CAAC,EAAEQ,CAAC,IAAMI,GACtBC,GAAOlB,EAAQK,CAAC,EAAEQ,CAAC,EACnBA,IAGFC,EAAS,MAAQI,GAAO,GACxBV,EAAYX,CACd,CAKF,SAAYG,EAAQK,CAAC,EAAEQ,EAAI,CAAC,EAsBrB,CASL,IAAIK,EAAM,GACV,KACElB,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAClBb,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAElBK,GAAOlB,EAAQK,CAAC,EAAEQ,CAAC,EACnBA,IAWF,GARAC,EAAS,MAAQI,GAAO,GACxBV,EAAYX,EAORG,EAAQK,CAAC,EAAEQ,CAAC,IAAM,IAAK,CACzB,IAAMF,EAAYF,EAAe,IAAI,EAChCA,EAAe,SAClB,MAAME,EAEV,CACF,SAtDEG,EAAS,MAAQb,EAASI,EAAI,CAAC,EAC/BG,EAAYX,EAETG,EAAQK,CAAC,EAAEQ,CAAC,IAAM,IACnBL,EAAYd,EACZa,EAAiBX,UAMRI,EAAQK,CAAC,EAAEQ,CAAC,IAAM,IAAK,CAChC,IAAMF,EAAYF,EAAe,IAAI,EAChCA,EAAe,SAClBD,EAAYd,EACZa,EAAiBb,EACjBY,EAAOd,EACPqB,IACA,MAAMF,EAEV,EAoCN,SAAWJ,IAAmBX,EAAU,CACtC,IAAMuB,EAAmBV,EAAeA,EAAe,OAAS,CAAC,EAMjE,GAAIT,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAAOb,EAAQK,CAAC,EAAEQ,EAAI,CAAC,IAAM,KAAOb,EAAQK,CAAC,EAAEQ,EAAI,CAAC,IAAM,IAAK,CAC/EH,IACFS,EAAiB,SAAS,KAAKT,CAAM,EACrCA,EAAS,IAGXG,GAAK,EAKL,IAAMF,EAAYF,EAAe,IAAI,EAChCA,EAAe,SAClBH,EAAOd,EACPe,EAAiBb,EACjB,MAAMiB,EAEV,MAAWX,EAAQK,CAAC,EAAEQ,CAAC,IAAM,KAAO,CAACb,EAAQK,CAAC,EAAEQ,EAAI,CAAC,GAAK,OAAOZ,EAASI,CAAC,GAAM,YAI3EK,IACFS,EAAiB,SAAS,KAAKT,CAAM,EACrCA,EAAS,IAEXH,EAAiBZ,EACjBa,EAAYX,EACZc,EAAU,GAAKV,EAASI,CAAC,EACzBI,EAAe,KAAKE,CAAS,GACnBX,EAAQK,CAAC,EAAEQ,EAAE,CAAC,EAUxBH,GAAUV,EAAQK,CAAC,EAAEQ,CAAC,EALlBH,GAAUS,IACZT,GAAUV,EAAQK,CAAC,EAAEQ,CAAC,EACtBM,EAAiB,SAAS,KAAKT,CAAM,EAM3C,SAAW,IAAM,IACfH,EAAiBX,UACR,IAAM,IACfW,EAAiBZ,EACjBa,EAAYX,UAEH,IAAM,KAAOG,EAAQK,CAAC,EAAEQ,EAAI,CAAC,IAAM,IAAK,CACjDP,EAAOd,EACPe,EAAiBb,EAKjB,IAAMiB,EAAYF,EAAe,IAAI,EAChCA,EAAe,SAClBC,EAAS,GACT,MAAMC,GAERE,GACF,MACEH,GAAU,OAGZA,GAAU,CAEd,CAEIH,IAAmBX,GAAYK,EAAS,OAASI,GAC1BI,EAAeA,EAAe,OAAS,CAAC,EAChD,SAAS,KAAKR,EAASI,CAAC,CAAC,EAGxCK,GAAUH,IAAmBX,IAC/B,MAAMc,GAGJD,EAAe,OAAS,GAAKE,EAAU,IACzCF,EAAeA,EAAe,OAAS,CAAC,EAAE,SAAS,KAAKE,CAAS,EAI/DV,EAAS,OAASI,GAAKC,IAASb,IAClC,MAAMQ,EAASI,CAAC,EAEpB,CACF,CACF,CC1TA,SAASe,EAAM,CAAC,QAAAC,EAAS,SAAAC,CAAQ,EAAG,CAClC,MAAO,CACL,QAAAD,EACA,SAAUC,EAAS,KAAKC,GAAK,OAAOA,GAAM,UAAU,CACtD,CACF,CAEAH,EAAM,KAAOI,EAEb,IAAMC,EAAO,CAACC,EAAWC,IAAaD,EAAYC,EAAS,EAAI,GCR/D,SAASC,EAAaC,EAAK,CACzB,OAAO,OAAOA,EAAI,WAAc,UAClC,CAEA,eAAuBC,EAAoBC,EAAQ,CACjD,IAAMC,EAASD,EAAO,UAAU,EAC1BE,EAAU,IAAI,YAAY,OAAO,EAEvC,GAAI,CACF,OAAa,CACX,GAAM,CAAE,KAAAC,EAAM,MAAAC,CAAM,EAAI,MAAMH,EAAO,KAAK,EAC1C,GAAIE,EAAM,OACV,MAAMD,EAAQ,OAAOE,CAAK,CAC5B,CACF,QAAE,CACAH,EAAO,YAAY,CACrB,CACF,CAEA,eAAgBI,EAAeC,EAAU,CACvC,GAAIT,EAAaS,CAAQ,EACvB,cAAiBC,KAASR,EAAoBO,CAAQ,EACpD,MAAMC,MAGR,eAAiBA,KAASD,EACxB,MAAMC,CAGZ,CAEA,eAAuBC,EAAOD,EAAOE,EAAU,CAC7C,GAAI,OAAOF,GAAU,SACnB,MAAMA,UACG,OAAOA,GAAU,WAC1B,MAAOC,EAAOD,EAAM,EAAGE,CAAQ,UACtB,MAAM,QAAQF,CAAK,EAC5B,MAAOG,EAAQH,EAAOE,CAAQ,UACrB,OAAOF,GAAO,MAAS,WAAY,CAC5C,IAAMI,EAAI,MAAMJ,EAChB,MAAOC,EAAOG,EAAGF,CAAQ,CAC3B,SAAWF,aAAiB,UAAYA,EAAM,KAC5C,MAAOF,EAAeE,EAAM,IAAI,UACvBA,IAAQ,OAAO,aAAa,GAAKA,IAAQ,OAAO,QAAQ,EACjE,MAAOG,EAAQH,EAAOE,CAAQ,UACrBF,GAAO,IAAI,OAASK,EAAc,CAC3C,GAAM,CAAE,QAAAC,EAAS,SAAAC,CAAS,EAAIP,EAAM,GAAG,CACrC,GAAGA,EAAM,WAAW,OAAO,CAACQ,EAAKC,KAAU,CAAC,GAAGD,EAAK,CAACC,EAAK,IAAI,EAAGA,EAAK,KAAK,GAAI,CAAC,CAAC,EACjF,SAAUT,EAAM,QAClB,CAAC,EACKU,EAAKR,EAAS,OACpBA,EAAS,KACPI,EAAQ,EACL,KAAKK,IAAS,CACb,GAAAD,EACA,SAAUH,EAAS,CAAC,QAAS,GAAO,MAAO,GAAO,QAAS,EAAI,EAAGI,EAAM,IAAI,CAC9E,EAAE,EACD,MAAMC,IACL,QAAQ,MAAMA,EAAM,KAAK,EAClB,CACL,GAAAF,EACA,SAAUH,EAAS,CAAC,QAAS,GAAO,MAAO,GAAM,QAAS,EAAK,EAAG,KAAMK,CAAK,CAC/E,EACD,CACL,EACA,MAAOT,EAAQU,0DAA6DH,EAAG,SAAS,CAAC,KAAKH,EAAS,CAAC,QAAS,GAAM,MAAO,GAAO,QAAS,EAAK,EAAG,KAAM,IAAI,CAAC,sBAAuBL,CAAQ,CAClM,SAAWF,GAAO,OAASc,EAAkB,CAC3C,IAAMC,EAAW,CAAC,EACZC,EAAQ,CAAC,EACf,QAAWC,KAASjB,EAAM,SACxB,GAAIiB,GAAO,IAAI,OAASC,EAAa,CACnC,IAAMC,EAAOF,EAAM,WAAW,KAAKR,GAAQA,EAAK,OAAS,MAAM,GAAG,OAAS,UAC3EO,EAAMG,CAAI,EAAIF,EAAM,QACtB,MACEF,EAAS,KAAKE,CAAK,EAIvB,MAAOhB,EACL,MAAMD,EAAM,GAAG,CACb,GAAGA,EAAM,WAAW,OAAO,CAACQ,EAAKC,KAAU,CAAC,GAAGD,EAAK,CAACC,EAAK,IAAI,EAAGA,EAAK,KAAK,GAAI,CAAC,CAAC,EACjF,SAAAM,EACA,MAAAC,CACF,CAAC,EACDd,CACF,CACF,KAAO,CACL,IAAMkB,EAAcpB,GAAO,SAAS,EACjCoB,IAAgB,kBACjB,MAAM,KAAK,UAAUpB,CAAK,EAE1B,MAAMoB,CAEV,CACF,CAEA,eAAgBjB,EAAQI,EAAUL,EAAU,CAC1C,cAAiBF,KAASO,EACxB,MAAON,EAAOD,EAAOE,CAAQ,CAEjC,CAvGA,IAAAmB,EAyGA,eAAuBC,EAAOf,EAAU,CACtC,IAAIL,EAAW,CAAC,EAYhB,IAVA,MAAOC,EAAQI,EAAUL,CAAQ,EAEjCA,EAAWA,EAAS,IAAII,GAAW,CACjC,IAAIiB,EAAIjB,EAAQ,KAAKkB,IACnBtB,EAAS,OAAOA,EAAS,QAAQqB,CAAC,EAAG,CAAC,EAC/BC,EACR,EACD,OAAOD,CACT,CAAC,EAEMrB,EAAS,OAAS,GAAG,CAC1B,IAAMuB,EAAe,MAAM,QAAQ,KAAKvB,CAAQ,EAC1C,CAAE,GAAAQ,EAAI,SAAAH,CAAS,EAAIkB,EAEzB,MAAOH,EAAOT,EAAAQ,MAAIK,EAAA;AAAA,2BACkB,KAAa;AAAA;AAAA;AAAA,8EAGsC;AAAA,uEACP;AAAA;AAAA;AAAA;AAAA,SAJzDhB,EAAG,SAAS,EAAMH,EAGiCG,EAAG,SAAS,EACnBA,EAAG,SAAS,EAI9E,CACH,CACF,CCnIO,IAAMiB,EAAN,KAAa,CAClB,YAAY,CACV,OAAAC,EACA,SAAAC,EACA,QAAAC,EAAU,CAAC,EACX,SAAAC,EAAW,EACb,EAAG,CACD,KAAK,QAAUD,EACf,KAAK,SAAW,CACd,OAAQD,EACR,OAAQ,CAAC,CACX,EACA,KAAK,OAASD,EAAO,IAAII,IAAU,CACjC,GAAGA,EACH,WAAY,IAAI,WAAW,CACzB,SAAU,GAAGD,CAAQ,GAAGC,EAAM,IAAI,GAClC,OAAQ,IACR,KAAM,GACR,CAAC,CACH,EAAE,CACJ,CAEA,YAAYA,EAAO,CACjB,MAAO,CACL,GAAI,KAAK,SAAW,CAAC,EACrB,GAAIA,GAAO,SAAW,CAAC,CACzB,CACF,CAEA,MAAM,cAAcC,EAAS,CAC3B,IAAMC,EAAM,IAAI,IAAID,EAAQ,GAAG,EAC3BE,EAEJ,QAAWH,KAAS,KAAK,OAAQ,CAE/B,IAAMI,EAAQJ,EAAM,WAAW,KAAKE,CAAG,EACvC,GAAGE,EAAO,CACRD,EAAe,CACb,QAASH,EAAM,QACf,OAAQA,EAAM,OACd,OAAQI,GAAO,UAAU,QAAU,CAAC,EACpC,QAASJ,EAAM,OACjB,EACA,KACF,CACF,CAEA,IAAMA,EAAQG,GAAc,QAAU,MAAM,UAAU,OACtD,GAAIH,EAAO,CACT,IAAME,EAAM,IAAI,IAAID,EAAQ,GAAG,EACzBI,EAAQ,OAAO,YAAY,IAAI,gBAAgBH,EAAI,MAAM,CAAC,EAC1DI,EAASH,GAAc,OAEvBL,EAAU,KAAK,YAAYK,CAAY,EAC7C,QAAWI,KAAUT,EACnB,GAAI,CACF,IAAMU,EAAS,MAAMD,GAAQ,eAAe,CAAC,IAAAL,EAAK,MAAAG,EAAO,OAAAC,EAAQ,QAAAL,CAAO,CAAC,EACzE,GAAIO,EACF,OAAOA,CAEX,OAAQC,EAAG,CACT,cAAQ,IAAI,WAAWF,EAAO,IAAI,iCAAkCE,CAAC,EAC/DA,CACR,CAGF,OAAO,IAAIC,EAAa,MAAMV,EAAM,CAAC,IAAAE,EAAK,MAAAG,EAAO,OAAAC,EAAQ,QAAAL,CAAO,CAAC,EAAGE,GAAc,SAAW,CAAC,CAAC,CACjG,CACF,CACF,EAEaO,EAAN,KAAmB,CACxB,YAAYC,EAAUC,EAAU,CAAC,EAAG,CAClC,IAAMC,EAAWC,EAAOH,CAAQ,EAC1BI,EAAU,IAAI,YACdC,EAAS,IAAI,eAAe,CAChC,MAAM,KAAKC,EAAY,CACrB,GAAI,CACF,GAAM,CAAE,MAAAC,EAAO,KAAAC,CAAK,EAAI,MAAMN,EAAS,KAAK,EACxCM,EACFF,EAAW,MAAM,EAEjBA,EAAW,QAAQF,EAAQ,OAAOG,CAAK,CAAC,CAE5C,OAAQT,EAAG,CACT,cAAQ,MAAMA,EAAE,KAAK,EACfA,CACR,CACF,CACF,CAAC,EAED,OAAO,IAAI,SAASO,EAAQ,CAC1B,OAAQ,IACR,QAAS,CACP,eAAgB,YAChB,oBAAqB,UACrB,GAAIJ,GAAS,SAAW,CAAC,CAC3B,EACA,GAAGA,CACL,CAAC,CACH,CACF,ECrGA,SAASQ,GAAO,CAAC,CAEjBA,EAAK,KAAOC,ECJL,IAAMC,EAAQ,YAAY,QAAU,SAAW,SCAtD,IAAAC,EAGO,SAASC,EAAK,CAAC,MAAAC,EAAO,SAAAC,EAAU,MAAAC,CAAK,EAAG,CAC7C,OAAOC,EAAAL,MAAIM,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAOe;AAAA;AAAA,UAED;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YA+QP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAU8E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uDAS3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OApStDJ,GAAS,GAEhBE,GAAO,MAAQ,GA+QbD,EAU4BI,IAAQ,SAAW,gBAAkB,sBAStBA,IAAQ,SAmC/D,CClVO,SAASC,EAAMA,EAAO,CAC3B,IAAMC,EAAMD,EAAM,YAAY,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG,EACnD,OAAOC,EAAI,OAAO,CAAC,EAAE,YAAY,EAAIA,EAAI,MAAM,CAAC,CAClD,CCEA,SAASC,EAAW,CAAC,QAAAC,CAAO,EAAG,CAC7B,OAAOC;AAAA,OACFC,CAAK,YAAY,IAAMF,EAAQ,KAAKG,GAAKA,EAAE,KAAK,CAAC,CAAC;AAAA,QACjD,CAACC,EAAQC,EAAMC,IAAUL;AAAA,UACvBM,EAAKH,EAAO,QAAS,IAAMH;AAAA;AAAA;AAAA,cAGvB,MAAM,KAAK,CAAE,OAAQ,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,CAAE,EAAG,IAAM,KAAK,OAAO,CAAC,EAAE,IAAIO,GAAKP;AAAA,mCACpE,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,EAAE;AAAA,aACzD,CAAC;AAAA;AAAA,SAEL,CAAC;AAAA,UACAM,EAAKH,EAAO,MAAO,IAAMH,+BAAkC,CAAC;AAAA,UAC5DM,EAAKH,EAAO,QAAS,IAAMC,CAAI,CAAC;AAAA,OACnC;AAAA;AAAA,GAGP,CAEO,IAAMI,EAAS,IAAIC,EAAO,CAC/B,OAAQ,CACN,CACE,KAAM,IACN,OAAQ,CAAC,CAAC,IAAAC,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAAM,CACzC,IAAMC,EAAW,MAAMJ,EAAI,OAAS,uBAAuB,EAE3D,OAAOV;AAAA,aACFe,CAAI;AAAA;AAAA,cAEHD,CAAQ;AAAA;AAAA,SAGhB,CACF,EACA,CACE,KAAM,QACN,OAAQ,CAAC,CAAC,IAAAJ,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAAM,CACzC,IAAMC,EAAW,MAAMJ,EAAI,OAAS,4BAA4B,EAEhE,OAAOV;AAAA,aACFe,CAAI;AAAA;AAAA,cAEHD,CAAQ;AAAA;AAAA,SAGhB,CACF,EACA,CACE,KAAM,eACN,OAAQ,CAAC,CAAC,IAAAJ,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAAM,CACzC,IAAMG,EAAO,MAAMN,EAAI,OAAS,gBAAkBC,EAAO,MAAQ,aAAa,EAExEM,EAAYC,EAAMP,EAAO,KAAK,EAC9BQ,EAAU,GAAGT,EAAI,MAAM,SAASC,EAAO,KAAK,GAElD,OAAOX;AAAA,aACFe,CAAI,WAAW,iBAAmBE,CAAS;AAAA;AAAA,eAEzCG,CAAI;AAAA;AAAA,iDAE8BD,CAAO;AAAA;AAAA,mDAELF,CAAS;AAAA;AAAA,mDAETP,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA,uDAChCM,CAAS;AAAA;AAAA;AAAA;AAAA;AAAA,kDAKdE,CAAO;AAAA,oDACLF,CAAS;AAAA,wDACLA,CAAS;AAAA;AAAA,oDAEbP,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA,wDAChCD,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,iBAK3Eb,CAAU,YAAYkB,CAAI;AAAA;AAAA;AAAA,SAIrC,CACF,EACA,CACE,KAAM,YACN,OAAQ,CAAC,CAAC,IAAAN,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAAM,CACzC,IAAMC,EAAW,MAAMJ,EAAI,OAAS,gCAAgC,EAEpE,OAAOV;AAAA,aACFe,CAAI;AAAA;AAAA;AAAA;AAAA;AAAA,cAKHD,CAAQ;AAAA;AAAA,SAGhB,CACF,EACA,CACE,KAAM,eACN,OAAQ,CAAC,CAAC,IAAAJ,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAE5Bb;AAAA,aACFe,CAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAiBb,EACA,CACE,KAAM,mBACN,OAAQ,CAAC,CAAC,IAAAL,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAAM,CACzC,IAAMQ,EAAU,MAAMX,EAAI,OAAS,oBAAsBC,EAAO,MAAQ,aAAa,EAE/EW,EAAeJ,EAAMP,EAAO,KAAK,EACjCY,EAAa,GAAGb,EAAI,MAAM,aAAaC,EAAO,KAAK,GAEzD,OAAOX;AAAA,aACFe,CAAI,WAAW,iBAAmBO,CAAY;AAAA,eAC5CF,CAAI;AAAA;AAAA,iDAE8BG,CAAU;AAAA;AAAA,mDAERD,CAAY;AAAA;AAAA,mDAEZZ,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA,uDAChCW,CAAY;AAAA;AAAA;AAAA;AAAA;AAAA,kDAKjBC,CAAU;AAAA,oDACRD,CAAY;AAAA,wDACRA,CAAY;AAAA;AAAA,oDAEhBZ,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA,wDAChCD,EAAI,MAAM,cAAcC,EAAO,KAAK;AAAA;AAAA;AAAA;AAAA,iBAI3Eb,CAAU,YAAYuB,CAAO;AAAA;AAAA;AAAA,SAIxC,CACF,EACA,CACE,KAAM,OACN,OAAQ,CAAC,CAAC,IAAAX,EAAK,OAAAC,EAAQ,MAAAC,EAAO,QAAAC,CAAO,IAC5Bb;AAAA,aACFe,CAAI;AAAA;AAAA;AAAA,SAKb,CACF,CACF,CAAC,EChLD,KAAK,iBAAiB,UAAW,IAAM,CACrC,KAAK,YAAY,CACnB,CAAC,EAED,KAAK,iBAAiB,WAAaS,GAAU,CAC3CA,EAAM,UACJ,QAAQ,MAAM,EAAE,KAAK,IAAM,CACzB,KAAK,QAAQ,SAAS,EAAE,KAAMC,GAAY,CACxCA,EAAQ,QAASC,GACfA,EAAO,YAAY,CAAE,KAAM,cAAe,CAAC,CAC7C,CACF,CAAC,CACH,CAAC,CACH,CACF,CAAC,EAED,KAAK,iBAAiB,QAAUF,GAAU,CACpCA,EAAM,QAAQ,OAAS,YACzBA,EAAM,YAAYG,EAAO,cAAcH,EAAM,OAAO,CAAC,CAEzD,CAAC", 6 | "names": ["COMPONENT_SYMBOL", "AWAIT_SYMBOL", "SLOT_SYMBOL", "TEXT", "COMPONENT", "NONE", "PROP", "CHILDREN", "SET_PROP", "PROP_VAL", "html", "statics", "dynamics", "d", "acc", "s", "i", "MODE", "COMPONENT_MODE", "PROP_MODE", "componentStack", "result", "component", "COMPONENT_SYMBOL", "j", "property", "name", "value", "quote", "val", "currentComponent", "Await", "promise", "children", "c", "AWAIT_SYMBOL", "when", "condition", "template", "hasGetReader", "obj", "streamAsyncIterator", "stream", "reader", "decoder", "done", "value", "handleIterator", "iterable", "chunk", "handle", "promises", "_render", "v", "AWAIT_SYMBOL", "promise", "template", "acc", "prop", "id", "data", "error", "html", "COMPONENT_SYMBOL", "children", "slots", "child", "SLOT_SYMBOL", "name", "stringified", "_a", "render", "p", "val", "nextPromise", "__template", "Router", "routes", "fallback", "plugins", "baseHref", "route", "request", "url", "matchedRoute", "match", "query", "params", "plugin", "result", "e", "HtmlResponse", "template", "options", "iterator", "render", "encoder", "stream", "controller", "value", "done", "Slot", "SLOT_SYMBOL", "ENV", "_a", "Html", "title", "children", "slots", "html", "__template", "ENV", "title", "str", "RenderPost", "promise", "html", "Await", "b", "status", "data", "error", "when", "i", "router", "Router", "url", "params", "query", "request", "overview", "Html", "blog", "blogTitle", "title", "blogUrl", "Slot", "thought", "thoughtTitle", "thoughtUrl", "event", "clients", "client", "router"] 7 | } 8 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | import { html, Router, Slot, Await, when } from 'swtl'; 2 | import { Html } from './src/Html.js'; 3 | import { title } from './src/utils.js'; 4 | import { ENV } from './env.js'; 5 | 6 | function RenderPost({promise}) { 7 | return html` 8 | <${Await} promise=${() => promise.then(b => b.text())}> 9 | ${(status, data, error) => html` 10 | ${when(status.pending, () => html` 11 |
    12 |
    13 | ${Array.from({ length: Math.floor(Math.random() * 11) + 5 }, () => Math.random()).map(i => html` 14 |
    15 | `)} 16 |
    17 | `)} 18 | ${when(status.error, () => html`

    Failed to fetch blog.

    `)} 19 | ${when(status.success, () => data)} 20 | `} 21 | 22 | ` 23 | } 24 | 25 | export const router = new Router({ 26 | routes: [ 27 | { 28 | path: '/', 29 | render: ({url, params, query, request}) => { 30 | const overview = fetch(url.origin + '/output/overview.html'); 31 | 32 | return html` 33 | <${Html} title="Passle"> 34 |

    Overview

    35 | ${overview} 36 | 37 | ` 38 | } 39 | }, 40 | { 41 | path: '/blog', 42 | render: ({url, params, query, request}) => { 43 | const overview = fetch(url.origin + '/output/blog/overview.html'); 44 | 45 | return html` 46 | <${Html} title="Blog"> 47 |

    Blogs

    48 | ${overview} 49 | 50 | ` 51 | } 52 | }, 53 | { 54 | path: '/blog/:title', 55 | render: ({url, params, query, request}) => { 56 | const blog = fetch(url.origin + '/output/blog/' + params.title + '/index.html'); 57 | 58 | const blogTitle = title(params.title); 59 | const blogUrl = `${url.origin}/blog/${params.title}`; 60 | 61 | return html` 62 | <${Html} title="${"Passle blog - " + blogTitle}"> 63 | 64 | <${Slot} name="head"> 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
    86 | <${RenderPost} promise=${blog}/> 87 |
    88 | 89 | ` 90 | } 91 | }, 92 | { 93 | path: '/thoughts', 94 | render: ({url, params, query, request}) => { 95 | const overview = fetch(url.origin + '/output/thoughts/overview.html'); 96 | 97 | return html` 98 | <${Html} title="Thoughts"> 99 |

    Thoughts

    100 |

    101 | Not quite blogs, not quite tweets. Something in between. Likely opinionated, potentially wrong. Subject to change over time. 102 |

    103 | ${overview} 104 | 105 | ` 106 | } 107 | }, 108 | { 109 | path: '/definitions', 110 | render: ({url, params, query, request}) => { 111 | 112 | return html` 113 | <${Html} title="Definitions"> 114 |

    Definitions

    115 |
    116 |
    117 |
    Buildless development
    118 |
    Local development using native ESM and web standards; code that you write runs in the browser without any transformation. Note that this does not include Vite; Vite does a bunch of non-standard transformations and (pre-)bundling out of the box.
    119 | 120 |
    SWSR
    121 |
    Service Worker Side Rendering. SSR, but in a Service Worker.
    122 | 123 |
    SWTL
    124 |
    Service Worker Templating Language.
    125 |
    126 |
    127 | 128 | ` 129 | } 130 | }, 131 | { 132 | path: '/thoughts/:title', 133 | render: ({url, params, query, request}) => { 134 | const thought = fetch(url.origin + '/output/thoughts/' + params.title + '/index.html'); 135 | 136 | const thoughtTitle = title(params.title); 137 | const thoughtUrl = `${url.origin}/thoughts/${params.title}`; 138 | 139 | return html` 140 | <${Html} title="${"Passle blog - " + thoughtTitle}"> 141 | <${Slot} name="head"> 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
    162 | <${RenderPost} promise=${thought}/> 163 |
    164 | 165 | ` 166 | } 167 | }, 168 | { 169 | path: '/foo', 170 | render: ({url, params, query, request}) => { 171 | return html` 172 | <${Html} title="Foo"> 173 |

    Foo

    174 | 175 | ` 176 | } 177 | }, 178 | ], 179 | }); -------------------------------------------------------------------------------- /src/Html.js: -------------------------------------------------------------------------------- 1 | import { html } from 'swtl'; 2 | import { ENV } from '../env.js'; 3 | 4 | export function Html({title, children, slots}) { 5 | return html` 6 | 7 | 8 | 9 | 10 | 11 | 12 | ${title ?? ''} 13 | 14 | ${slots?.head ?? ''} 15 | 272 | 273 | 274 |
    275 |

    PASSLE

    276 | 283 |
    284 |
    285 | ${children} 286 |
    287 |
    288 | 294 |
    295 |

    This blog was rendered ${ENV === 'server' ? 'on the server' : 'in a service worker'}

    296 |

    The source for this blog can be found here, please feel free to steal it and use it for your own projects

    297 |
    298 |
    299 | 300 | 337 | 338 | ` 339 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function title(title) { 2 | const str = title.toLowerCase().split('-').join(' '); 3 | return str.charAt(0).toUpperCase() + str.slice(1) 4 | } 5 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | import { router } from './router.js'; 2 | 3 | self.addEventListener('install', () => { 4 | self.skipWaiting(); 5 | }); 6 | 7 | self.addEventListener('activate', (event) => { 8 | event.waitUntil( 9 | clients.claim().then(() => { 10 | self.clients.matchAll().then((clients) => { 11 | clients.forEach((client) => 12 | client.postMessage({ type: 'SW_ACTIVATED' }) 13 | ); 14 | }); 15 | }) 16 | ); 17 | }); 18 | 19 | self.addEventListener('fetch', (event) => { 20 | if (event.request.mode === 'navigate') { 21 | event.respondWith(router.handleRequest(event.request)); 22 | } 23 | }); -------------------------------------------------------------------------------- /thoughts/on-bun/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: On Bun 3 | description: And why standardization is a good thing 4 | updated: 2024-01-17 5 | --- 6 | # On Bun 7 | 8 | I recently had a discussion on Twitter about Bun, and I figured there's enough to talk about to warrant a post. Bun aims to be the [replacement](https://x.com/jarredsumner/status/1738435352883499278?s=20) of Node as the default runtime, and spends a lot of effort and resources at compatibility with Node; your Node code should work in Bun. The thing is that Bun's compatibility is a one-way street; Your Node code should work in Bun, but your Bun code may not work work in Node, because they provide lots of non-standard conveniences out of the box that may behave differently depending on the runtime/tool/bundler that you use. 9 | 10 | Some of those non-standard conveniences include non-standard imports, like: 11 | ```js 12 | import text from "./text.txt"; 13 | console.log(text); 14 | // => "Hello world!" 15 | ``` 16 | 17 | Or a build-time concept called "Macros" that let you run JavaScript at build-time via [Import Attributes](https://github.com/tc39/proposal-import-attributes): 18 | ```js 19 | import { random } from './random.ts' with { type: 'macro' }; 20 | 21 | console.log(`Your random number is ${random()}`); 22 | ``` 23 | 24 | > I have some thoughts about (ab)using import attributes for non-standard stuff as well, but thats a post for another day 25 | 26 | As well as otherwise [adding non-standard functionality to standardized API's](https://x.com/jarredsumner/status/1620414853214277634?s=20) that may well have led to Smooshgate-like scenarios. 27 | 28 | I find offering these kind of non-standardisms out of the box concerning for many different reasons, and I might even go as far to say that they're actively harmful to the rest of the JavaScript ecosystem. I've previously written about this in my blog called [The Cost of Convenience](/blog/the-cost-of-convenience). 29 | 30 | ## Standardization 31 | 32 | Standardization is a slow process. It is sometimes too slow. A good example of this is JSON and CSS modules, which is a clear example of a want that developers have had for a _long_ time, and we're _still_ not quite there yet. The [Import Attributes](https://github.com/tc39/proposal-import-attributes) proposal is a great way forward for this, but has gone through some revisions (moving from `assert` to `with`), and generally has taken a really long time to get where we're at today. 33 | 34 | Because importing these things werent available natively historically, this has led many bundlers and other tooling to implement their own version of these kinds of imports (sometimes without opting in, but enabled by default), and sometimes these versions of these kinds of imports are incompatible across tools. Tool A may give you something different than tool B which sometimes leads to lock-in, as well as packages being published to NPM _assuming_ you use the same tool as the author of the package, and other incompatibilities and frustrations (The JavaScript ecosystem is not well known for its stable ecosystem). This is why standardization is a _good thing_. 35 | 36 | Bun on the other hand does not seem to care about standardization and interoperability; they only care about Bun. This may sounds harsh, but they've made this very clear. There's a very big difference between _interoperability_ and _compatibility_, and Bun's compatibility only goes one way. This is disappointing, especially considering that there are good avenues dedicated to interoperability, like the [WinterCG](https://wintercg.org/) (Web-**interoperable** Runtimes Community Group), which Bun is not a part of at the time of writing. 37 | 38 | Additionally, it seems that a lot of Bun's decisions are based on convenience, and often on Twitter polls. Someone on Twitter even argued that "It's Node's task to keep up with Bun" (paraphrased) and this, to me, seems like an incredibly hostile way to go about standardization, where one runtime just ships anything based on Twitter vibes, and other runtimes are just expected to keep up. The JavaScript ecosystem is messy enough as it is; there is a reason that standardization takes time. 39 | 40 | ## But what about 41 | 42 | Other runtimes like Deno? I have similar concerns about Deno, which may warrant it's own post at some point, but here's the short of it: Deno supports type checking via TypeScript out of the box, but... which version? TypeScript doesn't follow semver, and often does breaking changes on minor or patch versions. They have a good reason for this, but it does go against the grain of the rest of the ecosystem, and many developers [don't seem to realize](https://x.com/robpalmer2/status/1727969814272966885?s=20) that this is the case. This means that if TypeScript makes a breaking change, and Deno upgrades the version of TypeScript, your Deno code may be broken. Deno considers this ["a good thing"](https://docs.deno.com/runtime/manual/advanced/typescript/faqs#there-was-a-breaking-change-in-the-version-of-typescript-that-deno-uses-why-did-you-break-my-program). 43 | 44 | Admittedly there is a lot of nuance to add to this, but as mentioned, that's for another day. 45 | 46 | ## The nuance 47 | 48 | It's not surprising that tools or even runtimes experiment with non-standard things and try to push the ecosystem forward; this is _healthy_. If no innovation takes place at all, what is there to standardize? Developers want to be productive, and want better and easier ways to achieve their goals. And if anything; Bun is definitely trying to innovate and trying to move the needle. 49 | 50 | I just don't think the way they're going about it is the best way to do it. -------------------------------------------------------------------------------- /thoughts/on-web-components/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: On Web Components 3 | description: Shocking 4 | updated: 2024-01-14 5 | --- 6 | # On Web Components 7 | 8 | Very good 9 | 10 | No notes --------------------------------------------------------------------------------