Repository: alii/website
Branch: master
Commit: 4c176843e38e
Files: 68
Total size: 149.4 KB
Directory structure:
gitextract_snw5_0ev/
├── .eslintrc.json
├── .github/
│ └── workflows/
│ └── lint.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── SECURITY.md
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── prettier.config.js
├── src/
│ ├── blog/
│ │ ├── 2022/
│ │ │ ├── 01/
│ │ │ │ ├── mochip/
│ │ │ │ │ └── mochip.tsx
│ │ │ │ ├── serverless-discord-oauth/
│ │ │ │ │ └── serverless-discord-oauth.tsx
│ │ │ │ └── zero-kb-blog/
│ │ │ │ └── zero-kb-blog.tsx
│ │ │ ├── 03/
│ │ │ │ └── open-source/
│ │ │ │ └── open-source.tsx
│ │ │ └── 08/
│ │ │ └── strict-tsconfig/
│ │ │ └── strict-tsconfig.tsx
│ │ ├── 2023/
│ │ │ └── wtf-esm/
│ │ │ └── wtf-esm.tsx
│ │ ├── 2025/
│ │ │ └── ambient-declarations/
│ │ │ └── ambient-declarations.tsx
│ │ ├── Post.ts
│ │ └── posts.ts
│ ├── components/
│ │ ├── blog-footer.tsx
│ │ ├── blog-post-list.tsx
│ │ ├── external-link.tsx
│ │ ├── message.tsx
│ │ ├── note.tsx
│ │ ├── stats.tsx
│ │ └── syntax-highligher.tsx
│ ├── global.d.ts
│ ├── globals.css
│ ├── hooks/
│ │ ├── layout.ts
│ │ ├── use-did-initial-page-animations.ts
│ │ ├── use-first-ever-load.ts
│ │ ├── use-isomorphic-value.ts
│ │ └── use-lerp-transform.ts
│ ├── pages/
│ │ ├── 404.tsx
│ │ ├── [slug].tsx
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── _error.tsx
│ │ ├── api/
│ │ │ ├── contact.ts
│ │ │ ├── map.ts
│ │ │ ├── oauth/
│ │ │ │ └── [platform]/
│ │ │ │ ├── callback.ts
│ │ │ │ └── redirect.ts
│ │ │ ├── oauth.ts
│ │ │ ├── og.tsx
│ │ │ ├── ping.ts
│ │ │ └── posts.ts
│ │ ├── blog.tsx
│ │ ├── demos/
│ │ │ └── serverless-discord-oauth.tsx
│ │ ├── experiments/
│ │ │ ├── index.tsx
│ │ │ ├── morphing-shapes.tsx
│ │ │ └── rekordbox-history-parser.tsx
│ │ ├── index.tsx
│ │ ├── monzo/
│ │ │ └── dashboard/
│ │ │ └── index.tsx
│ │ └── stats.tsx
│ ├── server/
│ │ ├── api.ts
│ │ ├── apple-maps.ts
│ │ ├── env.ts
│ │ ├── monzo.ts
│ │ └── sessions.ts
│ └── utils/
│ ├── constants.ts
│ ├── discord.ts
│ ├── lists.ts
│ ├── timers.ts
│ └── types.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-namespace": "off"
}
}
================================================
FILE: .github/workflows/lint.yml
================================================
name: 'lint'
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
name: 'Lint'
runs-on: ubuntu-latest
steps:
- name: Begin CI...
uses: actions/checkout@v2
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
env:
CI: true
- name: Lint
run: bun lint
env:
CI: true
================================================
FILE: .gitignore
================================================
/node_modules
/.pnp
.pnp.js
/build
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.log
.vercel
.idea
.eslintcache
.next
out
*.tsbuildinfo
# Yarn
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# bun
*.bun
# Cloud9 IDE files
.c9
================================================
FILE: .prettierignore
================================================
.next
dist
build
out
node_modules
.yarn
.git
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
🦄 alii/website
================================================
FILE: SECURITY.md
================================================
# Security Policy
Don't use this and expect it to be totally secure, but on that note, it's just a React app.
================================================
FILE: next-env.d.ts
================================================
///
///
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
================================================
FILE: next.config.mjs
================================================
import { config as dotenv } from 'dotenv';
// @ts-check
/** @type {import("next").NextConfig} */
const config = {
env: dotenv().parsed,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'i.scdn.co',
pathname: '/image/*',
},
{
protocol: 'https',
hostname: 'snapshot.apple-mapkit.com',
pathname: '/api/v1/snapshot',
},
],
},
async redirects() {
return [
{
source: '/outro',
destination: 'https://www.youtube.com/watch?v=HeF11Av9WuU',
permanent: true,
},
{
source: '/desu',
destination: 'https://www.youtube.com/watch?v=HotGxCSas6A',
permanent: true,
},
{
source: '/10',
destination: 'https://youtu.be/G5HcvgepK-I',
permanent: true,
},
{
source: '/lulzsec',
destination: 'https://www.youtube.com/watch?v=DurOYPdXyF4',
permanent: true,
},
{
source: '/wheels',
destination: 'https://www.youtube.com/watch?v=9xRFN2i1cwQ',
permanent: true,
},
{
source: '/letterone',
destination: 'https://hyperfollow.com/alistair6/letter100-3',
permanent: true,
},
{
source: '/live-25-01-2024',
destination: 'https://www.youtube.com/watch?v=OvTy9xYH7LA',
permanent: true,
},
{
source: '/live-02-02-2024',
destination: 'https://youtube.com/watch?v=zEoTeUEElZc',
permanent: true,
},
{
source: '/live-04-07-2024',
destination: 'https://youtube.com/watch?v=-XsKN44b7ho',
permanent: true,
},
];
},
};
export default config;
================================================
FILE: package.json
================================================
{
"name": "website",
"version": "1.0.0",
"repository": "git@github.com:alii/website.git",
"author": "Alistair Smith ",
"license": "Apache-2.0",
"private": true,
"packageManager": "bun@1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.2.2",
"@types/common-tags": "^1.8.4",
"@types/cookie": "^1.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/jwa": "^2.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"discord-api-types": "^0.38.42",
"eslint": "9.27.0",
"eslint-config-next": "15.1.8",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"sharp": "^0.34.5",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2"
},
"dependencies": {
"@altano/satori-fit-text": "^1.0.2",
"@c-side/next": "^1.0.0",
"@marsidev/react-turnstile": "^1.5.0",
"@next/third-parties": "^15.5.14",
"@otters/monzo": "^2.1.2",
"@prequist/lanyard": "^1.1.0",
"@tailwindcss/typography": "^0.5.19",
"@vercel/og": "^0.6.8",
"alistair": "^1.17.0",
"axios": "^1.14.0",
"bwitch": "^0.3.0",
"clsx": "^2.1.1",
"common-tags": "^1.8.2",
"cookie": "^1.1.1",
"dayjs": "^1.11.20",
"dotenv": "^16.6.1",
"envsafe": "^2.0.3",
"framer-motion": "^12.38.0",
"jsonwebtoken": "^9.0.3",
"jwa": "^2.0.1",
"next": "^16.2.1",
"nextkit": "^3.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.6.0",
"react-syntax-highlighter": "^15.6.6",
"satori": "^0.13.2",
"use-lanyard": "^1.7.0",
"uuid": "^11.1.0",
"zod": "^3.25.76"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};
================================================
FILE: prettier.config.js
================================================
const alistair = require('alistair/prettier');
module.exports = {
...alistair,
plugins: ['prettier-plugin-tailwindcss'],
};
================================================
FILE: src/blog/2022/01/mochip/mochip.tsx
================================================
import {stripIndent} from 'common-tags';
import {Highlighter} from '../../../../components/syntax-highligher';
import {Post} from '../../../Post';
import emailFromColin from './email-from-colin.png';
import gmeet from './gmeet.png';
import goodbyeMochip from './goodbye-mochip.png';
import hegartyTimeExploit from './hegarty-time-exploit.jpeg';
import mochipLanding from './landing.jpeg';
export class Mochip extends Post {
public name = 'Avoiding homework with code (and getting caught)';
public slug = 'mochip';
public date = new Date('6 Jan 2022');
public excerpt = 'The eventful tale of me getting fed up with my homework';
public hidden = false;
public keywords = [
'school',
'homework',
'clout',
'hegarty maths',
'educake',
'homework hack',
'maths homework',
'programming',
];
public render() {
return (
<>
Avoiding homework with code (and getting caught)
Back in 2020, my school used a few online learning platforms that allowed
professors/teachers to assign homework to students. I, as a lazy developer, wanted to
spend more time playing games and writing code, especially when everyone was spending
their time at home because of lockdown. I started writing this post in January of 2022,
but I put off publicizing it for a while. It has been long enough since this all happened,
so please sit back and enjoy.
The back story
Let's set the scene. 2018, my school introduces a new online homework platform for
students. It's called HegartyMaths and it does a lot. It's fairly simple, teachers
choose a topic to set for us as homework, with that we get a 10-15 minute
tutorial/informational video on the subject (of which we have to write down notes whilst
watching) and a shortish quiz to complete after finishing the video. It's a lot of work,
especially the quiz, and in the worst cases can take up to an hour to complete one topic
(bad).
Mostly, software engineers are rather lazy individuals. We tell metal how to do stuff for
us. Homework then, naturally, is an arduous task for a developer who is still at school.
So, still 2018, a close friend of mine by the name of{' '}
Scott Hiett
{' '}
and I decided to do something about the Hegarty situation. We started to reverse engineer
the frontend app and eventually came up with a Tampermonkey userscript that would glitch
the embedded YouTube player to say that we'd watched the video at least 1x. Crucially, our
teachers could see how many times we'd watched the video, so being able to skip up to 20
minutes of homework time was especially useful – and it was a lot of fun to build too.
So we flexed it on our Snapchat stories and had our school friends message us to use it
blah blah. We eventually figured out that we could also set it to be watched over 9999x
times; every time we did that our accounts were reset by the Hegarty team.
The first email
After this, we got in contact with our Math teacher in November of 2018 and got her to
send an email to HegartyMaths informing them of our petty exploit and they got back to us
very quickly.{' '}
I don't have the original email anymore but I distinctly remember it saying something
along the lines of "Stop trying to hack our platform and get back to doing your
homework."
{' '}
Edit: While writing this, I was able to uncover the deleted email from a photo we had
taken of it in 2020. See below{' '}
(certain details redacted for obvious reasons):
This response excited us a bit, as they were now aware of us messing around with the site
and they had no intention of fixing the minor vuln we had anyway, so we kept using it. We
had tried to build a script to answer the questions for us, but it was too much work at
the time (complex data structures, weird API responses, etc etc).
Educake
For a while, students had access to another platform called Educake. Similar to
HegartyMaths but targeting Biology, Chemistry and Physics. There was no video to watch at
the beginning. We'd used it for a few years, in fact since I joined the school, but I'd
never thought about reversing until all of this began.
One common factor between Hegarty and Educake is that they immediately give you the
correct answer if you got a question wrong. We took advantage of this and wrote a small
node/mongo app & tampermonkey script to detect when a user was on a quiz page, answer
every question with a random number, and then store the correct answer in mongodb. I don't
have the original source but the TamperMonkey script was probably something like
the following:
{stripIndent`
const guess = Math.random();
const result = await post('/api/answer', {
body: {
answer: guess,
},
});
await post('http://localhost:8080/save', {
body: {
question_id: question.id,
answer: result.success ? guess : result.correct_answer,
},
});
// Go to next question and repeat code above
nextQuestion();
`}
As you can see, it was quite literally a loop through every question, saving the correct
answer as we got it and moving on. Eventually I added a few more features to fetch from
the database if we already had the right answer (meaning we don't answer{' '}
Math.random every time) and also I added in support for multiple choice (so
that we actually pick one of the possible answers rather than making it up – however I was
surprised that the Educake backend would allow an answer that wasn't even in the possible
choices).
Now working on the project solo, I decided it would be time to build a nice UI for it all
and bundle it all into a simple Tampermonkey script for both flexing rights on Snapchat
(people constantly begging me to be able to use it was certainly ego fuel I hadn't
experienced before) and also for myself to get out of homework I didn't want to do.
The end result? A ~200 line codebase that scooped up all questions and answers on the site
that could repeatedly get 100% on every single assignment and a 15mb mongo database.
Below is a small video of what it all looked like. It also demonstrates a feature I added
allowing for a "target percentage" — meaning users could get something other than 100% to
look like more real/human score. Video was recorded on my Snapchat in November 2019.
Hegarty 2
The success of this script, along with pressure from my peers, led me to gain a lot of
motivation to start working on reversing Hegarty again. I reached out to an internet
friend who, for the sake of his privacy, will be named "Jake." He also used HegartyMaths
at his school and was in the same boat as me trying to avoid doing our homework. Together,
we managed to figure out how to answer many varying types of questions, including multiple
choice and ordered answers, resulting in a huge amount of data stored. We had sacrificial
user accounts and managed to answer 60,000 questions in a couple minutes, rocketing our
way to the top of the HegartyMaths global leaderboard.{' '}
Would like to give a special shoutout to Boon for lending us his login and letting us
decimate his statistics.
Together, Jake and I scraped the entirety of Hegarty's database and now had a JSON file
that could be argued to be worth as much as Hegarty the company itself due to the entire
product quite literally being the database we had copied.
With this file, I wanted to take it a step further and allow my friends and other people
to make good use of it without directly giving out the database (irresponsible)... And
here Mochip was coined.
Mochip
So, where does Mochip tie in to this? Mochip was a Chrome extension, a collection of both
our scraped Hegarty and scraped Educake databases sat behind a TypeScript API and a small
React app. Hosted on Heroku free tier and MongoDB Atlas free tier, users could log in,
enter a question (from either site) and get back a list of answers Mochip has for that
question. Here's what the landing page looked like:
In the screenshot we can see a few stats on the right like total estimated time saved and
how long you've had your account for. We gamified it a little just to keep people engaged
Our chrome extension was made for Educake as they disabled copying question text with the
clipboard. We re-enabled that just by clicking a button that was injected into the UI. The
chrome extension is no longer on the chrome web store, but we've found that mirrors still
have listings that we can't get taken down:{' '}
extpose.com/ext/195388
Our userbase grew so big that we ended up with a Discord server and even our own listing
on Urban dictionary — I'm yet to find out who made it!{' '}
urbandictionary.com/define.php?term=mochip
Eventually we "rebranded" as I wanted to disassociate my name from the project.
Unfortunately I do not have any screenshots from this era to show. I made an alt discord
account and a few announcements saying we'd "passed on ownership" however this ineveitably
only lasted for a couple weeks before we were rumbled.
Crashing down
All good things must come to and end, and Mochip's did after Scott posted about Mochip on
his reddit account. Like any good CEO, Colin searches his company every now and then on
Google to see what people are saying or doing and unfortunately came across our reddit
post. He signed up (although under a different email) and tested out the app and was
shocked to see it working. Shortly after this I received an email from Colin directly. See
below
I was upset but also a little content — it was sort of validation that I'd successfully
made it and that catching the attention of Colin himself was sort of a good thing. We
quickly scheduled a Google Meet, also inviting Scott, and I had one of the most memorable
conversations of my life. I am extremely grateful for the advice Colin gave us in the
call.
I'd like to give a special thank you to the legendary Colin Hegarty for his kindness and
consideration when reaching out to me. Things could have gone a lot worse for me had this
not been the case. HegartyMaths is a brilliant learning resource and at the end of the
day, it's there to help students learn rather than be an inconvenience.
Shortly after, Colin reached out to the Educake team, who we also scheduled a call with.
We explained our complete methodology and suggested ways to prevent this in the future.
The easiest fix from our point of view would be to implement an easy rate limit with Redis
that would make it wildly infeasible to automate a test. The other thing we suggested was
to scramble IDs in the database to invalidate our cloned database as much as
possible (e.g. we only had the Hegarty IDs, so we could no longer reverse lookup a
question).
Thank you for reading, truly. Mochip was a real passion project and I had a wild time
building it. ⭐
Edit 23 Sept, 2022: After making this post public, I posted this on HackerNews and
amazingly sat in the #1 spot overnight. This site consequently received a lot of traffic,
and I served almost 1.5TB in just shy of 6 hours. Some of the employees at Sparx (the
parent company of HegartyMaths) ended up seeing this and forwarded it to Colin. A few
minutes ago I just received a really lovely email from Mr Hegarty himself with the subject
"Congrats to you!" I am so grateful for the kindness and consideration Colin has shown
Scott and me, so if you are a teacher reading this, then please consider using
HegartyMaths at your school! This was the happy ending!
>
);
}
}
================================================
FILE: src/blog/2022/01/serverless-discord-oauth/serverless-discord-oauth.tsx
================================================
import {stripIndent} from 'common-tags';
import Link from 'next/link';
import {Highlighter, Shell} from '../../../../components/syntax-highligher';
import {Post} from '../../../Post';
import discordOAuthDashboardImage from './discord-oauth-dashboard.png';
export class ServerlessDiscordOAuth extends Post {
public name = 'Serverless Discord OAuth with Next.js';
public slug = 'serverless-discord-oauth';
public date = new Date('2 January 2022');
public excerpt = "Implementing basic Discord OAuth on Vercel's serverless platform";
public hidden = false;
public keywords = ['serverless', 'vercel', 'discord', 'oauth', 'node'];
public render() {
return (
<>
Serverless Discord OAuth with Next.js
OAuth is a brilliant solution to a difficult problem, but it can be hard to implement,
especially in a serverless environment. Hopefully, this post will help you get started.
Live demo:{' '}
/demos/serverless-discord-oauth
The setup
Firstly, we're going to need to create a Next.js with TypeScript app. Feel free to skip
this if you "have one that you made earlier."
bun create next-app my-app --typescript
Dependencies
We will be relying on a few dependencies, the first is discord-api-types{' '}
which provides up-to-date type definitions for Discord's API (who could've guessed). We'll
also need axios (or whatever your favourite http lib is) to make requests to
Discord. Additionally, we'll be encoding our user info into a JWT token & using the cookie
package to serialize and send cookies down to the client. Finally, we'll use{' '}
dayjs for basic date manipulation and pathcat to easily build
urls with query params.
{stripIndent`
bun add axios cookie pathcat dayjs jsonwebtoken
bun add --dev discord-api-types @types/jsonwebtoken @types/cookie
`}
Code
Dope, you've made it this far already! Let's get some code written
Firstly, you're going to want to open up the folder pages/api and create a
new file. We can call it oauth.ts. The api folder is where Next.js will
locate our serverless functions. Handily, I've written a library called{' '}
nextkit that can assist us with this process but for the time being it's out
of scope for this post – I'll eventually write a small migration guide.
{stripIndent`
import type {NextApiHandler} from 'next';
import type {RESTGetAPIUserResult} from 'discord-api-types/v8';
import {serialize} from 'cookie';
import {sign} from 'jsonwebtoken';
import dayjs from 'dayjs';
import {pathcat} from 'pathcat';
import axios from 'axios';
// Configuration constants
// TODO: Add these to environment variables
const CLIENT_ID = 'CLIENT_ID';
const CLIENT_SECRET = 'CLIENT_SECRET';
const JWT_SECRET = 'CHANGE ME!!!';
// The URL that we will redirect to
// note: this should be an environment variable
// but I'll cover that in part 2 since
// it will work fine locally for the time being
const REDIRECT_URI = 'http://localhost:3000/api/oauth';
// Scopes we want to be able to access as a user
const scope = ['identify'].join(' ');
// URL to redirect to outbound (to request authorization)
const OAUTH_URL = pathcat('https://discord.com/api/oauth2/authorize', {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope,
});
/**
* Exchanges an OAuth code for a full user object
* @param code The code from the callback querystring
*/
async function exchangeCode(code: string) {
const body = new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code,
scope,
}).toString();
const {data: auth} = await axios.post<{access_token: string; token_type: string}>(
'https://discord.com/api/oauth2/token',
body,
{headers: {'Content-Type': 'application/x-www-form-urlencoded'}},
);
const {data: user} = await axios.get(
'https://discord.com/api/users/@me',
{headers: {Authorization: \`Bearer \${auth.access_token}\`}},
);
return {user, auth};
}
/**
* Generates the set-cookie header value from a given JWT token
*/
function getCookieHeader(token: string) {
return serialize('token', token, {
httpOnly: true,
path: '/',
secure: process.env.NODE_ENV !== 'development',
expires: dayjs().add(1, 'day').toDate(),
sameSite: 'lax',
});
}
const handler: NextApiHandler = async (req, res) => {
// Find our callback code from req.query
const {code = null} = req.query as {code?: string};
// If it doesn't exist, we need to redirect the user
// so that we can get the code
if (typeof code !== 'string') {
res.redirect(OAUTH_URL);
return;
}
// Exchange the code for a valid user object
const {user} = await exchangeCode(code);
// Sign a JWT token with the user's details
// encoded into it
const token = sign(user, JWT_SECRET, {expiresIn: '24h'});
// Serialize a cookie and set it
const cookie = getCookieHeader(token);
res.setHeader('Set-Cookie', cookie);
// Redirect the user to wherever we want
// in our application
res.redirect('/');
};
export default handler;
`}
Cool! This is the bare bones that we will need to start writing our OAuth. It's quite a
lot to bite, but if you break it down line by line and read the comments, it should be
fairly self-explanatory. We're still missing a few prerequisites to tell Discord who we
are: the client id and secret.
Copy and paste your client ID into your oauth.ts file
Copy and paste your client secret into your oauth.ts file
Add your redirect URI (http://localhost:3000/api/oauth) on the dashboard
Make sure all your changes are saved and then we are ready to test it out for the first
time!
Testing it
Awesome, we've got everything setup correctly. Now we can give it a quick spin. You can
start your Next.js development server if you haven't already by running{' '}
bun dev in your terminal, you should be able to navigate to{' '}
localhost:3000/api/oauth
{' '}
and successfully authenticate.
Afterwards, if you open up your browser's devtools and check for the cookie section, you
should see a cookie by the name of token – this is ours! Copy the value and
paste it into{' '}
jwt.io
{' '}
to decode it and see your details encoded inside it!
Why JWT?
We've picked JWT because it lets us store information on the client side where only the
server can mutate and verify that the server created it. This means users can't modify the
data inside a JWT token, allowing the server to make guarantees about the data encoded.
Environment variables
Okay, we're almost there. Final stretch
Right now, we have our constants defined in this file which is fine for prototyping but it
now means that if you want to push your code to github, for example, your client secret
and perhaps other private information will be publicly available on your project's
repository! The solution? Environment varibles.
Environment variables are bits of information that are provided to a process at runtime.
It means we don't have to store secrets inside our source code.
Thankfully, Next.js makes it super easy for us to use environment variables with something
called an env file.
Creating our env file
Firstly, make a new file in your project's file structure called .env and add
the content below. The format for env files is KEY=value. You can use{' '}
openssl rand -hex 64 to generate a JWT secret.
And that should be all good! I'll be writing a part two and three later on that will cover
accessing the JWT from the server and also deployment to vercel.
Thanks for reading!
>
);
}
}
================================================
FILE: src/blog/2022/01/zero-kb-blog/zero-kb-blog.tsx
================================================
import {stripIndent} from 'common-tags';
import {Note} from '../../../../components/note';
import {Highlighter} from '../../../../components/syntax-highligher';
import {Post} from '../../../Post';
export class ZeroKbBlog extends Post {
public name = 'The 0kb Next.js blog';
public slug = 'zero-kb-nextjs-blog';
public date = new Date('6 Jan 2022');
public hidden = false;
public excerpt = 'How I shipped a Next.js app with a 0kb bundle';
public keywords = ['nextjs', 'zero', 'bundle', 'nextjs-zero-bundle', 'unstable_runtimeJS'];
public render() {
return (
<>
The 0kb Next.js blog
This only applies to apps using the pages directory of Next.js as App Dir
(released in v13) does not support the settings used here. RSCs offer a similar idealogy
of rendering components on the server only, while also allowing for client side JS.
Ok so the title was a liiittle bit clickbaity, but it's not technically a lie. This
entire website has zero JavaScript on every single page... a Next.js app with zero
client side JS. How can this be possible?
Some context
Next.js is a huge abstraction of react-dom/server and other helpful utilities
for building server side rendered apps powered by React. It's really easy to get started
with, and features things like file system routing, statically generated content and much
much more. The most important thing to understand here is that there's a server...
For those of you who are not familiar with React, it's a JavaScript framework for
building user interfaces. It handles the view layer of an app and is used to render the
UI. Next.js takes this a step further and allows you to write your own view layer to have
the first render performed on a server, which allows for a lot of performance and UI
optimizations because we can ship back a lot less to the client (foreshadowing).
Runtime JS
With something called PageConfig, we can instruct Next.js to supply zero
runtime JS to the client. It comes with a couple of trade-offs, but the general idea is
that we perform our first render on the server and the resulting HTML and CSS is "frozen"
and sent down to the client over the wire. Zero JavaScript runtime in the browser, no{' '}
<script> tags in sight!
What's the catch?
As the saying goes, there's no such thing as a free lunch. As with anything, doing this
comes with some trade-offs. For example, Next has a lot of built-in React components that
can speed up not only your app, but also development speed. Using the zero kb mode, we
unfortunately cannot make full use of the next/link component; a component
that prefetches pages so that clicks on links don't cause a full page reload.
Additionally, we cannot use next/image as this also requires some minimal
runtime JavaScript (a regular img works just fine). This also means zero
state updates or literally any JavaScript that would've otherwise been bundled can run.
So if you're fine with that, then you can go ahead and enable it. No more worrying about
worrying about installing huge npm packages and watching your user count drop in realtime.
See your gorgeous website in pure static HTML & CSS. Gone are the days of
bundlephobia.com... I bet at this point you are itching to know how to enable it. Well,
here's a quick example:
{stripIndent`
import type {PageConfig} from 'next';
export const config: PageConfig = {
unstable_runtimeJS: false,
};
export default function IndexPage() {
return
This page has no JavaScript!
;
}
`}
And boom! just like that, 0kb bundle. If you are going to use this though, just bear in
mind that as well as the trade offs mentioned above, you literally cannot use any
JavaScript in the client anymore for this page. Zero. Nada. Null. Void. That means no
state updates, no network requests, no useEffect, no event listeners, no timers. Nothing.
>
);
}
}
================================================
FILE: src/blog/2022/03/open-source/open-source.tsx
================================================
import {Post} from '../../../Post';
export class OpenSource extends Post {
public name = 'Open Source';
public slug = 'open-source';
public date = new Date('20 Mar 2022');
public hidden = true;
public excerpt = 'Thoughts & feelings on Open Source';
public keywords = ['Developer', 'Open Source', 'GitHub', 'sponsorships'];
public render() {
return (
<>
I really do love open source. I love being able to build software that I know people will
be able to make great use of. I love that we can extend already existing open source
software & I love that we're able to put licenses on our code and explain where and
how you can use it.
But open source is a truly double-edged sword. There are always stories on Twitter with
people explaining how they were taken advantage of, or don't have the resources to keep
maintaining a project.
Recently, a library called faker.js was nuked by the author in an attempt to
make a "political" point. Wildly unsuccessful and, in my opinion, extremely immature.
However, the community responded really quickly and quickly forked the project into{' '}
@faker-js/faker. This meant developers could easily switch their project over
to use a 100% API compatible, up-to-date and community-managed version of the project. The
original author, Marak Squires, ended up deleting the original repo. As well as that,
Squires also placed malicious code in another project of his, colors.js, that
would infinite loop the victim's computer.
But there was a reason for this. Thanks to the wonderful archive.org, we're able to see
old and deleted posts explaining why this had happened — and it's a common frustration
within the community. Famously, Marak mentions he will do{' '}
No more free work
{' '}
and he's also written a good{' '}
few hundred words
{' '}
on the topic, too.
Okay, so he's frustrated with people taking Open Source for granted. What's the solution
here? Well, fantastic companies like OpenCollective and GitHub are taking the initiative
to provide a direct, low-fee method of sponsoring open source projects. Big companies like
Discord, Stripe, and Microsoft have all sponsored small and large projects and sometimes
they get their name on the README in return. At the moment, we're not quite there
completely, but we're definitely heading in the right direction.
Alternatively, a recent JavaScript library popped up called motion (
motion.dev) which caused a bit of a stir in the
community for shaking things up a little bit with regards to their monetization strategy.
The library itself exists on npm and can be installed as you would any other node module,
except there is no GitHub URL for the package..? Taking a look at the README on npm says
the following:
Become a sponsor and get access to the private Motion One repo. File issues, read the
changelog and source code, and join discussions that help shape the future of the API.
Okay, interesting, so you can use the module and read the documentation for free, but
accessing the source code requires somewhat of a paid subscription. There is valid
incentive for companies to do this as it allows them to not only audit the codebase (under
security concerns), but also it allows for reading the source code to see how everything
works and learn a lot.
So what's the downside to this? Well, one of the reasons traditional open source is
brilliant is because it allows anybody to freely see how something works — so assuming
that is true, could we even call motion an open source project? What's more,
a lot of individual developers are students or kids learning and as such, they're not
financially able to support projects. On top of that, they are the generation we want to
be educating the MOST about programming and so immediately cutting them off is definitely
not a win.
One final example of open source being painful has got to be Fastify. Fastify's creator is
active on Twitter very often and there have been a few Tweets describing some headaches
they have had to go through as a team to get Fastify to be successful. Keeping it short,
but one thing they have done extensively has been advertising Fastify, which has kept it
mainstream and allowed for more users and therefore sponsorships. Matteo Collina,
Fastify's creator, has explained that had there not been the advertising done, Fastify
would not be as maintained, if at all, as it is today. Right now, Fastify has two core
maintainers and 16 on the core team overall.
There is plenty of room for innovation in this space. I'm excited to see where GitHub
sponsors and OpenCollective go and if we can see some large tech companies spreading the
word about open source.
By the way, I do accept sponsorships via GitHub, so if you enjoy my work or writing then
please consider any spare VC funding you have just raised 😊{' '}
github.com/sponsors/alii
Thanks for reading!
>
);
}
}
================================================
FILE: src/blog/2022/08/strict-tsconfig/strict-tsconfig.tsx
================================================
import {stripIndent} from 'common-tags';
import {Highlighter} from '../../../../components/syntax-highligher';
import {Post} from '../../../Post';
export class StrictTSConfig extends Post {
public name = 'A strict TSConfig';
public slug = 'strict-tsconfig';
public date = new Date('08 Sep 2022');
public hidden = false;
public excerpt = 'The strictest TypeScript configuration possible. "Look ma, no errors!"';
public keywords = ['strict', 'tsconfig', 'typescript'];
public render() {
return (
<>
Here's a very strict TypeScript configuration file. All the safety checks you could ever
want. "Look ma, no errors!"
>
);
}
}
================================================
FILE: src/blog/2023/wtf-esm/wtf-esm.tsx
================================================
import {stripIndent} from 'common-tags';
import {ExternalLink} from '../../../components/external-link';
import {Note} from '../../../components/note';
import {Highlighter} from '../../../components/syntax-highligher';
import {Post} from '../../Post';
export class WTFESM extends Post {
public name = 'WTF, ESM!?';
public slug = 'wtf-esm';
public date = new Date('2023-04-03');
public hidden = true;
public excerpt =
'I recently Tweeted about publishing a dual ESM and CJS package to npm. It got a lot of likes, and here is why that matters.';
public keywords = ['javascript', 'esm', 'typescript', 'publish', 'package', 'npm', 'node'];
public render() {
return (
<>
WTF, ESM!?
I{' '}
recently Tweeted
{' '}
about publishing a dual ESM and CJS package to npm. It got a lot of likes, and here is why
that matters. It's important that you understand that I was wrong in my Tweet, and things
are arguably easier or more difficult than they seem. This is the current state of
publishing a JS package.
Preface
I am so incredibly grateful for the absolutely wonderful{' '}
Andrew Branch, who took a lot
of time out of his vacation to correct my Tweet and wrote{' '}
this excellent thread
. A lot of this blog post is regurgitated text of how I interpreted his Tweets.
Andrew works on TypeScript itself at Microsoft, specifically on auto imports and modules.
It's likely he's the only person on the planet who knows exactly how this works inside
out. It's been rumoured that he will be summoned if you utter "module resolution" three
times in the dark. Thank you Andy - you're truly a super star ⭐💖
Right now, it's extraordinarily clear we are experiencing growing pains in our great
migration to ECMAScript Modules. Below is the part of my package.json that I
posted.
As mentioned above, I made some mistakes here. First of all, it's important to
differentiate between what is runtime code that engines will understand (what is
JavaScript), and what is type definitions (what is TypeScript). This (seems) easy enough,
we can see clearly that there are two types fields. One is under the{' '}
. entrypoint for exports, the other is at the root. Let's break
it down.
Where did I go wrong?
It's pretty hard to get a conclusive answer from the "crowd" of JavaScript developers
about the best way to publish a package to npm. Everyone has conflicting answers & we
all seem to be following what already exists on GitHub and npm. There are lots of packages
that are published technically incorrectly but used and installed by millions of people.
This means a lot of packages follow what I'm calling a colloquial standard. Here's what I{' '}
*thought to be true*, and so do most other devs...
Below is not the correct way to publish a package to npm. This is what I thought was
correct at the time of Tweeting.
.types at the root is for TypeScript type definitions. A single{' '}
.d.ts file can define all exported symbols in your package.
.main is for CJS before exports existed. You can emit a single
CJS compatible file that can be consumed by (legacy) runtimes.
.module is for an ESM entrypoint before exports existed. This
was mostly used by bundlers like Webpack, and has never been part of any standard. It's
superseded by exports, but it might be good to keep in order to support the
older bundlers.
.exports is the new standard for defining entrypoints for your package. It
is a map of entrypoints to files. The . entrypoint is the default
entrypoint. We also include ./package.json so the package.json file is also
accessible. The exports field is supported in modern runtimes. Node has
supported it since v16.0.0 - for this reason, you will see exports{' '}
sometimes referenced as node16.
.exports.*.types is for TypeScript type definitions. A single{' '}
.d.ts file can define all exported symbols in your package for both CJS and
ESM.
.exports.*.import is for ESM. This is the entrypoint for how a modern
runtime should import your package when running under CommonJS. It is a single ESM
compatible file.
.exports.*.require is for CJS. This is the entrypoint for how a modern
runtime should import your package when running under CommonJS. It is a single CJS
compatible file.
.exports.*.default is for when a runtime does not match any other
condition, and is a fallback. It's also within the spec to specify default{' '}
as the only entrypoint. I did not use default in my initial Tweet.
I made a few mistakes here. First of all, types are specific to ESM and CJS. This means
there should be twotypes fields. One for ESM, one for CJS. Even the
TypeScript documentation gets this wrong, and is something they're working on updating.
Solutions for this are also pretty wild. I've managed to get things working by simply
copying ./dist/index.d.ts to ./dist/index.d.cts after bundling,
and making the following changes to my package.json.
Note that we point to a .js file and not .mjs when targeting ESM. This is because our
package.json has type set to module. This tells our runtime that
all files are assumed to be ESM unless they have a .cjs extension. There's no
such thing as an ESM package, only ESM files. Using "type": "module", is just
a way to tell the runtime to interpret existing files as ESM.
What gives?
I'm still figuring this all out, and I'm not an expert. I'm just trying to share what I
have learned so far. If you have any corrections or suggestions, please let me know!
Clearly, this is messy. It's messy because we're trying to support a lot of different
runtimes, and we're trying to support them all at once. We're trying to support ESM, CJS,
legacy bundlers, modern bundlers, and TypeScript. We're trying to support all of these
runtimes at once, and finally, we're trying to support them all at once in a single{' '}
package.json file. Few other languages suffer from this level of complexity
and fragmentation.
Let's break down the mess and why all these things are the way they are. Starting off with{' '}
exports.
exports is the modern way to define what your package exports. We have
already established that it is a map of entrypoints to files. Let's step through what
happens when a runtime/consumer (we'll use the word consumer, because TypeScript - which
is not a runtime - is also reading our code in this case) wants to import our package.
Consumer encounters an import statement
{stripIndent`
import {something} from 'my-package';
`}
Consumer resolve the source code for my-package. In Node.js this is done
by looking for the folder name in node_modules, and then finding the{' '}
package.json. In any case, this is up to the consumer to implement
Consumer finds package.json file in the source code folder, and begins to
read the exports field
It steps through each field (in order, despite it being an object) and checks if the
condition the consumer is looking for exists in the exports field.
If the condition is met, the consumer will use the file specified in the{' '}
exports field as the entrypoint for the package. If the condition is not
met, it will continue to the next field. If no condition is met, a consumer will
usually exit/throw an error.
An example of a condition being met could be Node.js looking for an ESM file. In this
case, it would look for the import condition first, before trying to fall
back to default if it exists.
>
);
}
}
================================================
FILE: src/blog/2025/ambient-declarations/ambient-declarations.tsx
================================================
import {stripIndent} from 'common-tags';
import {Note} from '../../../components/note';
import {Highlighter, Shell} from '../../../components/syntax-highligher';
import {Post} from '../../Post';
export class AmbientDeclarations extends Post {
public name = 'Ambient Declarations';
public slug = 'ambient-declarations';
public date = new Date('9 May 2025');
public hidden = false;
public keywords = ['Ambient Modules', 'TypeScript', 'Module Resolution'];
public excerpt = 'Explaining ambient declarations with @types/bun as an example';
public render() {
return (
<>
Ambient Declarations
I recently landed a pull request (
#18024) in{' '}
Bun that reorganized and rewrote significant portions of
Bun's TypeScript definitions. Working on this PR made me realize how little documentation
there is on ambient declarations, so I wanted to write about it.
What are ambient declarations?
I'll start by answering this question with a couple questions...
1. How does TypeScript know the types of my node_modules, which are mostly
all .js files?
2. How does TypeScript know the types of APIs that exist in my runtime?
The short answer: It can't!
The short but slightly longer answer is that it CAN with some extra files - ambient
declarations! These are files that exist somewhere in your project (usually in{' '}
node_modules) that contain type information and tell TypeScript what{' '}
things exist at runtime. They use the file extension .d.ts, with the
`.d` denoting "declaration".
By things I mean anything you import and use. That could be functions, classes,
variables, modules themselves, APIs from your runtime, etc.
They're called "ambient" declarations because in the TypeScript universe ambient simply
means "without implementation"
If you've ever imported a package and magically got autocomplete and type checking, you've
benefited from ambient declarations.
A simple ambient declaration file could look like this:
{stripIndent`
/**
* Performs addition using AI and LLMs
*
* @param a - The first number
* @param b - The second number
*
* @returns The sum of a and b (probably)
*/
export declare function add(a: number, b: number): Promise;
`}
If you can already read TypeScript this ambient declaration will be very easy to
understand. You can clearly see a JSDoc comment, the types of the arguments, the return
type, an export keyword, etc. It almost looks like real TypeScript, except the really
important part to note here is the keyword declare is used. This keyword
tells TypeScript to not expect any runtime code to exist here, it's purely a type
declaration only.
It's completely legitimate and legal to use the declare keyword inside of
regular .ts files. There are many use cases for this, a common one being declaring types
of globals.
How Does TypeScript Find Types?
Module resolution is an incredibly complex topic, but it boils down to TypeScript looking
for relevant types in a few places.
Bundled types: Some packages include their own .d.ts files.
DefinitelyTyped: If not, TypeScript looks in @types/ packages in{' '}
node_modules.
Your own project: You can add .d.ts files anywhere in your project
to describe types for JS code, global variables, or even new modules.
Source: If the module resolution algorithm resolves to an actual TypeScript file,
then the types can be read from the original source code anyway. Some packages on NPM
also publish their TypeScript source and allow modern tooling to consume it directly.{' '}
Ambient declarations are NOT used in either of these scenarios
.
Ambient vs. Regular Declarations
Regular declarations are for code you write and control.
{stripIndent`
export function add(a: number, b: number): number {
return a + b;
}`}
Ambient declarations are for code that exists elsewhere.
{stripIndent`
export declare function add(a: number, b: number): number;
`}
Module vs. Script Declarations
Module declarations: Any .d.ts file with a top-level{' '}
import or export. Types are added to the module.
Script (global) declarations: No top-level import/export. Types are added to the
global scope.
File Type
Example Syntax
Scope
Module
export declare function foo(): void;
Module only (must be imported)
Script (global)
declare function setTimeout(...): number; declare function foo(): void;
Global (available everywhere)
Rule of thumb: An ambient declaration file is global unless it has a top-level
import/export.
Script files can pollute the global namespace and can very easily{' '}
clash with other declarations.
Prefer the module pattern unless you really need to patch globalThis.
Why does this distinction exist? TypeScript is old in JavaScript's history - it predates
the modern module system (ESM) and needed to support the "everything is global" style of
early JS. That's why it still supports both module and script (global) declaration files.
How does TypeScript treat these differently?
Module: Everything you declare is private to that module and must be explicitly
imported by the consumer - just like regular TypeScript/ESM code.
Script (global): Everything is injected directly into the global scope of every
file in your program. This is how the DOM lib ships types like window,{' '}
document, and functions like setTimeout.
When would you use each?
Module: For packages, libraries, and almost all modern code.
Script: For patching browser globals, legacy code, or when you really need to add
something to the global scope.
You can still augment the global scope from inside a module-style declaration file by
using the global {'{ ... }'} escape hatch, but that should be reserved for
unavoidable edge-cases.
Declaring Global Types
Suppose you want to add a global variable that your runtime creates, or perhaps a library
you're using doesn't have types for:
{stripIndent`
declare function myAwesomeFunction(x: string): number;
`}
Because this declaration file is NOT a module, this will be accessible everywhere in your
program.
What if you wanted to add something to the window object? TypeScript declares
the window variable exists by assigning it to an interface called Window,
which is also declared globally. You can perform{' '}
Declaration Merging
{' '}
to extend that interface, and tell TypeScript about new properties that exist.
You can declare a module by its name. As long as the ambient declaration file gets
referenced or included in your build somehow, then TypeScript will make the module
available.
This syntax also allows for declaring modules with wildcard matching. We do this in{' '}
@types/bun, since Bun allows for importing .toml and{' '}
.html files.
Make sure your tsconfig.json includes the types folder (usually automatic).
Compiler contract
Since ambient modules don't contain runtime code, they should be treated like "promises"
or "contracts" that you are making with the compiler. They're like documentation that
TypeScript can understand. Just like documentation for humans, it can get out of sync with
the actual runtime code. A lot of the work I'm doing at Bun is ensuring our type
definitions are up to date with Bun's runtime APIs.
Conflicts
While doing research for the pull request mentioned at the beginning, I found a few cases
where the compiler was not able to resolve the types of some of Bun's APIs because we had
declared that certain symbols existed, where they might have already been declared by{' '}
lib.dom.d.ts
{' '}
(the builtin types that TypeScript provides by default) or things like{' '}
@types/node (the types for Node.js). .
Avoiding these conflicts is unfortunately not always possible. Bun implements a really
solid best-effort approach to this, but sometimes you just have to get creative. For
example, you might see code like this to "force" TypeScript to use one type over another:
{stripIndent`
declare var Worker: globalThis extends { onmessage: any; Worker: infer T }
? T
: never;
`}
Bun's types take this a step further by using a clever trick that lets us use the built-in
types if they exist, with a graceful fallback when they don't.
{stripIndent`
declare module "bun" {
namespace __internal {
// \`onabort\` is defined in lib.dom.d.ts, so we can check to see if lib dom is loaded by checking if \`onabort\` is defined
type LibDomIsLoaded = typeof globalThis extends { onabort: any } ? true : false;
/**
* Helper type for avoiding conflicts in types.
*
* Uses the lib.dom.d.ts definition if it exists, otherwise defines it locally.
*
* This is to avoid type conflicts between lib.dom.d.ts and \@types/bun.
*
* Unfortunately some symbols cannot be defined when both Bun types and lib.dom.d.ts types are loaded,
* and since we can't redeclare the symbol in a way that satisfies both, we need to fallback
* to the type that lib.dom.d.ts provides.
*/
type UseLibDomIfAvailable =
LibDomIsLoaded extends true
? typeof globalThis extends { [K in GlobalThisKeyName]: infer T } // if it is loaded, infer it from \`globalThis\` and use that value
? T
: Otherwise // Not defined in lib dom (or anywhere else), so no conflict. We can safely use our own definition
: Otherwise; // Lib dom not loaded anyway, so no conflict. We can safely use our own definition
}
}
`}
{stripIndent`
declare var Worker: Bun.__internal.UseLibDomIfAvailable<'Worker', {
new(filename: string, options?: Bun.WorkerOptions): Worker;
}>;
`}
This declares that the Worker runtime value exists, and will use the version
from TypeScript's builtin lib files if they're loaded already in the program, and if not
it will use the version passed as the second argument.
This trick means we can write types that can exist in many different environments without
worrying about impossible-to-fix conflicts breaking the build.
Declaring entire modules as global namespaces
In Bun, everything importable from the 'bun' module is also available on the
global namespace Bun
{stripIndent`
import { file } from 'bun';
await file('test.txt').text();
// Or, exactly the same thing:
await Bun.file('test.txt').text();
`}
In fact, you can do an equality to check to see that importing the module gives you the
same reference to the global namespace.
{stripIndent`
bun repl
Welcome to Bun v1.2.13
Type ".help" for more information.
> require("bun") === Bun
true
`}
Declaring this in TypeScript uses some strange syntax. You can{' '}
find the declaration here
, but it looks like this:
{stripIndent`
import * as BunModule from "bun";
declare global {
export import Bun = BunModule;
}
`}
Let's break it down
We have an import statement, so this file becomes a module.
We import everything from the bun module and alias to a namespace called{' '}
BunModule
We use the `declare global` block to escape back into global scope, and then use the
funky syntax export import to re-export the namespace to the global scope
This export import syntax a way of saying "re-export this namespace" - except
when declaring on the global scope (inside a declare global {'{ }'} block or
inside a script/global file) the export keyword kind of turns into a namespace declaration
for the global scope.
Here is the "rest" of @types/bun that piece this all together
{stripIndent`
declare module "bun" {
/**
* Creates a new BunFile instance
* @param path - The path to the file
* @returns A new BunFile instance
*/
function file(path: string): BunFile;
interface BunFile {
/* ... */
}
}
`}
{stripIndent`
// You "import" types by using triple-slash references,
// which tell TypeScript to add these declarations to the build.
///
///
`}
{stripIndent`
{
"name": "@types/bun",
"version": "1.2.13",
"types": "./index.d.ts",
// ...
}
`}
In previous versions of Bun's types, the Bun global was defined as a variable that
imported the bun module.
{stripIndent`
declare var Bun: typeof import("bun");
`}
But since this is a runtime value, we have lost all of the types that are exported from
the bun module. For example we can't use Bun.BunFile in our
code.
In the pull request mentioned at the beginning, I changed previous declaration to use the{' '}
export import syntax which fixed this issue. It means you can now use the Bun
namespace exactly like you'd expect the bun module to behave.
Ambient declaration gotchas
"Cannot find module" or "type not found" errors: Make sure your{' '}
.d.ts file is included in the project (check tsconfig.json's{' '}
include/exclude).
Conflicts: If two libraries declare the same global, you'll get errors. Prefer
module declarations, and avoid globals unless necessary.
);
}
================================================
FILE: src/components/stats.tsx
================================================
import {useMemo} from 'react';
import {useFirstEverLoad, useVisitCounts} from '../hooks/use-first-ever-load';
export function Stats() {
const [stats] = useFirstEverLoad();
const [visits] = useVisitCounts();
const firstEverLoadTime = useMemo(() => new Date(stats.time), [stats.time]);
return (
You first visited my website on {firstEverLoadTime.toLocaleDateString()} at{' '}
{firstEverLoadTime.toLocaleTimeString()} and on this first visit, you were on the{' '}
{stats.path} page. Since then, you have visited {visits - 1} more times.{' '}
{visits > 1 && 'Thanks for coming back!'}
);
}
================================================
FILE: src/components/syntax-highligher.tsx
================================================
import type {PropsWithChildren} from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import light from 'react-syntax-highlighter/dist/cjs/styles/hljs/lightfair';
import dark from 'react-syntax-highlighter/dist/cjs/styles/hljs/vs2015';
import clsx from 'clsx';
import {TbBrandCss3, TbBrandHtml5, TbBrandJavascript, TbBrandTypescript} from 'react-icons/tb';
const Pre = ({children}: PropsWithChildren) =>
This is a list of random experiments I've built on this website. There's not a lot here and
this is all quite old.
Morphing Shapes
Animating and shifting divs using just css transitions and JS to initiate them
Monzo Dashboard
Using the Monzo API to display personal account details. Unfortunately, the Monzo API
requires me to manually add users, so if you want access, contact me.
Rekordbox History Parser
Rekordbox exports history in a format not so useful for copy pasting. This is a tiny
tool to fix that
);
}
================================================
FILE: src/pages/index.tsx
================================================
import {get} from '@prequist/lanyard';
import {motion} from 'framer-motion';
import type {GetStaticProps} from 'next';
import Link from 'next/link';
import {CiTwitter} from 'react-icons/ci';
import {SiBun, SiClaude, SiGithub, SiSpotify} from 'react-icons/si';
import {useLanyardWS, type Types} from 'use-lanyard';
import album from '../../public/album.png';
import type {Post} from '../blog/Post';
import {posts} from '../blog/posts';
import {BlogPostList} from '../components/blog-post-list';
import {message, MessageGroup} from '../components/message';
import {useShouldDoInitialPageAnimations} from '../hooks/use-did-initial-page-animations';
import {env} from '../server/env';
import {backupDiscordId, discordId} from '../utils/constants';
export interface Props {
lanyard: Types.Presence;
backupLanyard: Types.Presence;
location: string;
recentBlogPosts: Post.TinyJSON[];
}
export const getStaticProps: GetStaticProps = async () => {
const lanyard = await get(discordId);
const backupLanyard = await get(backupDiscordId);
const location = lanyard.kv.location ?? env.DEFAULT_LOCATION;
const recentBlogPosts = [...posts]
.filter(post => !post.hidden)
// .sort((a, b) => dayjs(b.date).unix() - dayjs(a.date).unix())
.slice(0, 3)
.map(post => post.toTinyJSON());
return {
revalidate: 10,
props: {
location,
lanyard,
backupLanyard,
recentBlogPosts,
},
};
};
export default function Home(props: Props) {
const lanyard = useLanyardWS(discordId, {
initialData: props.lanyard,
});
const backupLanyard = useLanyardWS(backupDiscordId, {
initialData: props.backupLanyard,
});
const spotify = lanyard.spotify ?? backupLanyard?.spotify ?? null;
const shouldAnimate = useShouldDoInitialPageAnimations();
return (
I'm Alistair. I work at{' '}
Anthropic
{' '}
on {' '}
Bun
{' '}
and {' '}
Claude Code
. I'm interested in things like language specifications and type systems. I've
been called a TypeScript wizard at least a few times. It's nice to meet you.
),
},
]}
/>
{spotify && (
I listen to a lot of music, and{' '}
right now I'm listening to this
song on Spotify:
),
},
{
key: 'the-current-song',
content: (
{spotify.song}
{spotify.artist && (
{spotify.artist.split('; ').join(', ')}
)}
),
},
]}
/>
)}
{/*
// In the rare case I'm not listening to anything, you can usually find me out and
// about riding my{' '}
//
// Evolve skateboard
//
// ,{' '}
//
// DJing (on YouTube)
// {' '}
// or{' '}
//
// trying my hardest to figure out Ableton Live
//
//
// ),
// },
]}
/> */}
)]} />
),
},
{
key: 'location-caption',
content: (
I'm currently in{' '}
{lanyard.kv.location}
{' '}
📍
),
},
]}
/>
{/*
I have some fun experiments on this site, some are functional things I use, others
are just me messing around.{' '}
Click here to see them
.
),
},
]}
/> */}
);
}
================================================
FILE: src/pages/monzo/dashboard/index.tsx
================================================
import {MonzoAPI, type Id, type Models} from '@otters/monzo';
import {HTTPClientError} from 'alistair/http';
import {bwitch} from 'bwitch';
import type {GetServerSideProps, Redirect} from 'next';
import Link from 'next/link';
import {parseSessionJWT} from '../../../server/sessions';
type Props =
| {
success: true;
data: {
accounts: Array<
Models.Account & {
balance?: Models.Balance | null;
pots?: Models.Pot[] | null;
webhooks?: Array<{
id: Id<'webhook'>;
account_id: Models.Account['id'];
url: string;
}> | null;
}
>;
};
}
| {
success: false;
error: string;
body: unknown;
};
export default function MonzoDashboard(props: Props) {
if (!props.success) {
return (
{props.error}
{JSON.stringify(props.body, null, 4)}
You may need to explicitly enable permissions in the Monzo app, on your phone.
{bwitch(acct.type)
.case(
'uk_monzo_flex_backing_loan',
() =>
'This is a loan account used for flex transactions. It has no balance and will be considered closed once the debt is paid off.',
)
.or(() => 'This type of acccount has no balance')}