{ "version": "https://jsonfeed.org/version/1.1", "title": "uncenter.dev", "description": "Articles about programming, software development, and other interests of mine.", "language": "en", "authors": [ { "name": "uncenter", "avatar": "https://uncenter.dev/1024w.png?v=2316a73de1f9" } ], "home_page_url": "https://uncenter.dev", "feed_url": "https://uncenter.dev/feed.json", "items": [ { "id": "/posts/learn-eleventy-update", "url": "https://uncenter.dev/posts/learn-eleventy-update/", "title": "My new Learn Eleventy course", "content_html": "

If you're familiar with the Eleventy community, you've likely come across Andy Bell's excellent free course, Learn Eleventy from Scratch. Learn Eleventy from Scratch has been an invaluable resource to the community since it was published. Unforunately, time inevitably moves on. Originally written in 2021 — before the release of Eleventy v1.0.0! — parts of the course have gradually become outdated as Eleventy evolved.

As an avid user of Eleventy — you might have seen me in GitHub issues, on the Discord support channels, or used one of my plugins — I decided to create a fork of the course, titled "Learn Eleventy", in April of 2023. Initially, with my limited time for it, the fork focused on modernizing the foundation: it was rebuilt with a newer Eleventy-based site (huge thanks to Sandro Roth for the Eleventy Notes framework!) and accumulated various fixes and updates.

In late 2024, I noticed a sizeable jump — over a six-fold increase — in visitors to the course! As it turns out, a random contributor had suggested a change that Andy had merged, adding a link to my fork in a note at the top of the course. With so many new learners, I felt a new responsibility to work on the course and keep it up to date — especially since Eleventy itself had been evolving, reaching v3.0.0 in the time since the fork began. Although it took until June for me to find enough time to devote to the course, I began to work in the background on more substantial content updates.

I had hoped to complete the content overhaul before the new year, and I got pretty close! On January 2nd of this year, I finally merged the dev branch and removed the original Gulp-based asset pipeline, teaching instead Eleventy's native asset pipeline techniques (the official Eleventy Image plugin, Sass via Custom Template Languages, and more), as well as migrating all of the code samples to modern ESM syntax.

Several other updates arrived at the same time or since. The course has switched to the latest version — v2 — of the official Eleventy RSS plugin, and Moment has been replaced with Luxon for date formatting. I'm also excited to introduce a personal favorite, downloadable lesson files! A separate repository tracks all content changes in the course repository and compiles per-lesson ZIP archives of what the course expects you to have built by that point.

And one final update: this weekend, after much deliberation, I officially purchased and launched a new domain for the course — welcome to learneleventy.dev!
The course's "Issue 33" completed project site also now has it's own home, separate from the original course demo, at issue33.learneleventy.dev.

The domain for the course previously — learn-eleventy.pages.dev — is still active and up to date, though this might change to redirect to the new domain soon.

If you are an Eleventy user, or want to learn to become one, I hope this course can be a valuable resource for you! ❤️

", "summary": "Updates on reviving a resource for the Eleventy community", "date_published": "2026-03-02T00:00:00.000Z", "tags": [ "eleventy", "learn-eleventy" ] }, { "id": "/posts/learning-fortran", "url": "https://uncenter.dev/posts/learning-fortran/", "title": "Learning the oldest programming language", "content_html": "

While I probably should be learning a language like C, Go, or whatever new trendy language the ThePrimeagen mentions on Twitter (OCaml?), I'm going to attempt to learn Fortran[1].

A quick history

Fortran, which stands for FORmula TRANslator[2], was created at IBM by John Backus in 1957 for scientific applications and has apparently been popular for high-performance computing and benchmarking supercomputers in recent years. Fortran has had several subsequent releases since then; FORTRAN 77, Fortran 90, Fortran 95, Fortran 2003, Fortran 2008, and the latest Fortran 2018.

Which version of Fortran?

To understand what version of Fortran to learn/use, we first must understand the difference between fixed form and free form Fortran. The fixed form layout comes from the very beginning of Fortran, inherited from punch cards, and has odd restrictions about the column in which comments and statements are placed. The free form layout, first introduced in Fortran 90, removed special columns and added the ability to write comments wherever, and is what we'll be learning in this article. The compiler we'll be using is GNU Fortran, or gfortran. You can install it via Homebrew (macOS) with the gcc formula, or install it using a package manager for your OS. To tell gfortran that your code uses the free form layout, set the file extension to .f90 or newer. The following comment on the Fortran discussion board explains this well.

The .f90 suffix means that the source code is free format, not that
the code conforms to the Fortran 90 standard. Code that uses the .f90
suffix can use features from any Fortran standard. All Fortran
compilers recognize .f90 as a suffix indicating free source form, but
some may not recognize a suffix such as .f95, .f03, .f08, or .f18.
Some users may have build tools that do not recognize suffixes other
than .f90. Most Fortran source code on GitHub that uses features from
a standard more recent than Fortran 90 still uses the .f90 suffix.

Understanding the syntax

Coming from TypeScript, and before that, Python, I'm very used to (and comfortable with) modern — you might say "aesthetic" — syntax . Although I wouldn't say Fortran syntax is quite modern, it seems to avoid the syntactic sugar nightmares that plague beginners in other languages[3]. Take a look at this helloworld.f90 example below.

program helloworld\n\n  print *, 'Hello, world!'\n\nend program helloworld

Older Fortran programs required the use of SCREAMING_CASE for all keywords, but in modern Fortran you can and it is recommended to use snake_case (you can still use SCREAMING_CASE or any other case you want though).

Just from this small example we can gather that...

The syntax for printing is a little funky though. What is that asterisk doing there? The asterisk, aside from being used as a mathematical operator, indicates the "default". So for print, * means "print to the default output channel" (or "print to the default output file unit" to be precise), which is typically going to be STDOUT.

I can't find exactly where this is documented but you don't actually need the start and end program <program-name>; you could write a hello world program like this, though as I just mentioned this doesn't seem to be a common practice and isn't really very useful in any practical scenario.

print *, 'Hello, world!'; end

Here's another, slightly more complicated example.

program calculator\n  implicit none\n\n  real :: x, y, answer\n  character(1) :: choice\n\n  print *, 'x:'\n  read *, x\n  print *, 'y:'\n  read *, y\n\n  print *, '+, -, *, /:'\n  read *, choice\n  if (choice == '+') then\n    answer = x + y\n  end if\n  if (choice == '-') then\n    answer = x - y\n  end if\n  if (choice == '*') then\n    answer = x * y\n  end if\n  if (choice == '/') then\n    answer = x / y\n  end if\n\n  print *, 'Answer:', answer\n\nend program calculator

Starting right at the top, we have something new: implicit none. Added in Fortran 90, implicit none disables implicit typing defaults and all variables must be explicitly declared. In Fortran, implicit typing is the practice of assigning default types to variables based on the character a variable name begins with. Variables starting with I through N are INTEGERs, everything else is REAL. It is "a legacy of the past" and usage of an implicit none statement is "strongly advised" (implicit none - Fortran Wiki).

A common Fortran joke goes along the lines of “GOD is REAL, unless declared INTEGER"[4] because of implicit typing!

Moving on, we declare our first variables in this program.

real :: x, y, answer\ncharacter(1) :: choice

Here we are declaring x, y, and answer with the REAL type, and choice with the CHARACTER type. The REAL type stores floating point numbers[5], and CHARACTER... stores characters.

Next, we prompt the user for our x and y values.

print *, 'x:'\nread *, x\nprint *, 'y:'\nread *, y

Notice how we can take input from the user with read and assign it to a value with the read *, <variable> syntax. The asterisk here means read from the default input channel/file unit, which would be STDIN.

We do the same for prompting the user to select an operation.

print *, '+, -, *, /:'\nread *, choice

Finally, we use a series of basic if-statements to calculate our answer and display it in the terminal.

if (choice == '+') then\n  answer = x + y\nend if\nif (choice == '-') then\n  answer = x - y\nend if\nif (choice == '*') then\n  answer = x * y\nend if\nif (choice == '/') then\n  answer = x / y\nend if\n\nprint *, 'Answer:', answer

If we run this, we- wait. Did I even tell you how to compile a Fortran program yet?

How do I actually run this?

First, compile our calculator program with gfortran -o calculator calculator.f90 . Then you can run it with ./calculator. If you only instruct gfortran of the input file (gfortran calculator.f90), the default output executable will be named a.out.

Let's run our program now.

$ gfortran -o calculator calculator.f90\n$ ./calculator\n x:\n10\n y:\n2\n +, -, *, /:\n*\n Answer:   20.0000000

Pretty cool, huh?

A few improvements

Our calculator isn't perfect yet though. What if the user tries to divide by zero?

 x:\n10\n y:\n0\n +, -, *, /:\n/\n Answer:         Infinity

Probably not the answer you expected. Let's try to fix that.

if (choice == '/') then\n  if (y /= 0.0) then\n    answer = x / y\n  else\n    print *, 'Error: Division by zero is not allowed.'\n\tstop\n  end if\nend if

Here we use the inequality operator, /=, to check if the y value is zero. Now, if the user tries to divide by zero, we'll print an error message and use the stop statement to end the program.

Great. We got rid of the zero division mess, but our code isn't pretty at all. Who wants a bunch of if statements? We can simplify this using the select case statement (also known as the case statement).

select case (choice)\n  case ('+')\n    answer = x + y\n  case ('-')\n    answer = x - y\n  case ('*')\n    answer = x * y\n  case ('/')\n    if (y /= 0.0) then\n      answer = x / y\n    else\n      print *, 'Error: Division by zero is not allowed.'\n      stop\n    end if\n  case default\n    print *, 'Invalid choice. Please choose +, -, *, or /.'\n    stop\nend select

This also has the handy benefit of telling the user if they made an invalid choice while selecting the operation.

That’s just a quick introduction to a few modern Fortran features: declaring variables, printing and reading to and from the terminal, if and select case, and stop. Next time, we’ll talk more about where Fortran is actually used, cooler things you can build with it, and how the Fortran language & community are rapidly modernizing!


  1. Ironically, in the ~3-ish months since I started writing this article, ThePrimagen has recently said he "take[s] back everything i said about FORTRAN" — apparently having some interest in the language! ↩︎

  2. According to sources listed on Fortran's Wikipedia, the name might also have stood for Formula Translating System or just Formula Translation. ↩︎

  3. See The Rust programming language absolutely positively sucks : r/rust and Rust is a nightmare to learn coming from Java - community - The Rust Programming Language Forum. ↩︎

  4. The first letter of "GOD", a "G", is not within I through N and is therefore of the REAL type ("GOD is REAL"). ↩︎

  5. You can also use double precision for larger (more precise) floating point numbers. ↩︎

", "summary": "Who needs Rust when we have Fortran?", "date_published": "2024-01-31T00:00:00.000Z", "tags": [ "fortran" ] }, { "id": "/posts/npm-install-everything/index", "url": "https://uncenter.dev/posts/npm-install-everything/", "title": "The package that broke npm (accidentally)", "content_html": "

An update!

You might want to read the rest of the article first...

GitHub has now, a day after writing this, fully "disabled" (whatever that means) our everything-registry organization on NPM and GitHub; you can see the email they sent to me below. While I may not agree entirely with the reasoning they provided, I am very thankful that our personal accounts are still intact!

Email from GitHub Trust & Safety

\"Email

All of our scoped packages have been deleted, so unpublishing packages should no longer be an issue.

Another note; this story was picked up by some media outlets in the cybersecurity world! SC Media, Checkmarx, and BleepingComputer.

The aforementioned articles

\"Screenshot
\"Screenshot
\"Screenshot

\"A

Ten years ago, PatrickJS created the everything package on NPM, containing every package on the NPM registry in the first 5 years of the registry's existence. The package remained the same for years, but that all changed just a few days ago with a single tweet.

\"Screenshot

I saw the tweet on my timeline and made a quick PR to clean up a few things and help bring the repository up to speed. At the same time, Patrick had started an attempt to publish a 2.0.0 version of the package, but he discovered that there was now a 10 megabyte limit for the uncompressed size of a package. I made a comment about the issue and we quickly began brainstorming a solution.

Brainstorming...

We moved to Twitter DMs, and by this time others who saw Trash's tweet wanted to join — Hacksore, and Trash himself. We came up with a plan to divide the ~2.5m packages into "scoped" groups of packages; a group for packages starting with the letter "a", the letter "b", and the rest of the alphabet, and then the numbers "0" to "9", and finally an "other" category for anything else. Since each of these scoped packages would only be a subset of the total, they would easily pass the size limit, and the main everything package could just depend on each of these scoped packages.

\"A

Unforeseen issues

I began implementing some code to generate the required packages, and a few hours later we were ready to go- except we forget one thing. Or, rather, NPM didn't tell us one thing. It turns out that NPM has a limit for how many dependencies a package can have. And we were apparently way over it. NPM has no apparent documentation on this and the limit wasn't visible in any public source code (the registry is private), so Hacksore did some testing and discovered the limit to be 800 dependencies. At the current range of 90k to 300k dependencies per scoped package... we needed a new plan.

Back to the drawing board

I suggested a new, very basic plan: just split them into "chunks" (groups) of 800 dependencies.

\"A

This leaves 3246 groups though, and 3246 is still too many for our main everything package to hold. So we simply "chunk" the 3246 groups of 800 into groups of 800 again.

\"A

3...2...1... go!

Set on our new plan, we updated the code and triggered our GitHub Actions workflow...

\"Screenshot

It worked! The GitHub Action logs rolled in, one after another, as the packages slowly got published. We had a brief scare after realizing that GitHub Actions jobs and workflows have a maximum time that we might reach, but some quick calculations revealed that we had no cause for worry. Workflow jobs time out after 6 hours, and at the current rate of one package published every ~4.5 seconds, we could comfortably publish 4,800+ packages in that time.

\"Screenshot

We all went back to doing other things, and I checked the logs occasionally. Half an hour later though, we ran into a different problem... we had been rate limited. In 32 minutes, we had published 454 packages: the main everything package, all five "chunks", but only 448 "sub-chunks". It was only a fraction (roughly 14%) of everything (hah, pun intended) we needed to publish.

What next??

I made a quick fix before heading to bed to skip the packages we had already published, but we still didn't have any sort of plan to deal with rate limiting. Overnight between the 29th and the 30th, we settled on a new plan. We would periodically run a workflow that publishes as many packages as it can, and then the workflow saves the work it did to the repository so the next run can pick up where the last one left off. I replaced the sketchy manual intervention from the night before with a proper published.json file to keep track of the published packages, and initialized it. I wrote a release script that wrote back to published.json after publishing each package (I know, I know, this could be better) and added a step to the workflow to commit the changes after each run. After a few hiccups, it finally worked!

So it began. Throughout the day I (very irregularly) manually dispatched the workflow. For a while, we sat and waited. We even began an effort to actually run npm install everything (well, yarn add everything) and put up a Twitch stream of the installation on a virtual machine.

\"Screenshot

We also made a website! Many thanks to the rest of the contributors I have mentioned so far, but notably Evan Boehs for leading the charge and PickleNik made it look nice.

Finale

Finally, at 11:27PM, the final workflow run completed publishing the last 20 sub-chunks. All 5 chunks, 3246 sub-chunks, and the main everything package. In total, depending on over 2.5 million NPM packages!

A vulnerability?

The initial response to our endeavour was... not positive. People began coming to the repository, complaining about not being able to unpublish. What?! We looked into it, and it turns out that the issue was our usage of "star" versions; that is, specifying the version not as a typical semantic version in the format of X.Y.Z, but as *. The star means "any and all" versions of a package - here is where the issue lies. NPM blocks package authors from unpublishing packages if another package depends on that version of the package. But since the star is all versions, all versions of a package cannot be unpublished. This is usually harmless, but us (unintentionally) doing this on a large scale prevented anyone from unpublishing. We immediately reached out to GitHub; Patrick used his network and contacts to speak to people at GitHub, and we sent multiple emails to the support and security teams on NPM. Unfortunately, these events transpired over the holidays and the NPM/GitHub teams were not responding (likely out of the office). We continued to get harsh and rude comments from random people with a little too much time on their hands... one person even wrote a 1400 word rant about the unpublishing issue, despite us repeatedly telling them we could do nothing further.

Thankfully, on the night of January 2nd, GitHub reached out and let us know they were aware of the problem. On the 3rd of January, we received a notice that our GitHub organization had been "flagged" and our organization and repositories were hidden. Not what we wanted to see, but progress nonetheless.

They also began removing our organization's scoped packages on NPM, as we had suggested. The initial problem had been solved, but we are still waiting to see how NPM prevents this issue in the future. My two cents are that NPM should either a) prevent folks from publishing packages with star versions in the package.json entirely, or b) don't consider a dependent of a package if it uses a star version when tallying how many packages depend on a package for unpublishing.

Lastly, I want to apologize for anyone frustrated, annoyed, or just angry at us. We made a mistake, and we've owned up to it. This all started as a harmless joke and we had no intentions of breaking, abusing, or doing any sort of damage to the registry. In short we, uhh... fucked around and found out.

\"A

Thanks for reading this, and have a lovely day!

Now you can read the update if you haven't already!

", "summary": "How we made a package that depends on every single npm package... and completely broke npm in the process.", "date_published": "2024-01-03T00:00:00.000Z", "tags": [ "npm" ] }, { "id": "/posts/spellchecking-with-eleventy/index", "url": "https://uncenter.dev/posts/spellchecking-with-eleventy/", "title": "Spell-checking blog posts with cSpell", "content_html": "

Though I haven't written much on this blog, I wanted to add some basic spell-checking to my posts. I looked up "spell-checking markdown" and found an article by TJ Addison that explained how to do this with a tool called cSpell (the backbone of the somewhat popular Code Spell Checker VS Code extension). Definitely check out TJ's article for a more in-depth explanation of cSpell and how to use it, but here I'll explain how I set it up for my Eleventy blog.

Installing cSpell

You can use cSpell without installing it as a dependency by running it with npx:

npx cspell src/posts/**/*.md

But I opted for installing it permanently as a dev dependency and using an npm script to run it:

npm install cspell --save-dev
{\n\t// ...\n\t\"scripts\": {\n\t\t// ...\n\t\t\"spell\": \"cspell src/posts/**/*.md\"\n\t}\n}

Configuration

cSpell allows multiple filenames for its configuration but I went with cspell.config.js for consistency with my other config files (like eleventy.config.js and tailwind.config.js).

To start, set the version to 0.2 (currently always 0.2) and the language to either en or en-GB (both are included by default).

module.exports = {\n\tversion: '0.2',\n\tlanguage: 'en',\n};

There are over 26 other language dictionaries available, but I'm only writing in English so I didn't need to add any others.

An important step is to define specific words to exclude or flag. I told cSpell to ignore some 11ty-specific terminology and a few other words I use occasionally.

\twords: [\n\t\t'eleventy',\n\t\t'11ty',\n\t\t'shortcodes',\n\t\t'webc',\n\t],\n\tflagWords: [],

Dictionaries

In addition to the words property, you can also define dictionaries - just longer lists of words. I added a dictionary for my GitHub repositories to prevent those from being spell-checked if I ever write about them.

\tdictionaries: [\"repos\"],\n\tdictionaryDefinitions: [\n\t\t{ \"name\": \"repos\", \"path\": \"./utils/dicts/repos.txt\" },\n\t],

Instead of manually updating my repos.txt dict, I wrote a script to fetch my repositories from the GitHub API and write them to the file.

#!/usr/bin/env node\n\nconst fs = require('node:fs/promises');\nconst { join } = require('node:path');\n\nasync function getRepos() {\n\ttry {\n\t\tconst response = await fetch(\n\t\t\t'https://api.github.com/users/uncenter/repos',\n\t\t);\n\t\tconst json = await response.json();\n\n\t\tif (Array.isArray(json)) {\n\t\t\tfs.writeFile(\n\t\t\t\tjoin(__dirname, './dicts/repos.txt'),\n\t\t\t\tjson.map((repo) => repo.name).join('\\n'),\n\t\t\t);\n\t\t} else {\n\t\t\tthrow new TypeError('Invalid response content.');\n\t\t}\n\t} catch {\n\t\tconsole.error('[cspell:update] Something went wrong.');\n\t}\n}

If you're using Netlify, you can run this script and the spell-check script during the build process by adding it to the build command in your netlify.toml file (or the GUI on Netlify's website):

[build]\ncommand = \"node ./utils/get-repos.js && npm run spell && npm run build\"

\"Screenshot

Ignore patterns

Finally, the config file allows you to define regular expression patterns to ignore. I added patterns to ignore words in Nunjucks expressions, Markdown code blocks and inline code, and proper nouns (words that start with a capital letter).

\tignoreRegExpList: [\n\t\t'nunjucksExpression',\n\t\t'markdownCodeBlock',\n\t\t'markdownInlineCode',\n\t\t'properNouns',\n\t],\n\tpatterns: [{% raw %}\n\t\t{\n\t\t\tname: 'nunjucksExpression',\n\t\t\tpattern: /{%.*?%}/gis,\n\t\t},\n\t\t{\n\t\t\tname: 'markdownCodeBlock',\n\t\t\tpattern: /`{3}[\\s\\S]*?`{3}(?=\\n|$)/gi,\n\t\t},\n\t\t{\n\t\t\tname: 'markdownInlineCode',\n\t\t\tpattern: /`[^`]*`/gi,\n\t\t},\n\t\t{\n\t\t\tname: 'properNouns',\n\t\t\tpattern: /(?<=\\s|^|[^\\w\\s])[A-Z][a-z]+(?=\\s|$|[^\\w\\s])/g,\n\t\t},\n\t],{% endraw %}

I'm surprised that there isn't a pattern for Markdown code blocks by default; I was having issues with common JavaScript libraries and methods being flagged as typos. Additionally, I use a few custom shortcodes that kept getting flagged as a typo, so the nunjucksExpression pattern was a must.

Ignore words in front matter

One other neat thing about cSpell is that you can define words to ignore per file. For example, you could ignore the word supercalifragilisticexpialidocious in just one file by adding cSpell:ignore supercalifragilisticexpialidocious as a comment at the top of the file:

---\ntags: [...]\ntitle: Magna voluptate officia cillum Lorem proident.\ndescription: Cupidatat excepteur ullamco laboris in veniam qui officia tempor aliquip et commodo.\ndate: 2000-01-01\n# cSpell:ignore supercalifragilisticexpialidocious\n---

You can also use spell-checker:ignore instead of cSpell:ignore. For more information on per document settings, see https://cspell.org/configuration/document-settings/#in-document-settings.

You don't have to put it in the front matter, but I like to keep my posts as clean & organized as possible and it looks nice there.

Let me know if you have any questions or suggestions!

", "summary": "A little magic to help catch typos in your blog posts.", "date_published": "2023-03-23T00:00:00.000Z", "tags": [ "eleventy", "blogging" ] } ] }