···1+2+Default to using Bun instead of Node.js.
3+4+- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
5+- Use `bun test` instead of `jest` or `vitest`
6+- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
7+- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
8+- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
9+- Use `bunx <package> <command>` instead of `npx <package> <command>`
10+- Bun automatically loads .env, so don't use dotenv.
11+12+## APIs
13+14+- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15+- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16+- `Bun.redis` for Redis. Don't use `ioredis`.
17+- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18+- `WebSocket` is built-in. Don't use `ws`.
19+- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20+- Bun.$`ls` instead of execa.
21+22+## Testing
23+24+Use `bun test` to run tests.
25+26+```ts#index.test.ts
27+import { test, expect } from "bun:test";
28+29+test("hello world", () => {
30+ expect(1).toBe(1);
31+});
32+```
33+34+## Frontend
35+36+Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37+38+Server:
39+40+```ts#index.ts
41+import index from "./index.html"
42+43+Bun.serve({
44+ routes: {
45+ "/": index,
46+ "/api/users/:id": {
47+ GET: (req) => {
48+ return new Response(JSON.stringify({ id: req.params.id }));
49+ },
50+ },
51+ },
52+ // optional websocket support
53+ websocket: {
54+ open: (ws) => {
55+ ws.send("Hello, world!");
56+ },
57+ message: (ws, message) => {
58+ ws.send(message);
59+ },
60+ close: (ws) => {
61+ // handle close
62+ }
63+ },
64+ development: {
65+ hmr: true,
66+ console: true,
67+ }
68+})
69+```
70+71+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.
72+73+```html#index.html
74+<html>
75+ <body>
76+ <h1>Hello, world!</h1>
77+ <script type="module" src="./frontend.tsx"></script>
78+ </body>
79+</html>
80+```
81+82+With the following `frontend.tsx`:
83+84+```tsx#frontend.tsx
85+import React from "react";
86+import { createRoot } from "react-dom/client";
87+88+// import .css files directly and it works
89+import './index.css';
90+91+const root = createRoot(document.body);
92+93+export default function Frontend() {
94+ return <h1>Hello, world!</h1>;
95+}
96+97+root.render(<Frontend />);
98+```
99+100+Then, run index.ts
101+102+```sh
103+bun --hot ./index.ts
104+```
105+106+For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+15
README.md
···000000000000000
···1+# catnip
2+3+To install dependencies:
4+5+```bash
6+bun install
7+```
8+9+To run:
10+11+```bash
12+bun run index.ts
13+```
14+15+This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···1+# React + TypeScript + Vite
2+3+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+5+Currently, two official plugins are available:
6+7+- [@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
8+- [@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
9+10+## React Compiler
11+12+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.
13+14+## Expanding the ESLint configuration
15+16+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17+18+```js
19+export default defineConfig([
20+ globalIgnores(['dist']),
21+ {
22+ files: ['**/*.{ts,tsx}'],
23+ extends: [
24+ // Other configs...
25+26+ // Remove tseslint.configs.recommended and replace with this
27+ tseslint.configs.recommendedTypeChecked,
28+ // Alternatively, use this for stricter rules
29+ tseslint.configs.strictTypeChecked,
30+ // Optionally, add this for stylistic rules
31+ tseslint.configs.stylisticTypeChecked,
32+33+ // Other configs...
34+ ],
35+ languageOptions: {
36+ parserOptions: {
37+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38+ tsconfigRootDir: import.meta.dirname,
39+ },
40+ // other options...
41+ },
42+ },
43+])
44+```
45+46+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:
47+48+```js
49+// eslint.config.js
50+import reactX from 'eslint-plugin-react-x'
51+import reactDom from 'eslint-plugin-react-dom'
52+53+export default defineConfig([
54+ globalIgnores(['dist']),
55+ {
56+ files: ['**/*.{ts,tsx}'],
57+ extends: [
58+ // Other configs...
59+ // Enable lint rules for React
60+ reactX.configs['recommended-typescript'],
61+ // Enable lint rules for React DOM
62+ reactDom.configs.recommended,
63+ ],
64+ languageOptions: {
65+ parserOptions: {
66+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67+ tsconfigRootDir: import.meta.dirname,
68+ },
69+ // other options...
70+ },
71+ },
72+])
73+```
···1+import { document, record, object, required, string, integer, array, blob, ref } from '@atcute/lexicon-doc/builder';
2+3+const artistCredit = object({
4+ description: 'An artist credit. Either a DID (for atproto users) or a plain name (for non-atproto collaborators), or both.',
5+ properties: {
6+ did: string({
7+ format: 'did',
8+ description: 'The atproto DID of the collaborator, if they are an atproto user.',
9+ }),
10+ name: string({
11+ maxLength: 128,
12+ description: 'Display name or stage name of the collaborator. Required if DID is absent.',
13+ }),
14+ },
15+});
16+17+export default document({
18+ id: 'ca.ansxor.catnip.track',
19+ description: 'A music track record for the Catnip music platform.',
20+ defs: {
21+ main: record({
22+ key: 'tid',
23+ record: object({
24+ properties: {
25+ title: required(string({
26+ maxLength: 256,
27+ description: 'The title of the track.',
28+ })),
29+30+ description: string({
31+ maxLength: 2000,
32+ description: 'An optional description or note about the track.',
33+ }),
34+35+ createdAt: required(string({
36+ format: 'datetime',
37+ description: 'Timestamp of when the record was created on the PDS.',
38+ })),
39+40+ releaseDate: string({
41+ format: 'datetime',
42+ description: 'Optional self-reported release date of the track. Used for display only.',
43+ }),
44+45+ durationMs: integer({
46+ minimum: 0,
47+ description: 'Optional track duration in milliseconds. Treat as a display hint only — not authoritative.',
48+ }),
49+50+ artists: array({
51+ maxLength: 32,
52+ description: 'List of artists or collaborators. Each entry must have at least a DID or a name.',
53+ items: ref({ ref: '#artistCredit' }),
54+ }),
55+56+ tags: array({
57+ maxLength: 10,
58+ description: 'Optional tags for the track, e.g. genre, mood, instrumentation.',
59+ items: string({
60+ maxLength: 64,
61+ }),
62+ }),
63+64+ language: string({
65+ maxLength: 16,
66+ description: "BCP 47 language code for the track's lyrics or primary language (e.g. 'en', 'ja', 'fr-CA').",
67+ }),
68+69+ license: string({
70+ knownValues: [
71+ 'CC0',
72+ 'CC BY',
73+ 'CC BY-SA',
74+ 'CC BY-NC',
75+ 'CC BY-NC-SA',
76+ 'CC BY-ND',
77+ 'CC BY-NC-ND',
78+ 'All Rights Reserved',
79+ 'Other',
80+ ],
81+ description: 'The license under which the track is released.',
82+ }),
83+84+ lyrics: string({
85+ maxLength: 50000,
86+ description: 'Optional lyrics for the track, as plain text.',
87+ }),
88+89+ albumArt: blob({
90+ accept: ['image/jpeg', 'image/png', 'image/webp'],
91+ maxSize: 1000000,
92+ description: 'Optional album art image. Recommended size: at least 500x500px.',
93+ }),
94+95+ audio: required(blob({
96+ accept: ['audio/ogg', 'audio/opus'],
97+ maxSize: 52428800,
98+ description: 'Required self-hosted audio file in Opus format (~50MB limit). Mutually exclusive with externalUrl at the app level.',
99+ })),
100+101+ externalUrl: string({
102+ format: 'uri',
103+ maxLength: 512,
104+ description: 'Optional link to the track on an external platform (e.g. BandCamp, SoundCloud). Mutually exclusive with audio at the app level.',
105+ }),
106+ },
107+ }),
108+ }),
109+110+ artistCredit,
111+ },
112+});