···11+22+Default to using Bun instead of Node.js.
33+44+- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
55+- Use `bun test` instead of `jest` or `vitest`
66+- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
77+- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
88+- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
99+- Use `bunx <package> <command>` instead of `npx <package> <command>`
1010+- Bun automatically loads .env, so don't use dotenv.
1111+1212+## APIs
1313+1414+- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
1515+- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
1616+- `Bun.redis` for Redis. Don't use `ioredis`.
1717+- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
1818+- `WebSocket` is built-in. Don't use `ws`.
1919+- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
2020+- Bun.$`ls` instead of execa.
2121+2222+## Testing
2323+2424+Use `bun test` to run tests.
2525+2626+```ts#index.test.ts
2727+import { test, expect } from "bun:test";
2828+2929+test("hello world", () => {
3030+ expect(1).toBe(1);
3131+});
3232+```
3333+3434+## Frontend
3535+3636+Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
3737+3838+Server:
3939+4040+```ts#index.ts
4141+import index from "./index.html"
4242+4343+Bun.serve({
4444+ routes: {
4545+ "/": index,
4646+ "/api/users/:id": {
4747+ GET: (req) => {
4848+ return new Response(JSON.stringify({ id: req.params.id }));
4949+ },
5050+ },
5151+ },
5252+ // optional websocket support
5353+ websocket: {
5454+ open: (ws) => {
5555+ ws.send("Hello, world!");
5656+ },
5757+ message: (ws, message) => {
5858+ ws.send(message);
5959+ },
6060+ close: (ws) => {
6161+ // handle close
6262+ }
6363+ },
6464+ development: {
6565+ hmr: true,
6666+ console: true,
6767+ }
6868+})
6969+```
7070+7171+HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
7272+7373+```html#index.html
7474+<html>
7575+ <body>
7676+ <h1>Hello, world!</h1>
7777+ <script type="module" src="./frontend.tsx"></script>
7878+ </body>
7979+</html>
8080+```
8181+8282+With the following `frontend.tsx`:
8383+8484+```tsx#frontend.tsx
8585+import React from "react";
8686+import { createRoot } from "react-dom/client";
8787+8888+// import .css files directly and it works
8989+import './index.css';
9090+9191+const root = createRoot(document.body);
9292+9393+export default function Frontend() {
9494+ return <h1>Hello, world!</h1>;
9595+}
9696+9797+root.render(<Frontend />);
9898+```
9999+100100+Then, run index.ts
101101+102102+```sh
103103+bun --hot ./index.ts
104104+```
105105+106106+For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+15
README.md
···11+# catnip
22+33+To install dependencies:
44+55+```bash
66+bun install
77+```
88+99+To run:
1010+1111+```bash
1212+bun run index.ts
1313+```
1414+1515+This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···11+# React + TypeScript + Vite
22+33+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
44+55+Currently, two official plugins are available:
66+77+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
88+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
99+1010+## React Compiler
1111+1212+The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
1313+1414+## Expanding the ESLint configuration
1515+1616+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
1717+1818+```js
1919+export default defineConfig([
2020+ globalIgnores(['dist']),
2121+ {
2222+ files: ['**/*.{ts,tsx}'],
2323+ extends: [
2424+ // Other configs...
2525+2626+ // Remove tseslint.configs.recommended and replace with this
2727+ tseslint.configs.recommendedTypeChecked,
2828+ // Alternatively, use this for stricter rules
2929+ tseslint.configs.strictTypeChecked,
3030+ // Optionally, add this for stylistic rules
3131+ tseslint.configs.stylisticTypeChecked,
3232+3333+ // Other configs...
3434+ ],
3535+ languageOptions: {
3636+ parserOptions: {
3737+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
3838+ tsconfigRootDir: import.meta.dirname,
3939+ },
4040+ // other options...
4141+ },
4242+ },
4343+])
4444+```
4545+4646+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
4747+4848+```js
4949+// eslint.config.js
5050+import reactX from 'eslint-plugin-react-x'
5151+import reactDom from 'eslint-plugin-react-dom'
5252+5353+export default defineConfig([
5454+ globalIgnores(['dist']),
5555+ {
5656+ files: ['**/*.{ts,tsx}'],
5757+ extends: [
5858+ // Other configs...
5959+ // Enable lint rules for React
6060+ reactX.configs['recommended-typescript'],
6161+ // Enable lint rules for React DOM
6262+ reactDom.configs.recommended,
6363+ ],
6464+ languageOptions: {
6565+ parserOptions: {
6666+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
6767+ tsconfigRootDir: import.meta.dirname,
6868+ },
6969+ // other options...
7070+ },
7171+ },
7272+])
7373+```
···11+import { document, record, object, required, string, integer, array, blob, ref } from '@atcute/lexicon-doc/builder';
22+33+const artistCredit = object({
44+ description: 'An artist credit. Either a DID (for atproto users) or a plain name (for non-atproto collaborators), or both.',
55+ properties: {
66+ did: string({
77+ format: 'did',
88+ description: 'The atproto DID of the collaborator, if they are an atproto user.',
99+ }),
1010+ name: string({
1111+ maxLength: 128,
1212+ description: 'Display name or stage name of the collaborator. Required if DID is absent.',
1313+ }),
1414+ },
1515+});
1616+1717+export default document({
1818+ id: 'ca.ansxor.catnip.track',
1919+ description: 'A music track record for the Catnip music platform.',
2020+ defs: {
2121+ main: record({
2222+ key: 'tid',
2323+ record: object({
2424+ properties: {
2525+ title: required(string({
2626+ maxLength: 256,
2727+ description: 'The title of the track.',
2828+ })),
2929+3030+ description: string({
3131+ maxLength: 2000,
3232+ description: 'An optional description or note about the track.',
3333+ }),
3434+3535+ createdAt: required(string({
3636+ format: 'datetime',
3737+ description: 'Timestamp of when the record was created on the PDS.',
3838+ })),
3939+4040+ releaseDate: string({
4141+ format: 'datetime',
4242+ description: 'Optional self-reported release date of the track. Used for display only.',
4343+ }),
4444+4545+ durationMs: integer({
4646+ minimum: 0,
4747+ description: 'Optional track duration in milliseconds. Treat as a display hint only — not authoritative.',
4848+ }),
4949+5050+ artists: array({
5151+ maxLength: 32,
5252+ description: 'List of artists or collaborators. Each entry must have at least a DID or a name.',
5353+ items: ref({ ref: '#artistCredit' }),
5454+ }),
5555+5656+ tags: array({
5757+ maxLength: 10,
5858+ description: 'Optional tags for the track, e.g. genre, mood, instrumentation.',
5959+ items: string({
6060+ maxLength: 64,
6161+ }),
6262+ }),
6363+6464+ language: string({
6565+ maxLength: 16,
6666+ description: "BCP 47 language code for the track's lyrics or primary language (e.g. 'en', 'ja', 'fr-CA').",
6767+ }),
6868+6969+ license: string({
7070+ knownValues: [
7171+ 'CC0',
7272+ 'CC BY',
7373+ 'CC BY-SA',
7474+ 'CC BY-NC',
7575+ 'CC BY-NC-SA',
7676+ 'CC BY-ND',
7777+ 'CC BY-NC-ND',
7878+ 'All Rights Reserved',
7979+ 'Other',
8080+ ],
8181+ description: 'The license under which the track is released.',
8282+ }),
8383+8484+ lyrics: string({
8585+ maxLength: 50000,
8686+ description: 'Optional lyrics for the track, as plain text.',
8787+ }),
8888+8989+ albumArt: blob({
9090+ accept: ['image/jpeg', 'image/png', 'image/webp'],
9191+ maxSize: 1000000,
9292+ description: 'Optional album art image. Recommended size: at least 500x500px.',
9393+ }),
9494+9595+ audio: required(blob({
9696+ accept: ['audio/ogg', 'audio/opus'],
9797+ maxSize: 52428800,
9898+ description: 'Required self-hosted audio file in Opus format (~50MB limit). Mutually exclusive with externalUrl at the app level.',
9999+ })),
100100+101101+ externalUrl: string({
102102+ format: 'uri',
103103+ maxLength: 512,
104104+ description: 'Optional link to the track on an external platform (e.g. BandCamp, SoundCloud). Mutually exclusive with audio at the app level.',
105105+ }),
106106+ },
107107+ }),
108108+ }),
109109+110110+ artistCredit,
111111+ },
112112+});