Seven Years, Three Files, One Permission
chrome-extensionjavascriptopen-sourceside-project

Seven Years, Three Files, One Permission

I wrote a lorem ipsum extension in 2018 because the one I used went paid. Seven years, 38 commits and 311 users later, it still does the same thing.

·10 min read

Seven Years, Three Files, One Permission

In June 2018 the lorem ipsum extension I had been using for months to fill my mockups pushed an update. Same icon, same popup, except it was now behind a paywall. Generating placeholder text is a random pick from an array of latin sentences. I closed the tab and wrote my own that evening.

The first commit landed on June 13, 2018: ten files, 177 lines. Seven years and 38 commits later it does exactly the same thing, and I think that is the most interesting story I have about it.

The Whole Extension

People assume browser extensions are more complicated than they are. Here is the entire manifest, as it stands today:

{
  "name": "Simple Lorem Ipsum Generator",
  "version": "1.4.0",
  "description": "Quick way to generate lorem ipsum text with customizable size. Light and forever free.",
  "permissions": ["storage"],
  "offline_enabled": true,
  "incognito": "spanning",
  "action": { "default_popup": "popup.html" },
  "manifest_version": 3
}

And here is the generator as it stood the day I sat down to write this post, minus the twenty sentences it draws from:

function appendContent(content) {
  const paragraphCount = parseInt(document.getElementById('paragraph-count').value, 10)
  const paragraphLength = parseInt(document.getElementById('paragraph-length').value, 10)
  const paragraphType = document.getElementById('paragraph-type').value
  let text = ''

  for (let paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
    if (paragraphType === 'Yes')
      text += '&lt;p&gt;'

    for (let sentenceIndex = 0; sentenceIndex < paragraphLength; sentenceIndex++) {
      const random = Math.floor(Math.random() * content.length)
      const sentence = content[random]
      text += sentence + (sentenceIndex === paragraphLength - 1 || paragraphType === 'Yes' ? '.' : '. ')
    }

    if (paragraphType === 'Yes')
      text += '&lt;/p&gt;'

    if (paragraphIndex !== paragraphCount - 1)
      text += paragraphType === 'Yes' ? "\n<br/>\n" : "\n\n"
  }

  document.getElementById('content-area').value = text
}

Read it if you like, there is nothing hidden in it. I will come back to it at the end of the post, because it is more broken than it looks.

And here is what it looked like in June 2018, in the screenshot I uploaded to the store, HTML tags option turned on:

The Simple Lorem Ipsum Generator popup in June 2018, version 1.0.5, showing generated paragraphs wrapped in p tags

Version 1.0.5, before the dark theme and before the buttons. Worth a closer look than I ever gave it.

Three files: popup.html for the form, popup.css for the theming, popup.js for about 140 lines. No build step beyond a shell script that zips eight paths. No dependencies, so no lockfile, no audit noise, no dependabot. The packed extension weighs 25 KB.

Two Things Annoyed Me About the Alternatives

The first is that most of them called a remote API. A network round trip for a random pick from a fixed array. That makes the extension useless on a train with no signal or behind a locked down corporate proxy, and it means whoever runs that API sees every click.

The second is that they asked for permissions they had no use for. Reading and modifying data on all websites, for a popup that outputs latin.

So I set two rules, and I wrote them into the manifest rather than into a README nobody reads. offline_enabled set to true, and a single permission, storage, to remember your dropdown settings. The sentences lived in a local loremIpsum.txt, read with an XMLHttpRequest when the popup opened. Not elegant, but nothing left the machine. I put a GitHub link in the footer so anyone could go check for themselves, renamed the project from "Lipsum Generator" to "Simple Lorem Ipsum Generator" so it would surface when someone searched the store for what it does, and put the promise in the store description: "Light and forever free."

I had invented nothing, and the store makes that clear. Lorem ipsum generators there number in the dozens, free, offline, often more complete than mine. What I had taken for an idea was in fact a need, shared by enough people that plenty of others answered it at the same time. Mine has 311 users and a single rating, which is roughly the size of the hole it fills.

What a Finished Project Does With Its Free Time

Nothing, essentially. After July 2018 the extension's own code did not change for five years. The commits in between swap a license, add a code of conduct, add a security policy. Not one line of the thing that generates text.

A single external event made me reopen the project: the deprecation of manifest v2. The migration, on December 6, 2023, took a morning and removed code. browser_action became action. The XHR called chrome.extension.getURL, which v3 dropped, so rather than port it to chrome.runtime.getURL I inlined the twenty sentences as a plain array and deleted loremIpsum.txt, along with the web_accessible_resources entry that had never needed to be there in the first place. The generator became a pure function over a literal.

That move to v3 was painful for a lot of people. It removed blocking webRequest, which gutted content blockers, and it banned remotely hosted code, which broke anything that pulled logic at runtime. None of it touched me, for the exact reason the extension works on a plane: if you never depended on the network or on remote code, there is nothing to rewrite.

The constraints I picked in 2018 to protect users turned out to protect me.

The rest fits into three evenings. The migration introduced a punctuation bug that I caught seventy minutes later, the inlined sentences keeping the trailing period the old file parser used to strip while the generator appended a second one. A dark theme in February 2024, through a media query rather than a toggle, so the popup follows whatever the browser already does. And in September 2025, the two gestures I was still doing by hand: copy, and regenerate, since until then the only way to get new text was to change a dropdown and change it back.

Finished Is Not the Same as Correct

While preparing this post I read popup.js end to end for the first time in a long while, and found the HTML tags option broken.

Look again at the snippet above. The code writes the escaped entities &lt;p&gt; and &lt;/p&gt;, which is odd, because the output element has been a plain textarea since the very first commit. It worked anyway, for seven years, by accident: the old line assigned the result with innerHTML, and setting innerHTML on a textarea runs the string through the HTML parser, which turns the entities back into real angle brackets before anyone reads value.

In September 2025, adding the copy button, I replaced that innerHTML with the obviously more correct value. Plain assignment decodes nothing. From that day, anyone who ticked the HTML tags box got literal &lt;p&gt; in their clipboard. Eleven months, on a feature I shipped, in a file I can read in one sitting, broken by the commit that made the code better.

Two more sat right next to it, both dating from June 2018 and both invisible for eight years. A condition eating the space between sentences whenever the option was on, and a stray <br/> inserted between paragraphs that already had <p> tags around them. Scroll back up to the screenshot. The tags come out as real angle brackets, which is the accident working. The <br/> is on its own line between paragraphs. And there it is, ligula.Nulla quis lorem, no space after the period. Both bugs were sitting in the store listing, at 1280 by 800, for eight years, in the image I chose myself to show the extension at its best. And the old innerHTML had a consequence I had never thought about: a textarea stops reflecting its markup once the user types into it, so anyone who edited the generated text and then changed a dropdown was looking at a generator that had silently stopped responding.

All of it is fixed in the repository now, and ships in the next store release. A finished project does not watch itself, and an empty issue tracker proves nothing. With 311 users, one rating, and an option most of them probably never touch, silence was never going to be a signal.

What I Take From It

The real cost of a piece of software is not in what it does, it is in what it depends on. This extension took four days over seven years, not because it is small, but because it has nothing to obey: no server to keep running, no dependencies to update, no permission to justify, no platform whose changes concern it. An equivalent extension calling an API would have spent those four days on hosting bills and forced migrations.

The corollary is less comfortable. A project that asks for nothing never reminds you it exists, so you stop looking at it, so its bugs live for years. Worse, the bug that lasted longest here was not introduced by neglect, it was introduced by a cleanup. That is the price, and for a lorem ipsum generator I pay it happily. For something people actually depend on, it would need to be replaced by something other than good intentions.

As for the paid extension that set all this off, I cannot find it anymore, it seems to have disappeared from the store. It was, I assume, trying to fund its own maintenance. Mine never needed funding because it never needed maintaining, and that was the only design decision that mattered.

Source is on Github, GPLv3, and it stays free.

Update (September 6, 2026)

This post describes the extension as it was on August 31, 2026. Five days later it is not that anymore, and several passages above no longer describe the repository.

The fixes announced at the end have shipped: they are in 1.5.0, released on September 5, 2026. The same version rewrote the generator. It now lives in a generator.js with no DOM and no browser API, so it can be tested; it draws its sentences without replacement, a Fisher-Yates shuffle and one bag per paragraph, so a paragraph no longer repeats itself until it has run through all twenty sentences; and it counts in words as well as in sentences. The popup gained a keyboard shortcut, Alt+Shift+L, and a translated interface. A browser.js absorbs the difference between the chrome and browser namespaces, and ./build --firefox produces a zip AMO will accept, with the data_collection_permissions set to none that Mozilla has required since November 2025. Version 1.6.0, the same day, took the translations to eight languages.

So the three files passage no longer holds, and neither does the manifest quoted above: it now goes through __MSG_ and default_locale, and declares the icons and the keyboard command. The single storage permission, offline_enabled and the complete absence of network are unchanged.

The paragraph about having no dependencies needs a caveat rather than a correction. There is indeed a pnpm-lock.yaml now, one development dependency (Biome), 36 tests run by node --test, a GitHub Actions CI that lints, tests and checks that both zips build, and yes, a dependabot.yml. But all of it is optional and sits at the edge of the project: none of it enters the zip, the extension still has zero runtime dependencies, and it still packages without installing anything, with the same shell script as in 2018. CI only checks, it publishes nothing: updating the stores stays manual. The zip went from 25 to 36 KB, mostly the eight translation files.

Those thirteen commits are probably the last ones for a while. Nothing is left on my list for this extension.