├── .eslintrc.json
├── .github
└── workflows
│ └── lint.yml
├── .gitignore
├── .prettierignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── SECURITY.md
├── bun.lock
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
├── album.png
├── alistair.jpeg
├── favicon.ico
├── grain.jpeg
└── videos
│ └── mochip-educake.mp4
├── src
├── blog
│ ├── 2022
│ │ ├── 01
│ │ │ ├── mochip
│ │ │ │ ├── email-from-colin.png
│ │ │ │ ├── gmeet.png
│ │ │ │ ├── goodbye-mochip.png
│ │ │ │ ├── hegarty-time-exploit-old.webp
│ │ │ │ ├── hegarty-time-exploit.jpeg
│ │ │ │ ├── landing.jpeg
│ │ │ │ └── mochip.tsx
│ │ │ ├── serverless-discord-oauth
│ │ │ │ ├── discord-oauth-dashboard.png
│ │ │ │ └── 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
├── fonts
│ ├── gambarino-regular.ttf
│ └── roobert-variable.woff2
├── globals.css
├── hooks
│ ├── layout.ts
│ ├── use-did-initial-page-animations.ts
│ ├── use-first-ever-load.ts
│ ├── use-isomorphic-value.ts
│ └── use-lerp-transform.ts
├── images
│ ├── banner.jpg
│ ├── matrix.gif
│ └── me.jpg
├── pages
│ ├── 404.tsx
│ ├── [slug].tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── _error.tsx
│ ├── api
│ │ ├── contact.ts
│ │ ├── map.ts
│ │ ├── oauth.ts
│ │ ├── oauth
│ │ │ └── [platform]
│ │ │ │ ├── callback.ts
│ │ │ │ └── redirect.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
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"],
3 | "rules": {
4 | "react/no-unescaped-entities": "off",
5 | "@next/next/no-img-element": "off",
6 | "@typescript-eslint/no-namespace": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: 'lint'
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 | name: 'Lint'
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Begin CI...
16 | uses: actions/checkout@v2
17 |
18 | - uses: oven-sh/setup-bun@v1
19 | with:
20 | bun-version: latest
21 |
22 | - name: Install dependencies
23 | run: bun install
24 | env:
25 | CI: true
26 |
27 | - name: Lint
28 | run: bun lint
29 | env:
30 | CI: true
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.pnp
3 | .pnp.js
4 | /build
5 | .DS_Store
6 | .env
7 | .env.local
8 | .env.development.local
9 | .env.test.local
10 | .env.production.local
11 | *.log
12 | .vercel
13 | .idea
14 | .eslintcache
15 | .next
16 | out
17 |
18 | # Yarn
19 | .yarn/*
20 | !.yarn/releases
21 | !.yarn/plugins
22 | !.yarn/sdks
23 | !.yarn/versions
24 |
25 | # bun
26 | *.bun
27 |
28 | # Cloud9 IDE files
29 | .c9
30 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | dist
3 | build
4 | out
5 | node_modules
6 | .yarn
7 | .git
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 🦄 alii/website
2 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | Don't use this and expect it to be totally secure, but on that note, it's just a React app.
4 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { config as dotenv } from 'dotenv';
2 |
3 | // @ts-check
4 |
5 | /** @type {import("next").NextConfig} */
6 | const config = {
7 | env: dotenv().parsed,
8 |
9 | images: {
10 | remotePatterns: [
11 | {
12 | protocol: 'https',
13 | hostname: 'i.scdn.co',
14 | pathname: '/image/*',
15 | },
16 |
17 | {
18 | protocol: 'https',
19 | hostname: 'snapshot.apple-mapkit.com',
20 | pathname: '/api/v1/snapshot',
21 | },
22 | ],
23 | },
24 |
25 | async redirects() {
26 | return [
27 | {
28 | source: '/outro',
29 | destination: 'https://www.youtube.com/watch?v=HeF11Av9WuU',
30 | permanent: true,
31 | },
32 | {
33 | source: '/desu',
34 | destination: 'https://www.youtube.com/watch?v=HotGxCSas6A',
35 | permanent: true,
36 | },
37 | {
38 | source: '/10',
39 | destination: 'https://youtu.be/G5HcvgepK-I',
40 | permanent: true,
41 | },
42 | {
43 | source: '/lulzsec',
44 | destination: 'https://www.youtube.com/watch?v=DurOYPdXyF4',
45 | permanent: true,
46 | },
47 | {
48 | source: '/wheels',
49 | destination: 'https://www.youtube.com/watch?v=9xRFN2i1cwQ',
50 | permanent: true,
51 | },
52 | {
53 | source: '/letterone',
54 | destination: 'https://hyperfollow.com/alistair6/letter100-3',
55 | permanent: true,
56 | },
57 | {
58 | source: '/live-25-01-2024',
59 | destination: 'https://www.youtube.com/watch?v=OvTy9xYH7LA',
60 | permanent: true,
61 | },
62 | {
63 | source: '/live-02-02-2024',
64 | destination: 'https://youtube.com/watch?v=zEoTeUEElZc',
65 | permanent: true,
66 | },
67 | {
68 | source: '/live-04-07-2024',
69 | destination: 'https://youtube.com/watch?v=-XsKN44b7ho',
70 | permanent: true,
71 | },
72 | ];
73 | },
74 | };
75 |
76 | export default config;
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "version": "1.0.0",
4 | "repository": "git@github.com:alii/website.git",
5 | "author": "Alistair Smith ",
6 | "license": "Apache-2.0",
7 | "private": true,
8 | "packageManager": "bun@1.0.0",
9 | "scripts": {
10 | "dev": "next dev",
11 | "build": "next build",
12 | "start": "next start",
13 | "lint": "next lint"
14 | },
15 | "devDependencies": {
16 | "@tailwindcss/forms": "^0.5.10",
17 | "@tailwindcss/postcss": "^4.1.7",
18 | "@types/common-tags": "^1.8.4",
19 | "@types/cookie": "^1.0.0",
20 | "@types/jsonwebtoken": "^9.0.9",
21 | "@types/jwa": "^2.0.3",
22 | "@types/react": "^19.1.5",
23 | "@types/react-dom": "^19.1.5",
24 | "@types/react-syntax-highlighter": "^15.5.13",
25 | "@types/uuid": "^10.0.0",
26 | "discord-api-types": "^0.38.8",
27 | "eslint": "9.27.0",
28 | "eslint-config-next": "15.1.8",
29 | "postcss": "^8.5.3",
30 | "prettier": "^3.5.3",
31 | "prettier-plugin-tailwindcss": "^0.6.11",
32 | "sharp": "^0.34.2",
33 | "tailwindcss": "^4.1.7",
34 | "typescript": "^5.8.3"
35 | },
36 | "dependencies": {
37 | "@altano/satori-fit-text": "^1.0.2",
38 | "@c-side/next": "^1.0.0",
39 | "@marsidev/react-turnstile": "^1.1.0",
40 | "@next/third-parties": "^15.1.8",
41 | "@otters/monzo": "^2.1.2",
42 | "@prequist/lanyard": "^1.1.0",
43 | "@tailwindcss/typography": "^0.5.16",
44 | "@vercel/og": "^0.6.8",
45 | "alistair": "^1.14.4",
46 | "axios": "^1.9.0",
47 | "bwitch": "^0.3.0",
48 | "clsx": "^2.1.1",
49 | "common-tags": "^1.8.2",
50 | "cookie": "^1.0.2",
51 | "dayjs": "^1.11.13",
52 | "dotenv": "^16.5.0",
53 | "envsafe": "^2.0.3",
54 | "framer-motion": "^12.12.2",
55 | "jsonwebtoken": "^9.0.2",
56 | "jwa": "^2.0.1",
57 | "next": "^15.1.8",
58 | "nextkit": "^3.4.3",
59 | "react": "^19.1.0",
60 | "react-dom": "^19.1.0",
61 | "react-hot-toast": "^2.5.2",
62 | "react-icons": "^5.5.0",
63 | "react-syntax-highlighter": "^15.6.1",
64 | "satori": "^0.13.1",
65 | "use-lanyard": "^1.7.0",
66 | "uuid": "^11.1.0",
67 | "zod": "^3.25.28"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | const alistair = require('alistair/prettier');
2 |
3 | module.exports = {
4 | ...alistair,
5 | plugins: ['prettier-plugin-tailwindcss'],
6 | };
7 |
--------------------------------------------------------------------------------
/public/album.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/public/album.png
--------------------------------------------------------------------------------
/public/alistair.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/public/alistair.jpeg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/public/favicon.ico
--------------------------------------------------------------------------------
/public/grain.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/public/grain.jpeg
--------------------------------------------------------------------------------
/public/videos/mochip-educake.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/public/videos/mochip-educake.mp4
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/email-from-colin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/email-from-colin.png
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/gmeet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/gmeet.png
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/goodbye-mochip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/goodbye-mochip.png
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/hegarty-time-exploit-old.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/hegarty-time-exploit-old.webp
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/hegarty-time-exploit.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/hegarty-time-exploit.jpeg
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/landing.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/mochip/landing.jpeg
--------------------------------------------------------------------------------
/src/blog/2022/01/mochip/mochip.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import {Highlighter} from '../../../../components/syntax-highligher';
3 | import {Post} from '../../../Post';
4 | import emailFromColin from './email-from-colin.png';
5 | import gmeet from './gmeet.png';
6 | import goodbyeMochip from './goodbye-mochip.png';
7 | import hegartyTimeExploit from './hegarty-time-exploit.jpeg';
8 | import mochipLanding from './landing.jpeg';
9 |
10 | export class Mochip extends Post {
11 | public name = 'Avoiding homework with code (and getting caught)';
12 | public slug = 'mochip';
13 | public date = new Date('6 Jan 2022');
14 | public excerpt = 'The eventful tale of me getting fed up with my homework';
15 | public hidden = false;
16 | public keywords = [
17 | 'school',
18 | 'homework',
19 | 'clout',
20 | 'hegarty maths',
21 | 'educake',
22 | 'homework hack',
23 | 'maths homework',
24 | 'programming',
25 | ];
26 |
27 | public render() {
28 | return (
29 | <>
30 |
Avoiding homework with code (and getting caught)
31 |
32 |
33 | Back in 2020, my school used a few online learning platforms that allowed
34 | professors/teachers to assign homework to students. I, as a lazy developer, wanted to
35 | spend more time playing games and writing code, especially when everyone was spending
36 | their time at home because of lockdown. I started writing this post in January of 2022,
37 | but I put off publicizing it for a while. It has been long enough since this all happened,
38 | so please sit back and enjoy.
39 |
40 |
41 |
The back story
42 |
43 | Let's set the scene. 2018, my school introduces a new online homework platform for
44 | students. It's called HegartyMaths and it does a lot. It's fairly simple, teachers
45 | choose a topic to set for us as homework, with that we get a 10-15 minute
46 | tutorial/informational video on the subject (of which we have to write down notes whilst
47 | watching) and a shortish quiz to complete after finishing the video. It's a lot of work,
48 | especially the quiz, and in the worst cases can take up to an hour to complete one topic
49 | (bad).
50 |
51 |
52 | Mostly, software engineers are rather lazy individuals. We tell metal how to do stuff for
53 | us. Homework then, naturally, is an arduous task for a developer who is still at school.
54 | So, still 2018, a close friend of mine by the name of{' '}
55 |
56 | Scott Hiett
57 | {' '}
58 | and I decided to do something about the Hegarty situation. We started to reverse engineer
59 | the frontend app and eventually came up with a Tampermonkey userscript that would glitch
60 | the embedded YouTube player to say that we'd watched the video at least 1x. Crucially, our
61 | teachers could see how many times we'd watched the video, so being able to skip up to 20
62 | minutes of homework time was especially useful – and it was a lot of fun to build too.
63 |
64 |
65 | So we flexed it on our Snapchat stories and had our school friends message us to use it
66 | blah blah. We eventually figured out that we could also set it to be watched over 9999x
67 | times; every time we did that our accounts were reset by the Hegarty team.
68 |
69 |
The first email
70 |
71 | After this, we got in contact with our Math teacher in November of 2018 and got her to
72 | send an email to HegartyMaths informing them of our petty exploit and they got back to us
73 | very quickly.{' '}
74 |
75 | I don't have the original email anymore but I distinctly remember it saying something
76 | along the lines of "Stop trying to hack our platform and get back to doing your
77 | homework."
78 | {' '}
79 | Edit: While writing this, I was able to uncover the deleted email from a photo we had
80 | taken of it in 2020. See below{' '}
81 | (certain details redacted for obvious reasons):
82 |
83 |
84 |
85 | This response excited us a bit, as they were now aware of us messing around with the site
86 | and they had no intention of fixing the minor vuln we had anyway, so we kept using it. We
87 | had tried to build a script to answer the questions for us, but it was too much work at
88 | the time (complex data structures, weird API responses, etc etc).
89 |
90 |
Educake
91 |
92 | For a while, students had access to another platform called Educake. Similar to
93 | HegartyMaths but targeting Biology, Chemistry and Physics. There was no video to watch at
94 | the beginning. We'd used it for a few years, in fact since I joined the school, but I'd
95 | never thought about reversing until all of this began.
96 |
97 |
98 | One common factor between Hegarty and Educake is that they immediately give you the
99 | correct answer if you got a question wrong. We took advantage of this and wrote a small
100 | node/mongo app & tampermonkey script to detect when a user was on a quiz page, answer
101 | every question with a random number, and then store the correct answer in mongodb. I don't
102 | have the original source but the TamperMonkey script was probably something like
103 | the following:
104 |
128 | As you can see, it was quite literally a loop through every question, saving the correct
129 | answer as we got it and moving on. Eventually I added a few more features to fetch from
130 | the database if we already had the right answer (meaning we don't answer{' '}
131 | Math.random every time) and also I added in support for multiple choice (so
132 | that we actually pick one of the possible answers rather than making it up – however I was
133 | surprised that the Educake backend would allow an answer that wasn't even in the possible
134 | choices).
135 |
136 |
137 |
138 | Now working on the project solo, I decided it would be time to build a nice UI for it all
139 | and bundle it all into a simple Tampermonkey script for both flexing rights on Snapchat
140 | (people constantly begging me to be able to use it was certainly ego fuel I hadn't
141 | experienced before) and also for myself to get out of homework I didn't want to do.
142 |
143 |
144 | The end result? A ~200 line codebase that scooped up all questions and answers on the site
145 | that could repeatedly get 100% on every single assignment and a 15mb mongo database.
146 |
147 |
148 |
149 | Below is a small video of what it all looked like. It also demonstrates a feature I added
150 | allowing for a "target percentage" — meaning users could get something other than 100% to
151 | look like more real/human score. Video was recorded on my Snapchat in November 2019.
152 |
153 |
154 |
155 |
156 |
Hegarty 2
157 |
158 | The success of this script, along with pressure from my peers, led me to gain a lot of
159 | motivation to start working on reversing Hegarty again. I reached out to an internet
160 | friend who, for the sake of his privacy, will be named "Jake." He also used HegartyMaths
161 | at his school and was in the same boat as me trying to avoid doing our homework. Together,
162 | we managed to figure out how to answer many varying types of questions, including multiple
163 | choice and ordered answers, resulting in a huge amount of data stored. We had sacrificial
164 | user accounts and managed to answer 60,000 questions in a couple minutes, rocketing our
165 | way to the top of the HegartyMaths global leaderboard.{' '}
166 |
167 | Would like to give a special shoutout to Boon for lending us his login and letting us
168 | decimate his statistics.
169 |
170 |
171 |
172 | Together, Jake and I scraped the entirety of Hegarty's database and now had a JSON file
173 | that could be argued to be worth as much as Hegarty the company itself due to the entire
174 | product quite literally being the database we had copied.
175 |
176 |
177 | With this file, I wanted to take it a step further and allow my friends and other people
178 | to make good use of it without directly giving out the database (irresponsible)... And
179 | here Mochip was coined.
180 |
181 |
Mochip
182 |
183 | So, where does Mochip tie in to this? Mochip was a Chrome extension, a collection of both
184 | our scraped Hegarty and scraped Educake databases sat behind a TypeScript API and a small
185 | React app. Hosted on Heroku free tier and MongoDB Atlas free tier, users could log in,
186 | enter a question (from either site) and get back a list of answers Mochip has for that
187 | question. Here's what the landing page looked like:
188 |
189 |
190 |
191 | In the screenshot we can see a few stats on the right like total estimated time saved and
192 | how long you've had your account for. We gamified it a little just to keep people engaged
193 |
194 |
195 | Our chrome extension was made for Educake as they disabled copying question text with the
196 | clipboard. We re-enabled that just by clicking a button that was injected into the UI. The
197 | chrome extension is no longer on the chrome web store, but we've found that mirrors still
198 | have listings that we can't get taken down:{' '}
199 |
200 | extpose.com/ext/195388
201 |
202 |
203 |
204 | Our userbase grew so big that we ended up with a Discord server and even our own listing
205 | on Urban dictionary — I'm yet to find out who made it!{' '}
206 |
211 | urbandictionary.com/define.php?term=mochip
212 |
213 |
214 |
215 | Eventually we "rebranded" as I wanted to disassociate my name from the project.
216 | Unfortunately I do not have any screenshots from this era to show. I made an alt discord
217 | account and a few announcements saying we'd "passed on ownership" however this ineveitably
218 | only lasted for a couple weeks before we were rumbled.
219 |
220 |
Crashing down
221 |
222 | All good things must come to and end, and Mochip's did after Scott posted about Mochip on
223 | his reddit account. Like any good CEO, Colin searches his company every now and then on
224 | Google to see what people are saying or doing and unfortunately came across our reddit
225 | post. He signed up (although under a different email) and tested out the app and was
226 | shocked to see it working. Shortly after this I received an email from Colin directly. See
227 | below
228 |
229 |
230 |
231 | I was upset but also a little content — it was sort of validation that I'd successfully
232 | made it and that catching the attention of Colin himself was sort of a good thing. We
233 | quickly scheduled a Google Meet, also inviting Scott, and I had one of the most memorable
234 | conversations of my life. I am extremely grateful for the advice Colin gave us in the
235 | call.
236 |
237 |
238 |
239 | I'd like to give a special thank you to the legendary Colin Hegarty for his kindness and
240 | consideration when reaching out to me. Things could have gone a lot worse for me had this
241 | not been the case. HegartyMaths is a brilliant learning resource and at the end of the
242 | day, it's there to help students learn rather than be an inconvenience.
243 |
244 |
245 | Shortly after, Colin reached out to the Educake team, who we also scheduled a call with.
246 | We explained our complete methodology and suggested ways to prevent this in the future.
247 | The easiest fix from our point of view would be to implement an easy rate limit with Redis
248 | that would make it wildly infeasible to automate a test. The other thing we suggested was
249 | to scramble IDs in the database to invalidate our cloned database as much as
250 | possible (e.g. we only had the Hegarty IDs, so we could no longer reverse lookup a
251 | question).
252 |
253 |
254 |
255 |
256 |
257 | Thank you for reading, truly. Mochip was a real passion project and I had a wild time
258 | building it. ⭐
259 |
260 |
261 |
262 |
263 |
264 | Edit 23 Sept, 2022: After making this post public, I posted this on HackerNews and
265 | amazingly sat in the #1 spot overnight. This site consequently received a lot of traffic,
266 | and I served almost 1.5TB in just shy of 6 hours. Some of the employees at Sparx (the
267 | parent company of HegartyMaths) ended up seeing this and forwarded it to Colin. A few
268 | minutes ago I just received a really lovely email from Mr Hegarty himself with the subject
269 | "Congrats to you!" I am so grateful for the kindness and consideration Colin has shown
270 | Scott and me, so if you are a teacher reading this, then please consider using
271 | HegartyMaths at your school! This was the happy ending!
272 |
273 | >
274 | );
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/src/blog/2022/01/serverless-discord-oauth/discord-oauth-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alii/website/9e7e8f5aabf66a628dc36a7f083a1433ac764735/src/blog/2022/01/serverless-discord-oauth/discord-oauth-dashboard.png
--------------------------------------------------------------------------------
/src/blog/2022/01/serverless-discord-oauth/serverless-discord-oauth.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import Link from 'next/link';
3 | import {Highlighter, Shell} from '../../../../components/syntax-highligher';
4 | import {Post} from '../../../Post';
5 | import discordOAuthDashboardImage from './discord-oauth-dashboard.png';
6 |
7 | export class ServerlessDiscordOAuth extends Post {
8 | public name = 'Serverless Discord OAuth with Next.js';
9 | public slug = 'serverless-discord-oauth';
10 | public date = new Date('2 January 2022');
11 | public excerpt = "Implementing basic Discord OAuth on Vercel's serverless platform";
12 | public hidden = false;
13 | public keywords = ['serverless', 'vercel', 'discord', 'oauth', 'node'];
14 |
15 | public render() {
16 | return (
17 | <>
18 |
Serverless Discord OAuth with Next.js
19 |
20 |
21 | OAuth is a brilliant solution to a difficult problem, but it can be hard to implement,
22 | especially in a serverless environment. Hopefully, this post will help you get started.
23 |
24 |
25 |
26 | Live demo:{' '}
27 | /demos/serverless-discord-oauth
28 |
29 |
30 |
The setup
31 |
32 | Firstly, we're going to need to create a Next.js with TypeScript app. Feel free to skip
33 | this if you "have one that you made earlier."
34 |
35 | bun create next-app my-app --typescript
36 |
Dependencies
37 |
38 | We will be relying on a few dependencies, the first is discord-api-types{' '}
39 | which provides up-to-date type definitions for Discord's API (who could've guessed). We'll
40 | also need axios (or whatever your favourite http lib is) to make requests to
41 | Discord. Additionally, we'll be encoding our user info into a JWT token & using the cookie
42 | package to serialize and send cookies down to the client. Finally, we'll use{' '}
43 | dayjs for basic date manipulation and pathcat to easily build
44 | urls with query params.
45 |
Dope, you've made it this far already! Let's get some code written
54 |
55 | Firstly, you're going to want to open up the folder pages/api and create a
56 | new file. We can call it oauth.ts. The api folder is where Next.js will
57 | locate our serverless functions. Handily, I've written a library called{' '}
58 | nextkit that can assist us with this process but for the time being it's out
59 | of scope for this post – I'll eventually write a small migration guide.
60 |
61 |
62 | {stripIndent`
63 | import type {NextApiHandler} from 'next';
64 | import type {RESTGetAPIUserResult} from 'discord-api-types/v8';
65 | import {serialize} from 'cookie';
66 | import {sign} from 'jsonwebtoken';
67 | import dayjs from 'dayjs';
68 | import {pathcat} from 'pathcat';
69 | import axios from 'axios';
70 |
71 | // Configuration constants
72 | // TODO: Add these to environment variables
73 | const CLIENT_ID = 'CLIENT_ID';
74 | const CLIENT_SECRET = 'CLIENT_SECRET';
75 | const JWT_SECRET = 'CHANGE ME!!!';
76 |
77 | // The URL that we will redirect to
78 | // note: this should be an environment variable
79 | // but I'll cover that in part 2 since
80 | // it will work fine locally for the time being
81 | const REDIRECT_URI = 'http://localhost:3000/api/oauth';
82 |
83 | // Scopes we want to be able to access as a user
84 | const scope = ['identify'].join(' ');
85 |
86 | // URL to redirect to outbound (to request authorization)
87 | const OAUTH_URL = pathcat('https://discord.com/api/oauth2/authorize', {
88 | client_id: CLIENT_ID,
89 | redirect_uri: REDIRECT_URI,
90 | response_type: 'code',
91 | scope,
92 | });
93 |
94 | /**
95 | * Exchanges an OAuth code for a full user object
96 | * @param code The code from the callback querystring
97 | */
98 | async function exchangeCode(code: string) {
99 | const body = new URLSearchParams({
100 | client_id: CLIENT_ID,
101 | client_secret: CLIENT_SECRET,
102 | redirect_uri: REDIRECT_URI,
103 | grant_type: 'authorization_code',
104 | code,
105 | scope,
106 | }).toString();
107 |
108 | const {data: auth} = await axios.post<{access_token: string; token_type: string}>(
109 | 'https://discord.com/api/oauth2/token',
110 | body,
111 | {headers: {'Content-Type': 'application/x-www-form-urlencoded'}},
112 | );
113 |
114 | const {data: user} = await axios.get(
115 | 'https://discord.com/api/users/@me',
116 | {headers: {Authorization: \`Bearer \${auth.access_token}\`}},
117 | );
118 |
119 | return {user, auth};
120 | }
121 |
122 | /**
123 | * Generates the set-cookie header value from a given JWT token
124 | */
125 | function getCookieHeader(token: string) {
126 | return serialize('token', token, {
127 | httpOnly: true,
128 | path: '/',
129 | secure: process.env.NODE_ENV !== 'development',
130 | expires: dayjs().add(1, 'day').toDate(),
131 | sameSite: 'lax',
132 | });
133 | }
134 |
135 | const handler: NextApiHandler = async (req, res) => {
136 | // Find our callback code from req.query
137 | const {code = null} = req.query as {code?: string};
138 |
139 | // If it doesn't exist, we need to redirect the user
140 | // so that we can get the code
141 | if (typeof code !== 'string') {
142 | res.redirect(OAUTH_URL);
143 | return;
144 | }
145 |
146 | // Exchange the code for a valid user object
147 | const {user} = await exchangeCode(code);
148 |
149 | // Sign a JWT token with the user's details
150 | // encoded into it
151 | const token = sign(user, JWT_SECRET, {expiresIn: '24h'});
152 |
153 | // Serialize a cookie and set it
154 | const cookie = getCookieHeader(token);
155 | res.setHeader('Set-Cookie', cookie);
156 |
157 | // Redirect the user to wherever we want
158 | // in our application
159 | res.redirect('/');
160 | };
161 |
162 | export default handler;
163 | `}
164 |
165 |
166 | Cool! This is the bare bones that we will need to start writing our OAuth. It's quite a
167 | lot to bite, but if you break it down line by line and read the comments, it should be
168 | fairly self-explanatory. We're still missing a few prerequisites to tell Discord who we
169 | are: the client id and secret.
170 |
186 | Copy and paste your client ID into your oauth.ts file
187 |
188 |
189 | Copy and paste your client secret into your oauth.ts file
190 |
191 |
192 | Add your redirect URI (http://localhost:3000/api/oauth) on the dashboard
193 |
194 |
195 | Make sure all your changes are saved and then we are ready to test it out for the first
196 | time!
197 |
198 |
199 |
200 |
Testing it
201 |
202 | Awesome, we've got everything setup correctly. Now we can give it a quick spin. You can
203 | start your Next.js development server if you haven't already by running{' '}
204 | bun dev in your terminal, you should be able to navigate to{' '}
205 |
206 | localhost:3000/api/oauth
207 | {' '}
208 | and successfully authenticate.
209 |
210 |
211 |
212 | Afterwards, if you open up your browser's devtools and check for the cookie section, you
213 | should see a cookie by the name of token – this is ours! Copy the value and
214 | paste it into{' '}
215 |
216 | jwt.io
217 | {' '}
218 | to decode it and see your details encoded inside it!
219 |
220 |
221 |
Why JWT?
222 |
223 | We've picked JWT because it lets us store information on the client side where only the
224 | server can mutate and verify that the server created it. This means users can't modify the
225 | data inside a JWT token, allowing the server to make guarantees about the data encoded.
226 |
227 |
228 |
Environment variables
229 |
Okay, we're almost there. Final stretch
230 |
231 | Right now, we have our constants defined in this file which is fine for prototyping but it
232 | now means that if you want to push your code to github, for example, your client secret
233 | and perhaps other private information will be publicly available on your project's
234 | repository! The solution? Environment varibles.
235 |
236 |
237 | Environment variables are bits of information that are provided to a process at runtime.
238 | It means we don't have to store secrets inside our source code.
239 |
240 |
241 | Thankfully, Next.js makes it super easy for us to use environment variables with something
242 | called an env file.
243 |
244 |
245 |
Creating our env file
246 |
247 | Firstly, make a new file in your project's file structure called .env and add
248 | the content below. The format for env files is KEY=value. You can use{' '}
249 | openssl rand -hex 64 to generate a JWT secret.
250 |
275 | And that should be all good! I'll be writing a part two and three later on that will cover
276 | accessing the JWT from the server and also deployment to vercel.
277 |
278 |
279 |
Thanks for reading!
280 | >
281 | );
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/src/blog/2022/01/zero-kb-blog/zero-kb-blog.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import {Note} from '../../../../components/note';
3 | import {Highlighter} from '../../../../components/syntax-highligher';
4 | import {Post} from '../../../Post';
5 |
6 | export class ZeroKbBlog extends Post {
7 | public name = 'The 0kb Next.js blog';
8 | public slug = 'zero-kb-nextjs-blog';
9 | public date = new Date('6 Jan 2022');
10 | public hidden = false;
11 | public excerpt = 'How I shipped a Next.js app with a 0kb bundle';
12 | public keywords = ['nextjs', 'zero', 'bundle', 'nextjs-zero-bundle', 'unstable_runtimeJS'];
13 |
14 | public render() {
15 | return (
16 | <>
17 |
The 0kb Next.js blog
18 |
19 |
20 | This only applies to apps using the pages directory of Next.js as App Dir
21 | (released in v13) does not support the settings used here. RSCs offer a similar idealogy
22 | of rendering components on the server only, while also allowing for client side JS.
23 |
24 |
25 |
26 | Ok so the title was a liiittle bit clickbaity, but it's not technically a lie. This
27 | entire website has zero JavaScript on every single page... a Next.js app with zero
28 | client side JS. How can this be possible?
29 |
30 |
31 |
Some context
32 |
33 |
34 | Next.js is a huge abstraction of react-dom/server and other helpful utilities
35 | for building server side rendered apps powered by React. It's really easy to get started
36 | with, and features things like file system routing, statically generated content and much
37 | much more. The most important thing to understand here is that there's a server...
38 |
39 |
40 |
41 | For those of you who are not familiar with React, it's a JavaScript framework for
42 | building user interfaces. It handles the view layer of an app and is used to render the
43 | UI. Next.js takes this a step further and allows you to write your own view layer to have
44 | the first render performed on a server, which allows for a lot of performance and UI
45 | optimizations because we can ship back a lot less to the client (foreshadowing).
46 |
47 |
48 |
Runtime JS
49 |
50 |
51 | With something called PageConfig, we can instruct Next.js to supply zero
52 | runtime JS to the client. It comes with a couple of trade-offs, but the general idea is
53 | that we perform our first render on the server and the resulting HTML and CSS is "frozen"
54 | and sent down to the client over the wire. Zero JavaScript runtime in the browser, no{' '}
55 | <script> tags in sight!
56 |
57 |
58 |
What's the catch?
59 |
60 |
61 | As the saying goes, there's no such thing as a free lunch. As with anything, doing this
62 | comes with some trade-offs. For example, Next has a lot of built-in React components that
63 | can speed up not only your app, but also development speed. Using the zero kb mode, we
64 | unfortunately cannot make full use of the next/link component; a component
65 | that prefetches pages so that clicks on links don't cause a full page reload.
66 | Additionally, we cannot use next/image as this also requires some minimal
67 | runtime JavaScript (a regular img works just fine). This also means zero
68 | state updates or literally any JavaScript that would've otherwise been bundled can run.
69 |
70 |
71 |
72 | So if you're fine with that, then you can go ahead and enable it. No more worrying about
73 | worrying about installing huge npm packages and watching your user count drop in realtime.
74 | See your gorgeous website in pure static HTML & CSS. Gone are the days of
75 | bundlephobia.com... I bet at this point you are itching to know how to enable it. Well,
76 | here's a quick example:
77 |
94 | And boom! just like that, 0kb bundle. If you are going to use this though, just bear in
95 | mind that as well as the trade offs mentioned above, you literally cannot use any
96 | JavaScript in the client anymore for this page. Zero. Nada. Null. Void. That means no
97 | state updates, no network requests, no useEffect, no event listeners, no timers. Nothing.
98 |
99 | >
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/blog/2022/03/open-source/open-source.tsx:
--------------------------------------------------------------------------------
1 | import {Post} from '../../../Post';
2 |
3 | export class OpenSource extends Post {
4 | public name = 'Open Source';
5 | public slug = 'open-source';
6 | public date = new Date('20 Mar 2022');
7 | public hidden = true;
8 | public excerpt = 'Thoughts & feelings on Open Source';
9 | public keywords = ['Developer', 'Open Source', 'GitHub', 'sponsorships'];
10 |
11 | public render() {
12 | return (
13 | <>
14 |
23 | I really do love open source. I love being able to build software that I know people will
24 | be able to make great use of. I love that we can extend already existing open source
25 | software & I love that we're able to put licenses on our code and explain where and
26 | how you can use it.
27 |
28 |
29 | But open source is a truly double-edged sword. There are always stories on Twitter with
30 | people explaining how they were taken advantage of, or don't have the resources to keep
31 | maintaining a project.
32 |
33 |
34 | Recently, a library called faker.js was nuked by the author in an attempt to
35 | make a "political" point. Wildly unsuccessful and, in my opinion, extremely immature.
36 | However, the community responded really quickly and quickly forked the project into{' '}
37 | @faker-js/faker. This meant developers could easily switch their project over
38 | to use a 100% API compatible, up-to-date and community-managed version of the project. The
39 | original author, Marak Squires, ended up deleting the original repo. As well as that,
40 | Squires also placed malicious code in another project of his, colors.js, that
41 | would infinite loop the victim's computer.
42 |
43 |
44 | But there was a reason for this. Thanks to the wonderful archive.org, we're able to see
45 | old and deleted posts explaining why this had happened — and it's a common frustration
46 | within the community. Famously, Marak mentions he will do{' '}
47 |
48 | No more free work
49 | {' '}
50 | and he's also written a good{' '}
51 |
52 | few hundred words
53 | {' '}
54 | on the topic, too.
55 |
56 |
57 | Okay, so he's frustrated with people taking Open Source for granted. What's the solution
58 | here? Well, fantastic companies like OpenCollective and GitHub are taking the initiative
59 | to provide a direct, low-fee method of sponsoring open source projects. Big companies like
60 | Discord, Stripe, and Microsoft have all sponsored small and large projects and sometimes
61 | they get their name on the README in return. At the moment, we're not quite there
62 | completely, but we're definitely heading in the right direction.
63 |
64 |
65 | Alternatively, a recent JavaScript library popped up called motion (
66 | motion.dev) which caused a bit of a stir in the
67 | community for shaking things up a little bit with regards to their monetization strategy.
68 | The library itself exists on npm and can be installed as you would any other node module,
69 | except there is no GitHub URL for the package..? Taking a look at the README on npm says
70 | the following:
71 |
72 |
73 |
74 | Become a sponsor and get access to the private Motion One repo. File issues, read the
75 | changelog and source code, and join discussions that help shape the future of the API.
76 |
77 |
78 |
79 | Okay, interesting, so you can use the module and read the documentation for free, but
80 | accessing the source code requires somewhat of a paid subscription. There is valid
81 | incentive for companies to do this as it allows them to not only audit the codebase (under
82 | security concerns), but also it allows for reading the source code to see how everything
83 | works and learn a lot.
84 |
85 |
86 |
87 | So what's the downside to this? Well, one of the reasons traditional open source is
88 | brilliant is because it allows anybody to freely see how something works — so assuming
89 | that is true, could we even call motion an open source project? What's more,
90 | a lot of individual developers are students or kids learning and as such, they're not
91 | financially able to support projects. On top of that, they are the generation we want to
92 | be educating the MOST about programming and so immediately cutting them off is definitely
93 | not a win.
94 |
95 |
96 |
97 | One final example of open source being painful has got to be Fastify. Fastify's creator is
98 | active on Twitter very often and there have been a few Tweets describing some headaches
99 | they have had to go through as a team to get Fastify to be successful. Keeping it short,
100 | but one thing they have done extensively has been advertising Fastify, which has kept it
101 | mainstream and allowed for more users and therefore sponsorships. Matteo Collina,
102 | Fastify's creator, has explained that had there not been the advertising done, Fastify
103 | would not be as maintained, if at all, as it is today. Right now, Fastify has two core
104 | maintainers and 16 on the core team overall.
105 |
106 |
107 |
108 | There is plenty of room for innovation in this space. I'm excited to see where GitHub
109 | sponsors and OpenCollective go and if we can see some large tech companies spreading the
110 | word about open source.
111 |
112 |
113 |
114 | By the way, I do accept sponsorships via GitHub, so if you enjoy my work or writing then
115 | please consider any spare VC funding you have just raised 😊{' '}
116 | github.com/sponsors/alii
117 |
118 |
119 |
Thanks for reading!
120 | >
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/blog/2022/08/strict-tsconfig/strict-tsconfig.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import {Highlighter} from '../../../../components/syntax-highligher';
3 | import {Post} from '../../../Post';
4 |
5 | export class StrictTSConfig extends Post {
6 | public name = 'A strict TSConfig';
7 | public slug = 'strict-tsconfig';
8 | public date = new Date('08 Sep 2022');
9 | public hidden = false;
10 | public excerpt = 'The strictest TypeScript configuration possible. "Look ma, no errors!"';
11 | public keywords = ['strict', 'tsconfig', 'typescript'];
12 |
13 | public render() {
14 | return (
15 | <>
16 |
17 | Here's a very strict TypeScript configuration file. All the safety checks you could ever
18 | want. "Look ma, no errors!"
19 |
50 | Although, nowadays I'm just using bun init...
51 |
52 |
53 |
144 | >
145 | );
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/blog/2023/wtf-esm/wtf-esm.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import {ExternalLink} from '../../../components/external-link';
3 | import {Note} from '../../../components/note';
4 | import {Highlighter} from '../../../components/syntax-highligher';
5 | import {Post} from '../../Post';
6 |
7 | export class WTFESM extends Post {
8 | public name = 'WTF, ESM!?';
9 | public slug = 'wtf-esm';
10 | public date = new Date('2023-04-03');
11 | public hidden = true;
12 | public excerpt =
13 | '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.';
14 |
15 | public keywords = ['javascript', 'esm', 'typescript', 'publish', 'package', 'npm', 'node'];
16 |
17 | public render() {
18 | return (
19 | <>
20 |
WTF, ESM!?
21 |
22 |
23 | I{' '}
24 |
25 | recently Tweeted
26 | {' '}
27 | about publishing a dual ESM and CJS package to npm. It got a lot of likes, and here is why
28 | that matters. It's important that you understand that I was wrong in my Tweet, and things
29 | are arguably easier or more difficult than they seem. This is the current state of
30 | publishing a JS package.
31 |
32 |
33 |
Preface
34 |
35 |
36 | I am so incredibly grateful for the absolutely wonderful{' '}
37 | Andrew Branch, who took a lot
38 | of time out of his vacation to correct my Tweet and wrote{' '}
39 |
40 | this excellent thread
41 |
42 | . A lot of this blog post is regurgitated text of how I interpreted his Tweets.
43 |
44 |
45 |
46 | Andrew works on TypeScript itself at Microsoft, specifically on auto imports and modules.
47 | It's likely he's the only person on the planet who knows exactly how this works inside
48 | out. It's been rumoured that he will be summoned if you utter "module resolution" three
49 | times in the dark. Thank you Andy - you're truly a super star ⭐💖
50 |
51 |
52 |
53 |
54 |
55 | Right now, it's extraordinarily clear we are experiencing growing pains in our great
56 | migration to ECMAScript Modules. Below is the part of my package.json that I
57 | posted.
58 |
80 | As mentioned above, I made some mistakes here. First of all, it's important to
81 | differentiate between what is runtime code that engines will understand (what is
82 | JavaScript), and what is type definitions (what is TypeScript). This (seems) easy enough,
83 | we can see clearly that there are two types fields. One is under the{' '}
84 | . entrypoint for exports, the other is at the root. Let's break
85 | it down.
86 |
87 |
88 |
Where did I go wrong?
89 |
90 |
91 | It's pretty hard to get a conclusive answer from the "crowd" of JavaScript developers
92 | about the best way to publish a package to npm. Everyone has conflicting answers & we
93 | all seem to be following what already exists on GitHub and npm. There are lots of packages
94 | that are published technically incorrectly but used and installed by millions of people.
95 | This means a lot of packages follow what I'm calling a colloquial standard. Here's what I{' '}
96 | *thought to be true*, and so do most other devs...
97 |
98 |
99 |
100 | Below is not the correct way to publish a package to npm. This is what I thought was
101 | correct at the time of Tweeting.
102 |
103 |
104 |
105 |
106 | .types at the root is for TypeScript type definitions. A single{' '}
107 | .d.ts file can define all exported symbols in your package.
108 |
109 |
110 |
111 | .main is for CJS before exports existed. You can emit a single
112 | CJS compatible file that can be consumed by (legacy) runtimes.
113 |
114 |
115 |
116 | .module is for an ESM entrypoint before exports existed. This
117 | was mostly used by bundlers like Webpack, and has never been part of any standard. It's
118 | superseded by exports, but it might be good to keep in order to support the
119 | older bundlers.
120 |
121 |
122 |
123 | .exports is the new standard for defining entrypoints for your package. It
124 | is a map of entrypoints to files. The . entrypoint is the default
125 | entrypoint. We also include ./package.json so the package.json file is also
126 | accessible. The exports field is supported in modern runtimes. Node has
127 | supported it since v16.0.0 - for this reason, you will see exports{' '}
128 | sometimes referenced as node16.
129 |
130 |
131 |
132 | .exports.*.types is for TypeScript type definitions. A single{' '}
133 | .d.ts file can define all exported symbols in your package for both CJS and
134 | ESM.
135 |
136 |
137 |
138 | .exports.*.import is for ESM. This is the entrypoint for how a modern
139 | runtime should import your package when running under CommonJS. It is a single ESM
140 | compatible file.
141 |
142 |
143 |
144 | .exports.*.require is for CJS. This is the entrypoint for how a modern
145 | runtime should import your package when running under CommonJS. It is a single CJS
146 | compatible file.
147 |
148 |
149 |
150 | .exports.*.default is for when a runtime does not match any other
151 | condition, and is a fallback. It's also within the spec to specify default{' '}
152 | as the only entrypoint. I did not use default in my initial Tweet.
153 |
154 |
155 |
156 |
157 | I made a few mistakes here. First of all, types are specific to ESM and CJS. This means
158 | there should be twotypes fields. One for ESM, one for CJS. Even the
159 | TypeScript documentation gets this wrong, and is something they're working on updating.
160 | Solutions for this are also pretty wild. I've managed to get things working by simply
161 | copying ./dist/index.d.ts to ./dist/index.d.cts after bundling,
162 | and making the following changes to my package.json.
163 |
186 | Note that we point to a .js file and not .mjs when targeting ESM. This is because our
187 | package.json has type set to module. This tells our runtime that
188 | all files are assumed to be ESM unless they have a .cjs extension. There's no
189 | such thing as an ESM package, only ESM files. Using "type": "module", is just
190 | a way to tell the runtime to interpret existing files as ESM.
191 |
192 |
193 |
What gives?
194 |
195 |
196 | I'm still figuring this all out, and I'm not an expert. I'm just trying to share what I
197 | have learned so far. If you have any corrections or suggestions, please let me know!
198 |
199 |
200 |
201 | Clearly, this is messy. It's messy because we're trying to support a lot of different
202 | runtimes, and we're trying to support them all at once. We're trying to support ESM, CJS,
203 | legacy bundlers, modern bundlers, and TypeScript. We're trying to support all of these
204 | runtimes at once, and finally, we're trying to support them all at once in a single{' '}
205 | package.json file. Few other languages suffer from this level of complexity
206 | and fragmentation.
207 |
208 |
209 |
210 | Let's break down the mess and why all these things are the way they are. Starting off with{' '}
211 | exports.
212 |
213 |
214 |
215 | exports is the modern way to define what your package exports. We have
216 | already established that it is a map of entrypoints to files. Let's step through what
217 | happens when a runtime/consumer (we'll use the word consumer, because TypeScript - which
218 | is not a runtime - is also reading our code in this case) wants to import our package.
219 |
234 | Consumer resolve the source code for my-package. In Node.js this is done
235 | by looking for the folder name in node_modules, and then finding the{' '}
236 | package.json. In any case, this is up to the consumer to implement
237 |
238 |
239 |
240 |
241 |
242 | Consumer finds package.json file in the source code folder, and begins to
243 | read the exports field
244 |
245 |
246 |
247 |
248 | It steps through each field (in order, despite it being an object) and checks if the
249 | condition the consumer is looking for exists in the exports field.
250 |
251 |
252 |
253 |
254 | If the condition is met, the consumer will use the file specified in the{' '}
255 | exports field as the entrypoint for the package. If the condition is not
256 | met, it will continue to the next field. If no condition is met, a consumer will
257 | usually exit/throw an error.
258 |
259 |
260 |
261 | An example of a condition being met could be Node.js looking for an ESM file. In this
262 | case, it would look for the import condition first, before trying to fall
263 | back to default if it exists.
264 |
265 |
266 |
267 | >
268 | );
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/blog/2025/ambient-declarations/ambient-declarations.tsx:
--------------------------------------------------------------------------------
1 | import {stripIndent} from 'common-tags';
2 | import {Note} from '../../../components/note';
3 | import {Highlighter, Shell} from '../../../components/syntax-highligher';
4 | import {Post} from '../../Post';
5 |
6 | export class AmbientDeclarations extends Post {
7 | public name = 'Ambient Declarations';
8 | public slug = 'ambient-declarations';
9 | public date = new Date('9 May 2025');
10 | public hidden = false;
11 | public keywords = ['Ambient Modules', 'TypeScript', 'Module Resolution'];
12 | public excerpt = 'Explaining ambient declarations with @types/bun as an example';
13 |
14 | public render() {
15 | return (
16 | <>
17 |
Ambient Declarations
18 |
19 | I recently landed a pull request (
20 | #18024) in{' '}
21 | Bun that reorganized and rewrote significant portions of
22 | Bun's TypeScript definitions. Working on this PR made me realize how little documentation
23 | there is on ambient declarations, so I wanted to write about it.
24 |
25 |
What are ambient declarations?
26 |
I'll start by answering this question with a couple questions...
27 |
28 | 1. How does TypeScript know the types of my node_modules, which are mostly
29 | all .js files?
30 |
31 |
32 | 2. How does TypeScript know the types of APIs that exist in my runtime?
33 |
34 |
35 | The short answer: It can't!
36 |
37 |
38 | The short but slightly longer answer is that it CAN with some extra files - ambient
39 | declarations! These are files that exist somewhere in your project (usually in{' '}
40 | node_modules) that contain type information and tell TypeScript what{' '}
41 | things exist at runtime. They use the file extension .d.ts, with the
42 | `.d` denoting "declaration".
43 |
44 |
45 | By things I mean anything you import and use. That could be functions, classes,
46 | variables, modules themselves, APIs from your runtime, etc.
47 |
48 |
49 | They're called "ambient" declarations because in the TypeScript universe ambient simply
50 | means "without implementation"
51 |
52 |
53 | If you've ever imported a package and magically got autocomplete and type checking, you've
54 | benefited from ambient declarations.
55 |
56 |
A simple ambient declaration file could look like this:
57 |
58 | {stripIndent`
59 | /**
60 | * Performs addition using AI and LLMs
61 | *
62 | * @param a - The first number
63 | * @param b - The second number
64 | *
65 | * @returns The sum of a and b (probably)
66 | */
67 | export declare function add(a: number, b: number): Promise;
68 | `}
69 |
70 |
71 | If you can already read TypeScript this ambient declaration will be very easy to
72 | understand. You can clearly see a JSDoc comment, the types of the arguments, the return
73 | type, an export keyword, etc. It almost looks like real TypeScript, except the really
74 | important part to note here is the keyword declare is used. This keyword
75 | tells TypeScript to not expect any runtime code to exist here, it's purely a type
76 | declaration only.
77 |
78 |
79 | It's completely legitimate and legal to use the declare keyword inside of
80 | regular .ts files. There are many use cases for this, a common one being declaring types
81 | of globals.
82 |
83 |
84 |
How Does TypeScript Find Types?
85 |
86 | Module resolution is an incredibly complex topic, but it boils down to TypeScript looking
87 | for relevant types in a few places.
88 |
89 |
90 |
91 | Bundled types: Some packages include their own .d.ts files.
92 |
93 |
94 | DefinitelyTyped: If not, TypeScript looks in @types/ packages in{' '}
95 | node_modules.
96 |
97 |
98 | Your own project: You can add .d.ts files anywhere in your project
99 | to describe types for JS code, global variables, or even new modules.
100 |
101 |
102 | Source: If the module resolution algorithm resolves to an actual TypeScript file,
103 | then the types can be read from the original source code anyway. Some packages on NPM
104 | also publish their TypeScript source and allow modern tooling to consume it directly.{' '}
105 |
106 | Ambient declarations are NOT used in either of these scenarios
107 |
108 | .
109 |
110 |
111 |
Ambient vs. Regular Declarations
112 |
113 | Regular declarations are for code you write and control.
114 |
115 |
116 | {stripIndent`
117 | export function add(a: number, b: number): number {
118 | return a + b;
119 | }`}
120 |
121 |
122 | Ambient declarations are for code that exists elsewhere.
123 |
130 | The declare keyword tells TypeScript: "This exists at runtime, but you won't
131 | find it here."
132 |
133 |
Module vs. Script Declarations
134 |
135 |
136 | Module declarations: Any .d.ts file with a top-level{' '}
137 | import or export. Types are added to the module.
138 |
139 |
140 | Script (global) declarations: No top-level import/export. Types are added to the
141 | global scope.
142 |
143 |
144 |
145 |
146 |
147 |
File Type
148 |
Example Syntax
149 |
Scope
150 |
151 |
152 |
153 |
154 |
Module
155 |
156 | export declare function foo(): void;
157 |
158 |
Module only (must be imported)
159 |
160 |
161 |
Script (global)
162 |
163 | declare function setTimeout(...): number;
164 |
165 | declare function foo(): void;
166 |
167 |
Global (available everywhere)
168 |
169 |
170 |
171 |
172 | Rule of thumb: An ambient declaration file is global unless it has a top-level
173 | import/export.
174 |
175 |
176 | Script files can pollute the global namespace and can very easily{' '}
177 | clash with other declarations.
178 | Prefer the module pattern unless you really need to patch globalThis.
179 |
180 |
181 |
182 | Why does this distinction exist? TypeScript is old in JavaScript's history - it predates
183 | the modern module system (ESM) and needed to support the "everything is global" style of
184 | early JS. That's why it still supports both module and script (global) declaration files.
185 |
186 |
187 | How does TypeScript treat these differently?
188 |
189 |
190 |
191 | Module: Everything you declare is private to that module and must be explicitly
192 | imported by the consumer - just like regular TypeScript/ESM code.
193 |
194 |
195 | Script (global): Everything is injected directly into the global scope of every
196 | file in your program. This is how the DOM lib ships types like window,{' '}
197 | document, and functions like setTimeout.
198 |
199 |
200 |
201 | When would you use each?
202 |
203 |
204 |
205 | Module: For packages, libraries, and almost all modern code.
206 |
207 |
208 | Script: For patching browser globals, legacy code, or when you really need to add
209 | something to the global scope.
210 |
211 |
212 |
213 | You can still augment the global scope from inside a module-style declaration file by
214 | using the global {'{ ... }'} escape hatch, but that should be reserved for
215 | unavoidable edge-cases.
216 |
217 |
218 |
Declaring Global Types
219 |
220 | Suppose you want to add a global variable that your runtime creates, or perhaps a library
221 | you're using doesn't have types for:
222 |
229 | Because this declaration file is NOT a module, this will be accessible everywhere in your
230 | program.
231 |
232 |
233 | What if you wanted to add something to the window object? TypeScript declares
234 | the window variable exists by assigning it to an interface called Window,
235 | which is also declared globally. You can perform{' '}
236 |
237 | Declaration Merging
238 | {' '}
239 | to extend that interface, and tell TypeScript about new properties that exist.
240 |
251 | You can declare a module by its name. As long as the ambient declaration file gets
252 | referenced or included in your build somehow, then TypeScript will make the module
253 | available.
254 |
263 | This syntax also allows for declaring modules with wildcard matching. We do this in{' '}
264 | @types/bun, since Bun allows for importing .toml and{' '}
265 | .html files.
266 |
300 | Make sure your tsconfig.json includes the types folder (usually automatic).
301 |
302 |
303 |
Compiler contract
304 |
305 | Since ambient modules don't contain runtime code, they should be treated like "promises"
306 | or "contracts" that you are making with the compiler. They're like documentation that
307 | TypeScript can understand. Just like documentation for humans, it can get out of sync with
308 | the actual runtime code. A lot of the work I'm doing at Bun is ensuring our type
309 | definitions are up to date with Bun's runtime APIs.
310 |
311 |
Conflicts
312 |
313 | While doing research for the pull request mentioned at the beginning, I found a few cases
314 | where the compiler was not able to resolve the types of some of Bun's APIs because we had
315 | declared that certain symbols existed, where they might have already been declared by{' '}
316 |
317 | lib.dom.d.ts
318 | {' '}
319 | (the builtin types that TypeScript provides by default) or things like{' '}
320 | @types/node (the types for Node.js). .
321 |
322 |
323 | Avoiding these conflicts is unfortunately not always possible. Bun implements a really
324 | solid best-effort approach to this, but sometimes you just have to get creative. For
325 | example, you might see code like this to "force" TypeScript to use one type over another:
326 |
335 | Bun's types take this a step further by using a clever trick that lets us use the built-in
336 | types if they exist, with a graceful fallback when they don't.
337 |
338 |
339 |
340 | {stripIndent`
341 | declare module "bun" {
342 | namespace __internal {
343 | // \`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
344 | type LibDomIsLoaded = typeof globalThis extends { onabort: any } ? true : false;
345 |
346 | /**
347 | * Helper type for avoiding conflicts in types.
348 | *
349 | * Uses the lib.dom.d.ts definition if it exists, otherwise defines it locally.
350 | *
351 | * This is to avoid type conflicts between lib.dom.d.ts and \@types/bun.
352 | *
353 | * Unfortunately some symbols cannot be defined when both Bun types and lib.dom.d.ts types are loaded,
354 | * and since we can't redeclare the symbol in a way that satisfies both, we need to fallback
355 | * to the type that lib.dom.d.ts provides.
356 | */
357 | type UseLibDomIfAvailable =
358 | LibDomIsLoaded extends true
359 | ? typeof globalThis extends { [K in GlobalThisKeyName]: infer T } // if it is loaded, infer it from \`globalThis\` and use that value
360 | ? T
361 | : Otherwise // Not defined in lib dom (or anywhere else), so no conflict. We can safely use our own definition
362 | : Otherwise; // Lib dom not loaded anyway, so no conflict. We can safely use our own definition
363 | }
364 | }
365 | `}
366 |
367 |
368 |
369 | {stripIndent`
370 | declare var Worker: Bun.__internal.UseLibDomIfAvailable<'Worker', {
371 | new(filename: string, options?: Bun.WorkerOptions): Worker;
372 | }>;
373 | `}
374 |
375 |
376 |
377 | This declares that the Worker runtime value exists, and will use the version
378 | from TypeScript's builtin lib files if they're loaded already in the program, and if not
379 | it will use the version passed as the second argument.
380 |
381 |
382 | This trick means we can write types that can exist in many different environments without
383 | worrying about impossible-to-fix conflicts breaking the build.
384 |
385 |
386 |
Declaring entire modules as global namespaces
387 |
388 | In Bun, everything importable from the 'bun' module is also available on the
389 | global namespace Bun
390 |
423 |
424 | {stripIndent`
425 | import * as BunModule from "bun";
426 |
427 | declare global {
428 | export import Bun = BunModule;
429 | }
430 | `}
431 |
432 |
Let's break it down
433 |
434 |
435 |
We have an import statement, so this file becomes a module.
436 |
437 |
438 |
439 | We import everything from the bun module and alias to a namespace called{' '}
440 | BunModule
441 |
442 |
443 |
444 |
445 | We use the `declare global` block to escape back into global scope, and then use the
446 | funky syntax export import to re-export the namespace to the global scope
447 |
448 |
449 |
450 |
451 | This export import syntax a way of saying "re-export this namespace" - except
452 | when declaring on the global scope (inside a declare global {'{ }'} block or
453 | inside a script/global file) the export keyword kind of turns into a namespace declaration
454 | for the global scope.
455 |
456 |
457 | Here is the "rest" of @types/bun that piece this all together
458 |
459 |
460 |
461 | {stripIndent`
462 | declare module "bun" {
463 | /**
464 | * Creates a new BunFile instance
465 | * @param path - The path to the file
466 | * @returns A new BunFile instance
467 | */
468 | function file(path: string): BunFile;
469 |
470 | interface BunFile {
471 | /* ... */
472 | }
473 | }
474 | `}
475 |
476 |
477 |
478 | {stripIndent`
479 | // You "import" types by using triple-slash references,
480 | // which tell TypeScript to add these declarations to the build.
481 |
482 | ///
483 | ///
484 | `}
485 |
486 |
487 |
488 | {stripIndent`
489 | {
490 | "name": "@types/bun",
491 | "version": "1.2.13",
492 | "types": "./index.d.ts",
493 | // ...
494 | }
495 | `}
496 |
497 |
498 |
499 | In previous versions of Bun's types, the Bun global was defined as a variable that
500 | imported the bun module.
501 |
508 | But since this is a runtime value, we have lost all of the types that are exported from
509 | the bun module. For example we can't use Bun.BunFile in our
510 | code.
511 |
512 |
513 | In the pull request mentioned at the beginning, I changed previous declaration to use the{' '}
514 | export import syntax which fixed this issue. It means you can now use the Bun
515 | namespace exactly like you'd expect the bun module to behave.
516 |
517 |
518 |
Ambient declaration gotchas
519 |
520 |
521 | "Cannot find module" or "type not found" errors: Make sure your{' '}
522 | .d.ts file is included in the project (check tsconfig.json's{' '}
523 | include/exclude).
524 |
525 |
526 |
527 | Conflicts: If two libraries declare the same global, you'll get errors. Prefer
528 | module declarations, and avoid globals unless necessary.
529 |
13 | You first visited my website on {firstEverLoadTime.toLocaleDateString()} at{' '}
14 | {firstEverLoadTime.toLocaleTimeString()} and on this first visit, you were on the{' '}
15 | {stats.path} page. Since then, you have visited {visits - 1} more times.{' '}
16 | {visits > 1 && 'Thanks for coming back!'}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/syntax-highligher.tsx:
--------------------------------------------------------------------------------
1 | import type {PropsWithChildren} from 'react';
2 | import SyntaxHighlighter from 'react-syntax-highlighter';
3 |
4 | import light from 'react-syntax-highlighter/dist/cjs/styles/hljs/lightfair';
5 | import dark from 'react-syntax-highlighter/dist/cjs/styles/hljs/vs2015';
6 |
7 | import clsx from 'clsx';
8 | import {TbBrandCss3, TbBrandHtml5, TbBrandJavascript, TbBrandTypescript} from 'react-icons/tb';
9 |
10 | const Pre = ({children}: PropsWithChildren) =>
This is a list of random experiments I've built on this website. There's not a lot here
7 |
8 |
9 |
10 | Morphing Shapes
11 |
12 | Animating and shifting divs using just css transitions and JS to initiate them
13 |
14 |
15 |
16 |
17 | Monzo Dashboard
18 |
19 | Using the Monzo API to display personal account details. Unfortunately, the Monzo API
20 | requires me to manually add users, so if you want access, contact me.
21 |
22 |
23 |
24 |
25 | Rekordbox History Parser
26 |
27 | Rekordbox exports history in a format not so useful for copy pasting. This is a tiny
28 | tool to fix that
29 |
93 | {bwitch(acct.type)
94 | .case(
95 | 'uk_monzo_flex_backing_loan',
96 | () =>
97 | 'This is a loan account used for flex transactions. It has no balance and will be considered closed once the debt is paid off.',
98 | )
99 | .or(() => 'This type of acccount has no balance')}
100 |