chore: initial commit
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
public/posts
|
||||
public/post-index.json
|
||||
8
.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
32
README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Systems Obscure
|
||||
|
||||
> Another software engineer with a blog
|
||||
|
||||

|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
This runs `vite build` using [speedy web compiler](https://swc.rs/) for the
|
||||
transpilation of TSX to JSX. I am using TypeScript so that I can use the
|
||||
[shadcn](https://ui.shadcn.com/) component library however I don't actually care
|
||||
about types in this project and all my components are written as JSX. SWC
|
||||
transpiles very quickly without throwing TS warning and errors.
|
||||
|
||||
I still use Vite's native `esbuild` for HMR transpilation in development.
|
||||
|
||||
## Scripts
|
||||
|
||||
### Generate post index
|
||||
|
||||
```sh
|
||||
npm run build:posts
|
||||
```
|
||||
|
||||
This runs `scripts/generate-post-index.js` which reads all raw Markdown posts in
|
||||
`/posts` and parses the body content and YAML front-matter. It then writes this
|
||||
data to `public/post-index.json` which is read by the React application at
|
||||
runtime.
|
||||
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
28
eslint.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import js from "@eslint/js"
|
||||
import globals from "globals"
|
||||
import reactHooks from "eslint-plugin-react-hooks"
|
||||
import reactRefresh from "eslint-plugin-react-refresh"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
16
index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6757
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6757 @@
|
|||
{
|
||||
"name": "systems-obscure",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "systems-obscure",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@shikijs/colorized-brackets": "^3.6.0",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"i": "^0.3.7",
|
||||
"lucide-react": "^0.509.0",
|
||||
"marked": "^15.0.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.6.0",
|
||||
"sharp": "^0.34.2",
|
||||
"shiki": "^3.6.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
|
||||
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/helper-compilation-targets": "^7.27.1",
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helpers": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.3",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
||||
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
|
||||
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
|
||||
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
||||
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
||||
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse/node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
|
||||
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
|
||||
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
|
||||
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
|
||||
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
|
||||
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz",
|
||||
"integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.13.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
|
||||
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanwhocodes/retry": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
|
||||
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
|
||||
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
|
||||
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
|
||||
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
|
||||
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz",
|
||||
"integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"eventsource": "^3.0.2",
|
||||
"express": "^5.0.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
|
||||
"integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-slot": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
|
||||
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz",
|
||||
"integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.12.tgz",
|
||||
"integrity": "sha512-iExvawdu7n6DidDJRU5pMTdi+Z3DaVPN4UZbAGuTs7nJA8P4RvvkEz+XYI2UJjb/Hh23RrH19DakgZNLdaq9Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-collection": "1.1.6",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.9",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-visually-hidden": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
|
||||
"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
|
||||
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz",
|
||||
"integrity": "sha512-hrpa59m3zDnsa35LrTOH5s/a3iGv/VD+KKQjjiCTo/W4r0XwPpiWQvAv6Xl1nupSoaZeNNxW6sJH9ZydsjKdYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz",
|
||||
"integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
|
||||
"integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
|
||||
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
|
||||
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
|
||||
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
|
||||
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
|
||||
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
|
||||
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
|
||||
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
|
||||
"integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/colorized-brackets": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/colorized-brackets/-/colorized-brackets-3.6.0.tgz",
|
||||
"integrity": "sha512-vYAj5trQvNXrxp5gmvBX3SMQj5vKC9etfGZc01hjxQbgldKjmvXGoA/no1U5tuaqpjQGx0TBPyNdASg6mfvVFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shiki": "3.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.6.0.tgz",
|
||||
"integrity": "sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.6.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.6.0.tgz",
|
||||
"integrity": "sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.6.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz",
|
||||
"integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.6.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz",
|
||||
"integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz",
|
||||
"integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz",
|
||||
"integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/vscode-textmate": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.7.tgz",
|
||||
"integrity": "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.23"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.12.7",
|
||||
"@swc/core-darwin-x64": "1.12.7",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.12.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.7",
|
||||
"@swc/core-linux-arm64-musl": "1.12.7",
|
||||
"@swc/core-linux-x64-gnu": "1.12.7",
|
||||
"@swc/core-linux-x64-musl": "1.12.7",
|
||||
"@swc/core-win32-arm64-msvc": "1.12.7",
|
||||
"@swc/core-win32-ia32-msvc": "1.12.7",
|
||||
"@swc/core-win32-x64-msvc": "1.12.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.7.tgz",
|
||||
"integrity": "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.7.tgz",
|
||||
"integrity": "sha512-jN6LhFfGOpm4DY2mXPgwH4aa9GLOwublwMVFFZ/bGnHYYCRitLZs9+JWBbyWs7MyGcA246Ew+EREx36KVEAxjA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.7.tgz",
|
||||
"integrity": "sha512-rHn8XXi7G2StEtZRAeJ6c7nhJPDnqsHXmeNrAaYwk8Tvpa6ZYG2nT9E1OQNXj1/dfbSFTjdiA8M8ZvGYBlpBoA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.7.tgz",
|
||||
"integrity": "sha512-jxyINtBezpxd3eIUDiDXv7UQ87YWlPsM9KumOwJk09FkFSO4oYxV2RT+Wu+Nt5tVWue4N0MdXT/p7SQsDEk4YA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.7.tgz",
|
||||
"integrity": "sha512-zy7JWfQtQItgMfUjSbbcS3DZqQUn2d9VuV0LSGpJxtTXwgzhRpF1S2Sj7cU9hGpbM27Y8RJ4DeFb3qbAufjbrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.7.tgz",
|
||||
"integrity": "sha512-52PeF0tyX04ZFD8nibNhy/GjMFOZWTEWPmIB3wpD1vIJ1po+smtBnEdRRll5WIXITKoiND8AeHlBNBPqcsdcwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.7.tgz",
|
||||
"integrity": "sha512-WzQwkNMuhB1qQShT9uUgz/mX2j7NIEPExEtzvGsBT7TlZ9j1kGZ8NJcZH/fwOFcSJL4W7DnkL7nAhx6DBlSPaA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.7.tgz",
|
||||
"integrity": "sha512-R52ivBi2lgjl+Bd3XCPum0YfgbZq/W1AUExITysddP9ErsNSwnreYyNB3exEijiazWGcqHEas2ChiuMOP7NYrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz",
|
||||
"integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz",
|
||||
"integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"enhanced-resolve": "^5.18.1",
|
||||
"jiti": "^2.4.2",
|
||||
"lightningcss": "1.29.2",
|
||||
"magic-string": "^0.30.17",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz",
|
||||
"integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.6",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.6",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.6",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.6",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.6",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.6",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.6",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.6",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.6",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.6",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz",
|
||||
"integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz",
|
||||
"integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz",
|
||||
"integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz",
|
||||
"integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz",
|
||||
"integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz",
|
||||
"integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz",
|
||||
"integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz",
|
||||
"integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz",
|
||||
"integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz",
|
||||
"integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@emnapi/wasi-threads": "^1.0.2",
|
||||
"@napi-rs/wasm-runtime": "^0.2.9",
|
||||
"@tybys/wasm-util": "^0.9.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz",
|
||||
"integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz",
|
||||
"integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz",
|
||||
"integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.6",
|
||||
"@tailwindcss/oxide": "4.1.6",
|
||||
"tailwindcss": "4.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
"@types/babel__generator": "*",
|
||||
"@types/babel__template": "*",
|
||||
"@types/babel__traverse": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__template": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.1.0",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
|
||||
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
|
||||
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz",
|
||||
"integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz",
|
||||
"integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz",
|
||||
"integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.32.0",
|
||||
"@typescript-eslint/type-utils": "8.32.0",
|
||||
"@typescript-eslint/utils": "8.32.0",
|
||||
"@typescript-eslint/visitor-keys": "8.32.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz",
|
||||
"integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.32.0",
|
||||
"@typescript-eslint/types": "8.32.0",
|
||||
"@typescript-eslint/typescript-estree": "8.32.0",
|
||||
"@typescript-eslint/visitor-keys": "8.32.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz",
|
||||
"integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.0",
|
||||
"@typescript-eslint/visitor-keys": "8.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz",
|
||||
"integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.32.0",
|
||||
"@typescript-eslint/utils": "8.32.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz",
|
||||
"integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz",
|
||||
"integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.0",
|
||||
"@typescript-eslint/visitor-keys": "8.32.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz",
|
||||
"integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.32.0",
|
||||
"@typescript-eslint/types": "8.32.0",
|
||||
"@typescript-eslint/typescript-estree": "8.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz",
|
||||
"integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
|
||||
"integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.10",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.25.9",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.25.9",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-swc": {
|
||||
"version": "3.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
|
||||
"integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.11",
|
||||
"@swc/core": "^1.11.31"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001717",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
|
||||
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-html4": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.151",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz",
|
||||
"integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.4",
|
||||
"@esbuild/android-arm": "0.25.4",
|
||||
"@esbuild/android-arm64": "0.25.4",
|
||||
"@esbuild/android-x64": "0.25.4",
|
||||
"@esbuild/darwin-arm64": "0.25.4",
|
||||
"@esbuild/darwin-x64": "0.25.4",
|
||||
"@esbuild/freebsd-arm64": "0.25.4",
|
||||
"@esbuild/freebsd-x64": "0.25.4",
|
||||
"@esbuild/linux-arm": "0.25.4",
|
||||
"@esbuild/linux-arm64": "0.25.4",
|
||||
"@esbuild/linux-ia32": "0.25.4",
|
||||
"@esbuild/linux-loong64": "0.25.4",
|
||||
"@esbuild/linux-mips64el": "0.25.4",
|
||||
"@esbuild/linux-ppc64": "0.25.4",
|
||||
"@esbuild/linux-riscv64": "0.25.4",
|
||||
"@esbuild/linux-s390x": "0.25.4",
|
||||
"@esbuild/linux-x64": "0.25.4",
|
||||
"@esbuild/netbsd-arm64": "0.25.4",
|
||||
"@esbuild/netbsd-x64": "0.25.4",
|
||||
"@esbuild/openbsd-arm64": "0.25.4",
|
||||
"@esbuild/openbsd-x64": "0.25.4",
|
||||
"@esbuild/sunos-x64": "0.25.4",
|
||||
"@esbuild/win32-arm64": "0.25.4",
|
||||
"@esbuild/win32-ia32": "0.25.4",
|
||||
"@esbuild/win32-x64": "0.25.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.26.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz",
|
||||
"integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.20.0",
|
||||
"@eslint/config-helpers": "^0.2.1",
|
||||
"@eslint/core": "^0.13.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@eslint/plugin-kit": "^0.2.8",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.3.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"esquery": "^1.5.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^8.0.0",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"ignore": "^5.2.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jiti": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"jiti": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
|
||||
"integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-refresh": {
|
||||
"version": "0.4.20",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
|
||||
"integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.40"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
|
||||
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.11 || 5 || ^5.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extendable": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
|
||||
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1",
|
||||
"kind-of": "^6.0.2",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"zwitch": "^2.0.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-whitespace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/i": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
|
||||
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "~0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.29.2",
|
||||
"lightningcss-darwin-x64": "1.29.2",
|
||||
"lightningcss-freebsd-x64": "1.29.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.29.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.29.2",
|
||||
"lightningcss-linux-arm64-musl": "1.29.2",
|
||||
"lightningcss-linux-x64-gnu": "1.29.2",
|
||||
"lightningcss-linux-x64-musl": "1.29.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.29.2",
|
||||
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
|
||||
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
|
||||
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
|
||||
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
|
||||
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
|
||||
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
|
||||
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
|
||||
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
|
||||
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
|
||||
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
|
||||
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.509.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.509.0.tgz",
|
||||
"integrity": "sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-character": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||||
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-sanitize-uri": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||||
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-symbol": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||||
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||||
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
||||
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "dist/cjs/src/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
|
||||
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
|
||||
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"oniguruma-parser": "^0.12.1",
|
||||
"regex": "^6.0.1",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"levn": "^0.4.1",
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "^0.4.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
|
||||
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
|
||||
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-recursion": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||||
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-utilities": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||||
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
||||
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.40.2",
|
||||
"@rollup/rollup-android-arm64": "4.40.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.40.2",
|
||||
"@rollup/rollup-darwin-x64": "4.40.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.40.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.40.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.40.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.40.2",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.40.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.40.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.40.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.40.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.40.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.40.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"kind-of": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.5",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
|
||||
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.4",
|
||||
"semver": "^7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.2",
|
||||
"@img/sharp-darwin-x64": "0.34.2",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||
"@img/sharp-linux-arm": "0.34.2",
|
||||
"@img/sharp-linux-arm64": "0.34.2",
|
||||
"@img/sharp-linux-s390x": "0.34.2",
|
||||
"@img/sharp-linux-x64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.2",
|
||||
"@img/sharp-wasm32": "0.34.2",
|
||||
"@img/sharp-win32-arm64": "0.34.2",
|
||||
"@img/sharp-win32-ia32": "0.34.2",
|
||||
"@img/sharp-win32-x64": "0.34.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.6.0.tgz",
|
||||
"integrity": "sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "3.6.0",
|
||||
"@shikijs/engine-javascript": "3.6.0",
|
||||
"@shikijs/engine-oniguruma": "3.6.0",
|
||||
"@shikijs/langs": "3.6.0",
|
||||
"@shikijs/themes": "3.6.0",
|
||||
"@shikijs/types": "3.6.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities-html4": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz",
|
||||
"integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
|
||||
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.0.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tw-animate-css": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.9.tgz",
|
||||
"integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Wombosvideo"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.32.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz",
|
||||
"integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||
"@typescript-eslint/parser": "8.32.0",
|
||||
"@typescript-eslint/utils": "8.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unist-util-is": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
||||
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
|
||||
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit-parents": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
|
||||
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-message": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
|
||||
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
|
||||
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.5",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
|
||||
"integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "systems-obscure",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:posts": "node scripts/generate-post-index.js",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@shikijs/colorized-brackets": "^3.6.0",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"i": "^0.3.7",
|
||||
"lucide-react": "^0.509.0",
|
||||
"marked": "^15.0.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.6.0",
|
||||
"sharp": "^0.34.2",
|
||||
"shiki": "^3.6.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
11
posts/a-human-being-is.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "A human being is..."
|
||||
slug: /a-human-being-is/
|
||||
date: 2024-08-18
|
||||
tags: ["quotes"]
|
||||
---
|
||||
|
||||
> A human being is a packet of propagating information that dissipates at death.
|
||||
|
||||
I can't remember who said this or where it comes from but I just found it in an
|
||||
old notebook.
|
||||
194
posts/activity-log-backend.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
---
|
||||
title: "Activity Log backend"
|
||||
slug: /activity-log-backend/
|
||||
date: 2024-07-19
|
||||
tags: ["log", "aws", "typescript", "python", "bash"]
|
||||
---
|
||||
|
||||
I have just added a new feature to the site:
|
||||
[Activity Log](https://systemsobscure.blog/activity-log/), which presents my
|
||||
personal time tracking entries. This is how I configured the backend.
|
||||
|
||||

|
||||
|
||||
On my local machine I use [TimeWarrior](https://timewarrior.net/docs/what/) to
|
||||
record the time I spend on different activities. It runs from the terminal and
|
||||
can be exported to JSON.
|
||||
|
||||
To access the data remotely, I needed to store it in a database and be able to
|
||||
regularly export the time entries from my machine to the remote DB.
|
||||
|
||||
I created a simple AWS Lambda with a few endpoints:
|
||||
|
||||
- A POST that receives data and adds it to the database.
|
||||
- A GET at `/count` that returns a count of the entries for each day for the
|
||||
last twelve months
|
||||
- A GET at `/month` that returns the entries for the last month
|
||||
- A GET at `/day` that returns the entries for the specified single date
|
||||
|
||||
The idea is that the frontend will first present all the entries for the current
|
||||
month in a table. Along with this there will be histogram similar to GitHub's
|
||||
commit graph that will present a colour coded representation of the amount of
|
||||
activity for each day in the last year. When the user clicks on a day, a request
|
||||
is made for the entries for that day. That way I don't have to load all the
|
||||
entries into memory at once.
|
||||
|
||||
In development, I used MySQL but it turns out that production SQL databases are
|
||||
prohibitively expensive on AWS. This figures as MySQL requires a permanently
|
||||
running server, contravening the serverless architecture that makes AWS
|
||||
otherwise affordable on the Free Tier.
|
||||
|
||||
After some research I decided to use DynamoDB. This didn't immediately occur to
|
||||
me because, in my ignorance, I assumed a key-value database only permits one key
|
||||
to one value and my table contains multiple fields for each row. In fact, after
|
||||
specifying a unique _primary key_, you can associate it with any number of
|
||||
values (known as "attributes"), thus creating a "table-like" data structure.
|
||||
|
||||
The nomenclature for DynamoDB is quite confusing but I was able to create a
|
||||
simple table-like structure with the following schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"TableName": "TimeEntries",
|
||||
"AttributeDefinitions": [
|
||||
{ "AttributeName": "activity_start_end", "AttributeType": "S" },
|
||||
{ "AttributeName": "year", "AttributeType": "S" }
|
||||
],
|
||||
"KeySchema": [{ "AttributeName": "activity_start_end", "KeyType": "HASH" }],
|
||||
"GlobalSecondaryIndexes": [
|
||||
{
|
||||
"IndexName": "YearIndex",
|
||||
"KeySchema": [
|
||||
{ "AttributeName": "year", "KeyType": "HASH" },
|
||||
{ "AttributeName": "start", "KeyType": "RANGE" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"ProvisionedThroughput": {
|
||||
"ReadCapacityUnits": 1,
|
||||
"WriteCapacityUnits": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The attribute `activity_start_end` is my primary key which is basically
|
||||
equivalent to the primary key in SQL: the value I use to uniquely individuate
|
||||
each entry in the table. It is a simple concatenation of the fields
|
||||
`activity_type`, `start`, `end`. The latter two are ISO timestamps, ensuring
|
||||
uniqueness.
|
||||
|
||||
A _global secondary index_ (GSI) is an attribute in addition to the primary key
|
||||
that you may use to group entries. By suppling a GSI with your query you can
|
||||
reduce the compute required for lookups. By using `year` as a GSI I ensure that
|
||||
the query will only run against values matching the specified year.
|
||||
|
||||
When working locally, I used Docker to create a DynamoDB image. As the AWS SAM
|
||||
software (used for running the Lambda and API Gateway locally) also uses a
|
||||
Docker container (that you can't inspect or modify) it was necessary to specify
|
||||
a bridging network in the `docker-compose.yml` so that each container could
|
||||
communicate:
|
||||
|
||||
```yml
|
||||
services:
|
||||
dev:
|
||||
image: amazon/dynamodb-local
|
||||
container_name: timetracking_dynamodb_dev
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "/home/thomas/repos/lambdas/node-js/time-tracking/data/dev:/home/dynamodblocal/data/dev"
|
||||
command:
|
||||
"-jar DynamoDBLocal.jar -dbPath /home/dynamodblocal/data/dev --sharedDb"
|
||||
networks:
|
||||
- sam-local
|
||||
|
||||
networks:
|
||||
sam-local:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
With the server running I could then view the database using Amazon's _NoSQL
|
||||
Workbench_ client:
|
||||
|
||||

|
||||
|
||||
Creating the Lambda with TypeScript was simple enough using the AWS SDK. Here is
|
||||
the function that I use to return the entries for the month and specific dates:
|
||||
|
||||
```ts
|
||||
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
|
||||
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"
|
||||
import { generateDates, TPeriod } from "./generateDates"
|
||||
|
||||
interface ITimeEntry {
|
||||
activity_start_end: string
|
||||
year: string
|
||||
start: string
|
||||
end: string
|
||||
activity_type: string
|
||||
duration: number
|
||||
description: string
|
||||
}
|
||||
|
||||
const getTimeEntries = async (
|
||||
client: DynamoDBClient,
|
||||
timePeriod: TPeriod
|
||||
): Promise<ITimeEntry[]> => {
|
||||
const documentClient = DynamoDBDocumentClient.from(client)
|
||||
const dateParams = generateDates()[timePeriod]
|
||||
|
||||
const params = {
|
||||
TableName: "TimeEntries",
|
||||
IndexName: "YearIndex",
|
||||
KeyConditionExpression:
|
||||
"#yr = :year AND #start BETWEEN :start_date AND :end_date",
|
||||
ExpressionAttributeNames: {
|
||||
"#yr": "year",
|
||||
"#start": "start",
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":year": dateParams.year,
|
||||
":start_date": dateParams.start,
|
||||
":end_date": dateParams.end,
|
||||
},
|
||||
}
|
||||
|
||||
const command = new QueryCommand(params)
|
||||
const response = await documentClient.send(command)
|
||||
return (response?.Items as ITimeEntry[]) || []
|
||||
}
|
||||
|
||||
export { getTimeEntries, ITimeEntry }
|
||||
```
|
||||
|
||||
(The `DynamoDBDocumentClient` proved essential. It ensures that each row comes
|
||||
back as an array of objects matching the shape of `ITimeEntry`. Without this,
|
||||
the data returns in a nested format specific to DynamoDB that is cumbersome to
|
||||
work with.)
|
||||
|
||||
Outside of the Lambda and setting up the database, it was necessary to write
|
||||
some scripts to glue the different parts together:
|
||||
|
||||
- a Python
|
||||
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/export_timewarrior_entries.py)
|
||||
that exports the entries from TimeWarrior, parsing them into the format the
|
||||
database expects
|
||||
- a Python
|
||||
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/upload_daily_entries.py)
|
||||
(running on a cron timer) that runs the preceding export script every hour and
|
||||
uploads to the remote DB using the Lambda's POST endpoint, sending a
|
||||
notification to my
|
||||
[Slack channel](https://systemsobscure.blog/slack-notification-center/)
|
||||
- a Bash
|
||||
[ script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/migrate.sh)
|
||||
to migrate the contents of the production database to my local Docker instance
|
||||
(useful when working on the frontend locally)
|
||||
- a Bash
|
||||
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/seed.sh)
|
||||
to seed the production and local databases from a CSV
|
||||
|
||||
That's basically it. The biggest pain point was getting my DynamoDB Docker
|
||||
instance to communicate with the default SAM instance. That, and converting my
|
||||
SQL version into DynamoDB. I wouldn't say I find DynamoDB particularly
|
||||
straightforward or fun to use but I'll put up with it because it costs like
|
||||
£00.02 per month compared to the £54.00 they were asking for MySQL.
|
||||
42
posts/alien-blood-vscode-theme.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: "Alien Blood VS Code theme"
|
||||
slug: /alien-blood-vscode-theme/
|
||||
date: 2022-01-01
|
||||
tags: ["project", "productivity"]
|
||||
---
|
||||
|
||||
I'm endlessly toggling through colour themes in VS Code, combining them with
|
||||
different fonts and icons to approximate a Platonic ideal of the optimal working
|
||||
environment. I am never satisfied. There is always some aspect that displeases
|
||||
me.
|
||||
|
||||

|
||||
|
||||
Over the last month or so I decided to resolve the issue once and for all by
|
||||
creating my own custom theme.
|
||||
|
||||
I have always really enjoyed the Alien Blood colours that are one of the default
|
||||
themes of the iTerm2 terminal emulator for Mac. I decided I would use this as my
|
||||
base and try to construct a full syntax and UI scheme from the terminal
|
||||
pallette. This wasn't easy because the colour palettes of terminal themes are
|
||||
more austere than those designed for syntax-highlighting. It took lots of
|
||||
tweaking to try and make the resulting theme visually harmonious but not so
|
||||
homogenous that you cannot distinguish the syntax at glance.
|
||||
|
||||

|
||||
|
||||
I was able to achieve this without introducing any additional colours to the
|
||||
default Alien Blood palette. The theme can be demoed in the browser
|
||||
[here](https://vscode.dev/theme/ThomasBishop.alien-blood/Alien%20Blood%20) and
|
||||
downloaded from the
|
||||
[extension marketplace](https://marketplace.visualstudio.com/items?itemName=ThomasBishop.alien-blood).
|
||||
You can also view the theme on
|
||||
[GitHub](https://github.com/thomasabishop/alien-blood-vscode).
|
||||
|
||||
I'm particularly happy with the rendering of TypeScript and Angular components
|
||||
which is what I am editing the majority of the time at work, similarly with HTML
|
||||
and CSS. However I think there is room for improvement when it comes to plain
|
||||
JavaScript and other filetypes such as shell scripts. In these cases the
|
||||
highlighting is too homogenous and dark but now I have the basic palette
|
||||
completed I can add additional rules for specific programming languages in
|
||||
future releases.
|
||||
195
posts/backing-up-primary-machine.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
---
|
||||
title: "How I backup my primary machine"
|
||||
slug: /backing-up-primary-machine/
|
||||
date: 2023-01-08
|
||||
tags: ["log", "linux"]
|
||||
---
|
||||
|
||||
I use the `rsnapshots` package to run sequenced backups of my ThinkPad T15
|
||||
running Arch Linux.
|
||||
|
||||
`rsnapshots` is based on the `rsync` utility and makes it easy to maintain
|
||||
backups over several timeframes. I run hourly, daily, weekly, and monthly
|
||||
backups, sequenced using `systemd` timers.
|
||||
|
||||
## Preparing the external disk
|
||||
|
||||
I store my backups on an external 500GB SSD.
|
||||
|
||||
First I partition the disk:
|
||||
|
||||
```
|
||||
fdisk /dev/sda
|
||||
d # delete the existing partitions
|
||||
n # start a new partition
|
||||
+500GB # specify size
|
||||
w # write the partition to the disk
|
||||
```
|
||||
|
||||
I now have a 500GB partition at `/dev/sda/sda1`
|
||||
|
||||
Next I create the file system:
|
||||
|
||||
```
|
||||
mkfs -t ext4 /dev/sda1
|
||||
```
|
||||
|
||||
And I label my disk so that I it has a readable name rather than the default
|
||||
GUID that Linux will apply.
|
||||
|
||||
```
|
||||
e2label /dev/sda1 archbish_backups
|
||||
```
|
||||
|
||||
At this point you would create or specify a mount directory and mount the
|
||||
partition with `mount`, also adding it to your `fstab` file to ensure that the
|
||||
disk mounts to the same location in future.
|
||||
|
||||
I haven't chosen to do this because I use the KDE Plasma desktop environment and
|
||||
it automatically mounts any connected drives to `/run/media`. However in order
|
||||
to check the partition label and ensure the above processes have been successful
|
||||
I will disconnect and reconnect the device.
|
||||
|
||||
Now when I run `lsblk` to list the block devices on the machine, I see my new
|
||||
disk and partition:
|
||||
|
||||
```
|
||||
lsblk
|
||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
||||
sda 8:0 0 465.8G 0 disk
|
||||
└─sda1 8:1 0 465.8G 0 part /run/media/thomas/archbish_backups
|
||||
nvme0n1 259:0 0 476.9G 0 disk
|
||||
├─nvme0n1p1 259:1 0 512M 0 part
|
||||
├─nvme0n1p2 259:2 0 11.2G 0 part [SWAP]
|
||||
└─nvme0n1p3 259:3 0 465.3G 0 part /
|
||||
```
|
||||
|
||||
Typically you won't have access to the partition or mount directory yet. You can
|
||||
run the following against your username to ensure access:
|
||||
|
||||
```
|
||||
sudo chown -R <username>:users /run/media/<username>/
|
||||
```
|
||||
|
||||
## Configure snapshot backups with rsnapshot
|
||||
|
||||
I install `rsnapshot` and `rsync` as a dependency:
|
||||
|
||||
```
|
||||
pacman -Sy rsync rsnapshot
|
||||
```
|
||||
|
||||
`rsnapshot` is setup entirely through its config file: `/etc/rsnapshot.conf`.
|
||||
When you install the package the default config file contains lots of
|
||||
instructions and it's mostly a case of removing the comments for the
|
||||
functionality you require. The
|
||||
[Arch wiki](https://wiki.archlinux.org/title/rsnapshot) provides an exhaustive
|
||||
account but the key parts are as follows:
|
||||
|
||||
```
|
||||
# Set the snapshot root directory (the external HDD you plan to use)
|
||||
|
||||
snapshot_root /run/media/thomas/archbish_backups
|
||||
|
||||
# Set the backup intervals
|
||||
|
||||
retain hourly 24
|
||||
retain daily 7
|
||||
retain weekly 4
|
||||
retain monthly 12
|
||||
|
||||
# Name the dir you want to snapshot and what it should be called on the external disk
|
||||
|
||||
backup /home/ localhost
|
||||
```
|
||||
|
||||
So, obviously I am taking hourly, daily, weekly and monthly snapshots. In total,
|
||||
this gives me a years worth of retention whilst minimising the space taken by
|
||||
the older backups. (If I wanted to keep a record spanning several years, I could
|
||||
just make a copy the latest monthly backup in a year's time.)
|
||||
|
||||
The numbers next to the names indicate the retention period. After 24 hours, the
|
||||
oldest hourly backup becomes the daily backup; after seven days the oldest daily
|
||||
backup becomes the weekly backup; after four weeks the oldest weekly backup
|
||||
becomes the monthly backup and so on.
|
||||
|
||||
Now we need to automate the execution of `rsnapshot` at the times designated in
|
||||
the configuration file. We'll do this with a `systemd` service file and several
|
||||
timers. Each file created will be located at `/etc/systemd/system/`
|
||||
|
||||
First we create the service file. This will be a oneshot service that is run by
|
||||
the timers:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=rsnapshot (%I) backup
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Nice=19
|
||||
IOSchedulingClass=idle
|
||||
ExecStart=/usr/bin/rsnapshot %I
|
||||
```
|
||||
|
||||
Then we have to create a timer file for each of the intervals: hourly, daily,
|
||||
weekly, and monthly. Here's the hourly and monthly files to give an idea:
|
||||
|
||||
```
|
||||
# /etc/systemd/system/rsnapshot-hourly.timer
|
||||
[Unit]
|
||||
Description=rsnapshot hourly backup
|
||||
|
||||
[Timer]
|
||||
# Run every hour at 15mins past the hour
|
||||
OnCalendar=*:15
|
||||
Persistent=true
|
||||
Unit=rsnapshot@hourly.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
```
|
||||
# /etc/systemd/system/rsnapshot-monthly.timer
|
||||
[Unit]Description=rsnapshot monthly backup
|
||||
|
||||
[Timer]
|
||||
# Run once per month at 3:30 local time
|
||||
OnCalendar=*-*-1 03:30:00
|
||||
Persistent=true
|
||||
Unit=rsnapshot@monthly.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
Let's check one of our timers:
|
||||
|
||||
```
|
||||
systemd-analyze calendar "*:15"
|
||||
|
||||
Original form: *:15
|
||||
Normalized form: *-*-* *:15:00
|
||||
Next elapse: Sun 2023-01-08 15:15:00 GMT
|
||||
(in UTC): Sun 2023-01-08 15:15:00 UTC
|
||||
From now: 27min left
|
||||
```
|
||||
|
||||
Nice. Now we need to enable and start the timers. For each timer run:
|
||||
|
||||
```
|
||||
systemctl enable rsnapshot-[interval].timer
|
||||
systemctl start rsnapshot-[interval].timer
|
||||
```
|
||||
|
||||
Oh it's 15:21, let's check the first hourly snapshot was taken:
|
||||
|
||||
```
|
||||
journalctl -u rsnapshot@hourly
|
||||
Jan 08 15:15:04 archbish systemd[1]: Starting rsnapshot (hourly) backup...
|
||||
```
|
||||
|
||||
Great. After a few hours we can see the different snapshots mounting up in the
|
||||
backup directory:
|
||||
|
||||

|
||||
13
posts/beckett-quote.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: "Beckett quote"
|
||||
slug: /beckett-quote/
|
||||
date: 2023-10-19
|
||||
tags: ["quotes", "reading"]
|
||||
---
|
||||
|
||||
> For what is this shadow of the going in which we come, this shadow of the
|
||||
> coming in which we go, this shadow of the coming and the going in which we
|
||||
> wait, if not the shadow of purpose, of the purpose that budding withers, that
|
||||
> withering buds, whose blooming is a budding withering.
|
||||
|
||||
Samuel Beckett, _Watt_ (1953)
|
||||
96
posts/bletchley-park-tnmoc-holiday.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
title: "Visit to Bletchley Park and The National Museum of Computing"
|
||||
slug: /bletchley-park-tnmoc-holiday/
|
||||
date: 2024-08-07
|
||||
tags: ["personal", "beige", "gruvbox"]
|
||||
---
|
||||
|
||||
As part of our holiday this year my girlfriend and I went to visit
|
||||
[Bletchley Park](https://bletchleypark.org.uk/) and
|
||||
[The National Museum of Computing](https://www.tnmoc.org/).
|
||||
|
||||
With our customary taste for luxury, we stayed in one of the three Premier Inns
|
||||
in Milton Keynes. Milton Keynes is strange: a car park in search of a town.
|
||||
However, we had the good fortune to overlook one of its supermalls. This is
|
||||
notable only because it _looks exactly like a sandworm in Dune_:
|
||||
|
||||

|
||||
|
||||
Bletchley was superb. A model of how to run a heritage project and museum. We
|
||||
spent about five hours on site stopping for lunch and an excellent cup of tea.
|
||||
|
||||
As you work your way through the huts you pass through each region of what was a
|
||||
global signals intelligence factory: collection, decryption, evaluation, and
|
||||
finally dissemination.
|
||||
|
||||
The huts concerned with decryption were naturally the most compelling. There was
|
||||
life size replica of the Bombe computer used to derive the daily settings of the
|
||||
Enigma machine. It had spinning and clicking rotars however this was a
|
||||
simulation rather than a working reconstruction.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Probably the biggest highlight for me was standing at "the birthplace of the
|
||||
modern computer": the hut where the Colossus computer was used to decipher the
|
||||
Lorenz messages of the German high command.
|
||||
|
||||

|
||||
|
||||
Although not a modern computer in the sense of being general-purpose (it could
|
||||
only be used for breaking this type of cipher and and was not programmable), it
|
||||
was the first to use vacuum-tubes for logic operations, rather than
|
||||
electro-mechanical switches and relays. This made it fully electronic and
|
||||
therefore much quicker and with greater combinatorial range.
|
||||
|
||||

|
||||
|
||||
This was the insight of
|
||||
[Tommy Flowers](https://en.wikipedia.org/wiki/Tommy_Flowers) (a working class
|
||||
hero if ever there was), who designed and built it. He proposed using
|
||||
vacuum-tubes from his experience with telephony at the Post Office Research
|
||||
Station. This was met with scepticism and at one point he resorted to using his
|
||||
own money (never properly remunerated) to build it. He was vindicated. Not only
|
||||
did the machine prove critical in the final stages of the War (confirming the
|
||||
Nazis had bought the D-Day deception), it proved the speed and viability of
|
||||
purely-electronic components that would ultimately lead to the transistor and
|
||||
integrated circuit in later decades.
|
||||
|
||||

|
||||
|
||||
The next day we went to the National Museum of Computing which is unaffiliated
|
||||
with Bletchley but located on the same site.
|
||||
|
||||

|
||||
|
||||
This was a different experience. Certainly less polished and perhaps a bit
|
||||
forbidding for those not already well versed in computer lore.
|
||||
|
||||
This said, it had it's own scruffy charm and is clearly a labour of love. During
|
||||
our visit there were OG volunteer computer engineers actively working on the
|
||||
reconstructions.
|
||||
|
||||
It was a complete cornicopia of retro computers and we had a high time
|
||||
marvelling at the sheer amount of beige and retro-futurist design.
|
||||
|
||||
Here are some of my highlights...
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
110
posts/converting-pixel-phone-to-dap.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
title: "Converting a Pixel 2 XL into a private DAP"
|
||||
slug: /converting-pixel-phone-to-dap/
|
||||
date: 2024-12-15
|
||||
tags: ["projects"]
|
||||
---
|
||||
|
||||
I recently read
|
||||
[_The Age of Surveillance Capitalism_](https://en.wikipedia.org/wiki/The_Age_of_Surveillance_Capitalism)
|
||||
. This book and initiatives like the
|
||||
[Opt Out Project](https://www.optoutproject.net) have motivated me to try and
|
||||
improve my digital privacy.
|
||||
|
||||
Some will see this as a fool's errand and I agree to some extent. However it
|
||||
isn't something I expect to complete overnight. I view the journey as just as
|
||||
important as the destination as it provides opportunities to acquire new skills,
|
||||
discover new technologies, and work on projects that have utility in my daily
|
||||
life.
|
||||
|
||||
My first objective was to do away with my Spotify subscription and consume music
|
||||
from a dedicted offline device that plays albums I actually own.
|
||||
|
||||
I researched modern-day MP3 players but this is a pretty dead market these days
|
||||
and all the devices were ugly or surplus to my requirements. There is a
|
||||
community of hard-core audiophiles who have rechristened MP3 players _digital
|
||||
audio players_ (DAPs) but most of the recommended devices are very expensive and
|
||||
my goal is to try and keep the overall anti-surveillance project affordable,
|
||||
recycling and building my own solutions where I can.
|
||||
|
||||
It occurred to me that I could just repurpose an old phone into a single-purpose
|
||||
computer for playing music. Appropriately enough, I have an old Google Pixel 2
|
||||
XL from 2017 - the surveillance capitalist device _par excellence_.
|
||||
|
||||
It is able to play FLAC files natively and its 128GB harddrive should be more
|
||||
than sufficient for my library.
|
||||
|
||||
My first task was to "de-Google" the phone by finding a version of Android that
|
||||
respects privacy and doesn't bundle surveillance-ware. This was trickier that I
|
||||
expected. Not because there isn't such software but because phones have a more
|
||||
specific hardware set with greater variation than laptops of desktop computers.
|
||||
You have to find an OS that is compatible with your hardware and which is still
|
||||
regularly maintained even though the hardware is, in my case, eight years old
|
||||
and long since superseded by more recent devices.
|
||||
|
||||
Luckily [LineageOS](https://lineageos.org) offers a build that works with the
|
||||
Pixel 2 XL and which is fairly lightweight, allowing me to preserve greater disk
|
||||
space for the audio files.
|
||||
|
||||
Having enabled "Developer Mode" on the Pixel, I needed to install a few CLI
|
||||
tools on my Arch Linux machine that would enable me to interface with the
|
||||
device.
|
||||
|
||||
I installed `android-tools` and `android-udev`. `android-tools` includes `adb`
|
||||
which allows me to communicate with an Android device over USB, transfer files,
|
||||
and access the shell from another Linux device. It also includes `fastboot`
|
||||
which unlocks the Pixel bootloader, necessary to install third-party recovery
|
||||
software through which I can flash a new OS ROM to the device. I used
|
||||
`android-udev` to grant myself access to the Pixel from my Linux PC.
|
||||
|
||||

|
||||
|
||||
Having gained access to the device remotely I used `adb` to unlock the
|
||||
bootloader and then transferred over the [TWRP](https://twrp.me/about/) recovery
|
||||
software and the latest build of LineageOS, tagged to Android v.12.
|
||||
|
||||
Then, on the device, I booted into TWRP. From here I wiped the data and
|
||||
installed LineageOS. This took a few attempts to work. It kept booting into the
|
||||
recovery menu for some reason but eventually it just worked.
|
||||
|
||||

|
||||
|
||||
Once the new OS was installed there wasn't much else to do. I deleted the few
|
||||
apps included with LineageOS that I didn't need and installed F-Droid. F-Droid
|
||||
is a de-Googled version of Google's Play Store that serves as a package manager
|
||||
for FOSS Android apps.
|
||||
|
||||

|
||||
|
||||
Using F-Droid, I installed a few music apps to experiment with but ended up
|
||||
finding the default LineageOS player satisfactory. I also installed Duck Duck Go
|
||||
as my browser and KDE Connect. Although I intend to mostly keep the device
|
||||
offline, it's handy to have a browser to source images for the music player.
|
||||
|
||||
KDE Connect allows me to connect to the Pixel from my PC over WiFi. This is
|
||||
necessary for transferring the audio files. It also allows me to control media
|
||||
on the PC from the phone although I doubt I will have much use for this.
|
||||
|
||||
I am still in the process of recreating my Spotify library with albums I own. I
|
||||
have a lot of albums in MP3 on an old external HD from the pre-streaming era. I
|
||||
have also been able to use the Internet Archive as well as buying albums direct
|
||||
from artists on Bandcamp. For very obscure stuff that was originally released on
|
||||
tape and circulated online I am usually able to find torrents, although some
|
||||
stuff is very hard to track down. I'm planning on getting a CD drive so that I
|
||||
can rip CDs I find in second-hand shops and fairs. If I go full
|
||||
"digital-hoarder", I may even get a vinyl-to-digital converter turnable
|
||||
eventually, then I can exploit my friends' LP collections.
|
||||
|
||||

|
||||
|
||||
One annoying thing is that the metadata for the music files will often be
|
||||
missing or incomplete. For instance the tracks on an album might be out of order
|
||||
or lacking the album cover. I use [kid3](https://kid3.kde.org) to view and edit
|
||||
the metadata so that the tracks are recognised properly by the player with
|
||||
release year and album art.
|
||||
|
||||

|
||||
|
||||
My next challenge will be tackling audiobooks and podcasts. Ideally I would like
|
||||
to download them as files to the DAP and avoid streaming services. More on this
|
||||
to follow.
|
||||
21
posts/delillo-alignment.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
title: "DeLillo, alignment"
|
||||
slug: /delillo-alignment/
|
||||
date: 2023-04-16
|
||||
tags: ["quotes", "reading"]
|
||||
---
|
||||
|
||||
I am reading Don DeLillo's _Underworld_ (1997) at the moment. As we worry about
|
||||
the [problem of alignment](https://en.wikipedia.org/wiki/AI_alignment) in the
|
||||
wake of GPT-4 the following passage seemed especially apposite.
|
||||
|
||||
> I was driving a Lexus through a rustling wind. This is a car assembled in a
|
||||
> work area that's completely free of human presence. Not a spot of mortal sweat
|
||||
> except, okay, for the guys who drive the product out of the plant —
|
||||
> allow a little moisture where they grip the wheel. The system flows forever
|
||||
> onward, automated to priestly nuance, every gliding movement back-referenced
|
||||
> for prime performance. Hollow bodies coming in endless sequence. There's
|
||||
> nobody on the line with caffeine nerves or a history of clinical depression.
|
||||
> Just the eerie weave of chromium alloys carried in interlocking arcs, block
|
||||
> iron and asphalt sheeting, soaring ornaments of coachwork fitted and merged.
|
||||
> Robots tightening bolts, programmed drudges that do not dream of family dead.
|
||||
23
posts/gruvbox-image-filters.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: "Gruvbox image filters"
|
||||
slug: /gruvbox-image-filters/
|
||||
date: 2024-11-16
|
||||
tags: ["gruvbox", "beige", "log"]
|
||||
---
|
||||
|
||||
I've been experimenting with an
|
||||
<a href="https://en.wikipedia.org/wiki/ImageMagick">ImageMagick</a> script to
|
||||
create filters for images that I use as my screensaver. I applied a
|
||||
Gruvbox-themed colour swap and manipulated the dithering to create a retro
|
||||
effect with highlights. I was quite surprised how easy and effective this proved
|
||||
to be. Below are some of the ones I like best.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
133
posts/how-I-deploy-this-site.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
title: "How I deploy this site"
|
||||
slug: /how-I-deploy-this-site/
|
||||
date: 2023-01-02
|
||||
tags: ["log", "linux"]
|
||||
---
|
||||
|
||||
I want to make a note of how this blog is maintained and deployed for future
|
||||
reference as the AWS process is quite involved.
|
||||
|
||||
I use AWS because I have to know at least one cloud/serverless provider well and
|
||||
we use AWS at work. I intend to take the AWS Certified Developer exam eventually
|
||||
so knowing how to deploy a frontend application is useful.
|
||||
|
||||
## Frontend: Gatsby.js
|
||||
|
||||
Nothing special here. I am using the React-based [Gatsby]() framework to create
|
||||
the frontend and as you can see, there isn't much to the site. Just a homepage
|
||||
and a bunch of posts. I used the Gatsby starter template, changed the fonts and
|
||||
styled it in the manner of my [AlienBlood]() theme.
|
||||
|
||||
## Deployment: AWS S3, CloudFront, Route 53, Certificate Manager
|
||||
|
||||
- First I uploaded the build directory to a bucket on S3 making sure to set the
|
||||
permissions to public and to specify that the bucket hosts a static website.
|
||||
- Then I created a hosted zone using AWS Route 53. This is necessary for the
|
||||
bucket to be publicly accessible as a domain on the internet. I purchased the
|
||||
domain name from GoDaddy and uploaded the AWS nameservers there. Cue 24 hours
|
||||
propagation time.
|
||||
- All sites should be served over `https` so I requested a public SSL
|
||||
certificate from AWS Certificate Manager.
|
||||
- You can't serve an S3 bucket as `https` by default. The solution is to use
|
||||
CloudFront as a CDN and then specify a redirection rule from `http` to
|
||||
`https`. So you create the CloudFont instance and generate an endpoint. This
|
||||
endpoint is then added to the Route 53 specifications as an A record. That
|
||||
propagates in a matter of seconds and now the site is securely hosted.
|
||||
|
||||
## Middlewear: AWS Lambda
|
||||
|
||||
An annoying thing about CloudFront is that it won't recognise pages other than
|
||||
the root `index.html` on refresh. So if I went to this page
|
||||
(https://systemsobscure.blog/how-I-deploy-this-site/), on refresh it becomes a
|
||||
403 because the file format is not `how-I-deploy_this_site/index.html`.
|
||||
|
||||
To get around this the custom is to use a Lambda function that intercepts
|
||||
requests and add the trailing `index.html`:
|
||||
|
||||
```js
|
||||
exports.handler = (event, context, callback) => {
|
||||
// Extract the request from the CloudFront event that is sent to Lambda@Edge
|
||||
var request = event.Records[0].cf.request
|
||||
|
||||
// Extract the URI from the request
|
||||
var olduri = request.uri
|
||||
|
||||
// Match any '/' that occurs at the end of a URI. Replace it with a default index
|
||||
var newuri = olduri.replace(/\/$/, "/index.html")
|
||||
|
||||
// Replace the received URI with the URI that includes the index page
|
||||
request.uri = newuri
|
||||
|
||||
// Return to CloudFront
|
||||
return callback(null, request)
|
||||
}
|
||||
```
|
||||
|
||||
You then set this function to trigger on page requests to the CloudFront
|
||||
distribution that is handling the routing and the problem is resolved.
|
||||
|
||||
## Continuous deployment: GitHub Actions
|
||||
|
||||
It's slightly onerous to have to manually trigger a deploy to S3 every time I
|
||||
write a new post. A better scenario would be to trigger a build and a deployment
|
||||
to S3 every time I push to the `main` branch on GitHub. I could do this from
|
||||
within AWS but I've chosen to have GitHub communicate with AWS rather than the
|
||||
other way around. That way I get some experience of using Actions.
|
||||
|
||||
My GitHub Action declaration runs the standard `gatsby build` command on push
|
||||
and also runs the following NPM script once the build completes:
|
||||
|
||||
```bash
|
||||
gatsby-plugin-s3 deploy --yes; aws cloudfront create-invalidation --distribution-id <REDACTED> --paths '/*';",
|
||||
```
|
||||
|
||||
This uses a Gatsby plugin to deploy to S3 and clear the Cloudfront cache.
|
||||
|
||||
In order for this command to run from GitHub I had to create a "GitHub" user and
|
||||
custom permissions file in AWS IAM. This gives me an Access Key ID and secret
|
||||
which the GitHub Action can use to authenticate the deployment. I save these as
|
||||
secrets within the repository settings in GitHub and now the whole Action
|
||||
declaration works.
|
||||
|
||||
Here is the GitHub Action in full:
|
||||
|
||||
```yaml
|
||||
name: Deploy systemsobscure.blog
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Caching Gatsby
|
||||
id: gatsby-cache-build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
public
|
||||
.cache
|
||||
node_modules
|
||||
key: ${{ runner.os }}-systemsobscure-site-build-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-systemsobscure-site-build-
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Set AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
- name: Deploy to S3
|
||||
run: npm run deploy
|
||||
```
|
||||
32
posts/image-deletion-bash-script.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "Bash script: delete unused images"
|
||||
slug: /image-deletion-bash-script/
|
||||
date: 2023-04-16
|
||||
tags: ["log", "bash"]
|
||||
---
|
||||
|
||||
When I'm working on my technical notes I often insert images, diagrams,
|
||||
screenshots etc. Often, I end up creating multiple images that I end up not
|
||||
using. I don't like commiting dead bytes to the repo so I wrote a short script
|
||||
that reads each file in the image directory and looks to see if it is referenced
|
||||
in any Markdown files. If it isn't, the image is deleted.
|
||||
|
||||
```bash
|
||||
find /home/thomas/repos/eolas/_img -type f | while read filename; do
|
||||
rg "${filename##*/}" ../ --type markdown >/dev/null 2>&1
|
||||
if [ "$?" -eq 1 ]; then
|
||||
echo "Deleted unused image: ${filename##*/}"
|
||||
rm $filename
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
./clean_image_directory.sh
|
||||
Deleted unused image: multiplication_03.gif
|
||||
Deleted unused image: Pasted_image_20220319174839.png
|
||||
Deleted unused image: multiplication_04.gif
|
||||
Deleted unused image: multiplication_02.gif
|
||||
```
|
||||
BIN
posts/img/BJT_NPN_symbol.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
124
posts/img/BJT_NPN_symbol.svg
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
width="225"
|
||||
height="225"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="BJT_NPN_symbol.svg"
|
||||
inkscape:export-filename="bjt-again.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata19">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-height="1138"
|
||||
inkscape:window-width="1878"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
guidetolerance="10.0"
|
||||
gridtolerance="10.0"
|
||||
objecttolerance="10.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
width="150px"
|
||||
height="150px"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="43.487066"
|
||||
inkscape:cy="60.457628"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:current-layer="svg2"
|
||||
showgrid="true"
|
||||
inkscape:grid-bbox="false"
|
||||
inkscape:grid-points="true"
|
||||
inkscape:guide-bbox="false"
|
||||
inkscape:guide-points="true"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:window-maximized="0" />
|
||||
<defs
|
||||
id="defs4" />
|
||||
<g
|
||||
id="g311"
|
||||
transform="translate(35.99999,37.5)">
|
||||
<path
|
||||
id="path1307"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 89,99 79,96 86,89 Z"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
id="path1309"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 70,80 20,20 v 35"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
id="path1321"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 70,70 90,50 V 15"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
id="path1325"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 70,75 H 15"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
id="path2202"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.44444;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 112.7778,75.000015 c 0,17.550387 -14.227398,31.777785 -31.777785,31.777785 -17.550387,0 -31.777785,-14.227398 -31.777785,-31.777785 0,-17.550387 14.227398,-31.777785 31.777785,-31.777785 17.550387,0 31.777785,14.227398 31.777785,31.777785 z" />
|
||||
<path
|
||||
id="path3078"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 70,55 V 95"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="94.242188"
|
||||
y="34.086914"
|
||||
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3963"
|
||||
xml:space="preserve"><tspan
|
||||
x="94.242188"
|
||||
y="34.086914"
|
||||
id="tspan3965">C</tspan></text>
|
||||
<text
|
||||
x="94.822266"
|
||||
y="128.7627"
|
||||
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3967"
|
||||
xml:space="preserve"><tspan
|
||||
x="94.822266"
|
||||
y="128.7627"
|
||||
id="tspan3969">E</tspan></text>
|
||||
<text
|
||||
x="20"
|
||||
y="70"
|
||||
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3971"
|
||||
xml:space="preserve"><tspan
|
||||
x="20"
|
||||
y="70"
|
||||
id="tspan3973">B</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
160
posts/img/BJT_transistor_symbol.svg
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
width="850"
|
||||
height="1000"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="BJT_transistor_symbol.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata24">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-height="884"
|
||||
inkscape:window-width="1683"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
guidetolerance="10.0"
|
||||
gridtolerance="10.0"
|
||||
objecttolerance="10.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:zoom="1.0578317"
|
||||
inkscape:cx="173.94071"
|
||||
inkscape:cy="221.67987"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:window-maximized="0" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<marker
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
style="overflow:visible"
|
||||
id="TriangleOutL">
|
||||
<path
|
||||
d="M 5.77,0 L -2.88,5 L -2.88,-5 L 5.77,0 z "
|
||||
transform="scale(0.8,0.8)"
|
||||
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
|
||||
id="path4913" />
|
||||
</marker>
|
||||
<marker
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
style="overflow:visible"
|
||||
id="Arrow1Mend">
|
||||
<path
|
||||
d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z "
|
||||
transform="scale(-0.4,-0.4)"
|
||||
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
|
||||
id="path5005" />
|
||||
</marker>
|
||||
<marker
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
style="overflow:visible"
|
||||
id="Arrow1Lend">
|
||||
<path
|
||||
d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z "
|
||||
transform="scale(-0.8,-0.8)"
|
||||
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
|
||||
id="path5011" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="matrix(0.15767037,0,0,0.15767037,36.993876,16.385944)"
|
||||
inkscape:export-filename="layer1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<path
|
||||
d="m 800,500 a 300,300 0 1 1 -600,0 300,300 0 1 1 600,0 z"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1309" />
|
||||
<path
|
||||
d="M 50,500 H 350"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2184" />
|
||||
<path
|
||||
d="M 350,325 V 675"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2186" />
|
||||
<path
|
||||
d="M 350,400 550,250 V 50"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2188" />
|
||||
<g
|
||||
id="g1907">
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 349.71875,594.96875 c -2.11823,0.0989 -3.94375,1.52309 -4.55502,3.55362 -0.61126,2.03053 0.12466,4.2258 1.83627,5.47763 l 100,75 100,75 c 1.42724,1.10231 3.33745,1.35074 4.99917,0.65017 1.66172,-0.70057 2.8175,-2.24161 3.02476,-4.03302 C 555.2312,748.82575 554.45784,747.06151 553,746 L 453,671 353,596 c -0.9374,-0.72093 -2.09997,-1.0863 -3.28125,-1.03125 z"
|
||||
id="path3063" />
|
||||
<path
|
||||
id="path1913"
|
||||
style="fill-rule:evenodd;stroke:#000000;stroke-width:8pt;marker-start:none"
|
||||
d="m 486.928,702.696 -79.36,-9.52 48,-64 z" />
|
||||
</g>
|
||||
<text
|
||||
x="34.285713"
|
||||
y="460"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3938"
|
||||
xml:space="preserve"><tspan
|
||||
x="34.285713"
|
||||
y="460"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
|
||||
id="tspan3940">B</tspan></text>
|
||||
<text
|
||||
x="570"
|
||||
y="120"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3942"
|
||||
xml:space="preserve"><tspan
|
||||
x="570"
|
||||
y="120"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
|
||||
id="tspan3944">C</tspan></text>
|
||||
<text
|
||||
x="570"
|
||||
y="972.57141"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3946"
|
||||
xml:space="preserve"><tspan
|
||||
x="570"
|
||||
y="972.57141"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
|
||||
id="tspan3948">E</tspan></text>
|
||||
<path
|
||||
d="M 550,750 V 950"
|
||||
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
id="path5022" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
BIN
posts/img/BJT_transistor_symbol_two.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
posts/img/Operators_in_front_of_the_MANIAC_gruvbox.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
posts/img/access-api-permission.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
posts/img/activity-log-diagram-darker.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
posts/img/alien_blood_syntax_demo.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
posts/img/alien_blood_terminal_colours.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
posts/img/and-or-not-gates.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
posts/img/api-gateway-pocket-api.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
posts/img/aws-console-cloud-formation-new-entry.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
posts/img/basic-led-circuit.jpeg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
posts/img/basic-led-circuit.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
posts/img/bbc-leave-gruv-small.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
posts/img/bbc-leave-gruv.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
posts/img/bjt-again.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
posts/img/bjt-diagram.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
posts/img/bjt.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
posts/img/bombe-at-bletchley.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
posts/img/certbot_confirm.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
posts/img/chat-gpt-explanation-bug.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
posts/img/circuit-boards.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
posts/img/circuit-simple.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
posts/img/circuit-two-switch.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
posts/img/cloud-formation-cli-confirmation.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
posts/img/colossos-reconstruction.jpg
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
posts/img/colossus-birthplace-bletcley.jpg
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
posts/img/commit-table-workbench.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
50
posts/img/computer.svg
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="284.27203mm"
|
||||
height="284.27203mm"
|
||||
viewBox="0 0 284.27203 284.27203"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs2" /><g
|
||||
id="g4"><path
|
||||
stroke="#848484"
|
||||
d="M 79.951508,4.441751 H 239.85454 M 71.068008,13.325252 h 8.8835 m 151.019532,0 h 8.8835 M 62.184508,22.208754 h 8.8835 m 151.019522,0 h 17.76701 M 53.301008,31.092255 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,39.975756 h 8.8835 m 17.767,0 H 204.32053 m 17.767,0 h 17.76701 M 53.301008,48.859257 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,57.742759 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,66.62626 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.88351 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m 8.8835,0 h 17.767 m -213.204032,8.8835 H 230.97104 m 8.8835,0 h 17.767 m -26.6505,8.8835 h 17.767 m 8.8835,0 h 8.8835 M 35.534008,173.22828 H 239.85454 m 8.8835,0 h 17.767 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 97.718512,0 h 88.83501 m 17.76701,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 186.553522,0 h 53.30101 m -248.738032,8.8835 h 8.8835 m 26.6505,0 h 17.767 m 124.369022,0 h 8.8835 m 35.53401,0 h 8.8835 m 17.767,0 h 8.8835 m -257.621532,8.8835 h 26.6505 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 8.8835,0 h 79.95151 m -257.621542,8.8835 h 8.88351 m 8.8835,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 8.8835,0 h 8.8835 m 62.18451,0 h 17.767 M 8.883498,244.29629 h 8.8835 m 8.88351,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.767 m 17.767012,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.76701 m 79.95151,0 h 26.6505 M 0,253.17979 h 8.883498 m 168.786532,0 h 8.8835 m 53.30101,0 h 26.6505 M 0,262.06329 h 8.883498 m 168.786532,0 h 79.95151 m -26.6505,8.8835 h 8.8835"
|
||||
id="path1051"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /><path
|
||||
stroke="#c6c6c6"
|
||||
d="M 79.951508,13.325252 H 222.08753 M 71.068008,31.092255 H 222.08753 M 71.068008,39.975756 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 H 222.08753 m 35.53401,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.88351 h 8.8835 m -204.320532,17.767 H 239.85454 m -195.437032,8.8835 h 88.835012 m 88.83501,0 h 17.76701 m -195.437032,8.8835 h 17.767 m 17.767,0 h 53.301012 m -88.835012,8.8835 h 17.767 m 17.767,0 H 204.32053 m 8.8835,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m -88.83501,35.53401 h 8.8835 m 35.53401,0 h 8.8835 M 8.883498,262.06329 H 177.67003 m 8.8835,8.8835 h 44.41751"
|
||||
id="path1053"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#a89984;stroke-opacity:1" /><path
|
||||
stroke="#ffffff"
|
||||
d="m 222.08753,13.325252 h 8.88351 M 71.068008,22.208754 H 222.08753 M 62.184508,31.092255 h 8.8835 m -8.8835,8.883501 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 8.8835,0 H 213.20403 m -151.019522,8.8835 h 8.8835 m -35.534,35.53401 H 239.85454 m -204.320532,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m 177.670022,0 h 17.76701 m 17.767,0 h 8.8835 m -204.320532,8.8835 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 26.6505,0 h 62.18451 m -239.854542,8.88351 h 8.88351 m 159.903022,0 h 62.18451 M 8.883498,253.17979 H 177.67003 m 17.767,0 h 35.53401"
|
||||
id="path1055"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#d4be98;stroke-opacity:1" /><path
|
||||
stroke="#000000"
|
||||
d="m 239.85454,13.325252 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.883501 h 8.8835 M 79.951508,48.859257 H 195.43703 m 44.41751,0 h 8.8835 M 79.951508,57.742759 h 8.8835 m 151.019532,0 h 8.8835 M 79.951508,66.62626 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.88351 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m 26.6505,0 h 8.8835 m -222.087532,8.8835 H 230.97104 m 35.534,0 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -142.13602,8.8835 h 88.83501 m -168.786522,17.767 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m 79.95151,0 h 8.8835 m -239.854532,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 88.83501,0 h 8.8835 m -248.738032,8.88351 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 97.71851,0 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 M 8.883498,270.94679 H 177.67003 m 62.18451,0 h 17.767 m -71.06801,8.8835 h 53.30101"
|
||||
id="path1057"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#282828;stroke-opacity:1" /><path
|
||||
stroke="#000084"
|
||||
d="m 195.43703,48.859257 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -124.369022,8.8835 H 204.32053"
|
||||
id="path1059"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#2e3b3b;stroke-opacity:1" /><path
|
||||
stroke="#0000ff"
|
||||
d="M 88.835008,57.742759 H 195.43703 M 88.835008,66.62626 h 8.88351 m 8.883502,0 h 88.83501 m -106.602022,8.8835 h 8.88351 m 8.883502,0 h 88.83501 M 88.835008,84.39326 H 195.43703 M 88.835008,93.27676 H 195.43703 M 88.835008,102.16027 H 195.43703 m -106.602022,8.8835 H 195.43703 m -106.602022,8.8835 H 195.43703"
|
||||
id="path1061"
|
||||
style="fill:none;fill-opacity:1;stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7daea3;stroke-opacity:1" /><path
|
||||
stroke="#01feff"
|
||||
d="m 97.718518,66.62626 h 8.883502 m -8.883502,8.8835 h 8.883502"
|
||||
id="path1063"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#87efcc;stroke-opacity:1" /><path
|
||||
stroke="#008400"
|
||||
d="m 62.184508,208.76228 h 17.767"
|
||||
id="path1065"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#c4de3d;stroke-opacity:1" /><path
|
||||
stroke="#838383"
|
||||
d="m 97.718518,244.29629 h 8.883502 m 71.06801,0 h 8.8835 m -8.8835,26.6505 h 8.8835"
|
||||
id="path1067"
|
||||
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /></g></svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
BIN
posts/img/control-panel_gruvbox.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
posts/img/correcting-chatgpt.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
posts/img/create-iam-user-aws-console.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
posts/img/create-iam-user.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
posts/img/cropped-eolas-fe.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
posts/img/ddb-data-in-builder.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
posts/img/dns-records-detail.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
posts/img/docker-compose-up.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
posts/img/docker-net-ls.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
posts/img/docker-network-inspect.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
posts/img/docker-network-ls.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
posts/img/double-and-circ.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
posts/img/elliott-900.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
posts/img/eolas-static-diag.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
posts/img/file-launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
posts/img/full-note-view.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
posts/img/function-diagram-new.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
posts/img/function-diagram.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
posts/img/gaby-bbc-micro.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
posts/img/gaby-with-pcs-again.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
posts/img/grafana-login-screen.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
posts/img/grafana-server-metrics-dashboard.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
posts/img/grafana-varlogs-loki.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
posts/img/gruvbox95.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
posts/img/gruvbox_calculator.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
posts/img/hardware-simulator-basic-use.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
posts/img/hardware-simulator-three.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
posts/img/hardware-simulator.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
posts/img/hetzner-console.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
posts/img/hetzner-server.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
posts/img/httpie-showing-data.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
posts/img/ibm-pc.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
posts/img/keycaps.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
posts/img/kid3-screenshot.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
posts/img/lambda-diagram-new.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
posts/img/lambda-schematic-diagram.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
posts/img/laurie-burn_gruvbox.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
posts/img/lets-encrypt-certs.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
posts/img/lineage-bootloader.jpeg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
posts/img/lineage-library.jpeg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
posts/img/lineage-recovery.jpeg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
posts/img/lineage.jpeg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
posts/img/meridian-monument.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
posts/img/mk-worm.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
posts/img/nand-fork.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
posts/img/nand-with-and-not.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
posts/img/neuron-eolas-screenshot-small.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
posts/img/new-circuit-photo-again.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
posts/img/new-cloud-formation-application.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
posts/img/newly-created-pocket-lambda.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
posts/img/no-sql-workbench.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
posts/img/not-by-ai-alternative--dark.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
posts/img/not-by-ai-alternative--light.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
posts/img/note-view.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
posts/img/obsidian-backlinks.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
posts/img/obsidian-graph-view.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
posts/img/obsidian-tags.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
posts/img/outside-tmoc.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
posts/img/pdp-decompressed.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
posts/img/pdp-programming-card.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
posts/img/query-pocket-lambda-response-postman.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
posts/img/query-remote-endpoint-with-auth.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
posts/img/radigue_gruvbox.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
posts/img/reverse-proxy.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
posts/img/rnapshot-dolphin-file-viewr.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
posts/img/running-neuron-generator.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
posts/img/save-articles-architecture.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
posts/img/saved_tech_articles.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
posts/img/secrets-manager-aws-console.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
posts/img/secrets-manager-key-pairs.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
posts/img/single-and-gate.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
posts/img/single-switch-and-circuit.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
posts/img/slack-notification-center.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
posts/img/thermionic-valves.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
posts/img/timekeeping-diagram-new.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
posts/img/timer-module.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
posts/img/toolbar-one.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
posts/img/toolbar-three.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
posts/img/toolbar-two.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
4
posts/img/transistor-diag.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
posts/img/tt-two_gruvbox.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
posts/img/turing-notes-bombe.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
posts/img/turing_sculpture.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
posts/img/tux-icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
posts/img/tux.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
posts/img/wakatime-waybar.jpg
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
posts/img/waybar-full.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
posts/img/workbench-create-conn.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
posts/img/workbench-data-modeller.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
posts/img/zk-tags.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
442
posts/informal-concepts-propositional-logic.md
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
---
|
||||
title: "Informal definitions of key concepts in propositional logic"
|
||||
slug: /informal-concepts-propositional-logic/
|
||||
date: 2021-11-27
|
||||
tags: ["article"]
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This post is the first of a series on propositional logic that introduces the
|
||||
foundational logical concepts of validity, soundness, truth, falsity,
|
||||
possibility and indeterminacy. These concepts are defined here informally and
|
||||
will receive formal articulation in later posts.
|
||||
|
||||
## What is propositional logic?
|
||||
|
||||
The chief unit of propositional logic is the _proposition_. It is typically said
|
||||
that the sentences of a language _express_ propositions. Thus, in natural
|
||||
languages, propositions are expressed via declarative sentences: a statement
|
||||
that _such and such is the case_. For example, in English: _snow is white_,
|
||||
_London is the capital of the United Kingdom_, _John is travelling to Stockholm_
|
||||
etcetera. Whilst this is true it is important not to assume that a proposition
|
||||
reduces to its expression by a given sentence. There are numerous examples of
|
||||
why this assumption proves problematic. For example, _il pleut_ and _it is
|
||||
raining_ express the same proposition but do so via different sentences in
|
||||
different languages. Similarly the semantically ambiguous English sentence _Two
|
||||
cars were reported stolen by the police yesterday_ expresses two possible
|
||||
propositions (the police reported the car stolen, the police stole the car) that
|
||||
are left underdetermined by the surface grammar of the sentence. For simplicity
|
||||
we will talk about propositions exclusively in terms of sentences but it is
|
||||
important to note that the two are not straightforwardly interchangeable.
|
||||
|
||||
Not every sentence in a language has a propositional form. Consider for example
|
||||
_Áchtung!_ or _thanks_. It is not obvious that these sentences express a
|
||||
proposition in the manner of declarative sentences. Philosophers and linguists
|
||||
have called expressions that do not satisfy propositional criteria
|
||||
[speech acts](https://en.wikipedia.org/wiki/Speech_act). The difference between
|
||||
declarative sentences and speech acts (like thanking someone or issuing an
|
||||
order) centers on the fact that the former possess clear _truth-conditions_ that
|
||||
reduce to a given _truth-value_. In the case of _il pleut_ the sentence is true
|
||||
if it is raining and false otherwise. Those are its truth-conditions. Its
|
||||
truth-value is the particular assignment that is made on the basis of these
|
||||
conditions. Assume it is not raining; in this case the truth-value of _il pleut_
|
||||
is false.
|
||||
|
||||
Although declarative sentences are a subset of the totality of possible
|
||||
grammatical expressions in any natural language, they are clearly an important
|
||||
subset. They form the basis of all scientific and mathematical discourse and are
|
||||
our primary means of spreaking and reasoning about the world. The scope of
|
||||
propositional logic is limited to sentences that have this declarative property.
|
||||
There are no questions, commands or exhortations in propositional logic, only
|
||||
statements which may be true or false.<sup>1</sup>
|
||||
|
||||
The purpose of propositional logic is to analyse propositions in terms of their
|
||||
truth conditions and to derive rules governing their proper application and
|
||||
combination in arguments. Equipped with these rules, we are able to demonstrate
|
||||
for example that an argument is valid or that it displays sound reasoning. On
|
||||
the other hand, the same rules will allow us to demonstrate when an argument is
|
||||
invalid or that it leads to contradiction.
|
||||
|
||||
## Arguments and consistency
|
||||
|
||||
Propositional logic proceeds upon two interconnected axes:
|
||||
|
||||
<ol>
|
||||
<li>Analysing compound propositions in terms of their constituent parts</li>
|
||||
<li>Analysing a proposition in relation to other propositions</li>
|
||||
</ol>
|
||||
|
||||
In this post we are focused on the second axis. The first will be covered in a
|
||||
future post on logical connectives and truth-tables.
|
||||
|
||||
When we analyse a series of propositions and the relations between them we call
|
||||
the group a _set_. Sets possess logical _properties_. The first such property we
|
||||
will define is **consistency**.
|
||||
|
||||
> A given set of propositions is consistent if and only if it is possible for
|
||||
> each member of the set to be true at the same time. It is inconsistent just if
|
||||
> this is not the case.
|
||||
|
||||
The following set of propositions form an inconsistent set.<sup>2</sup> Can you
|
||||
spot the inconsistency?
|
||||
|
||||
```
|
||||
(1) Anyone who takes astrology seriously is crazy.
|
||||
(2) Jane is my sister and no sister of mine has a crazy person for a husband.
|
||||
(3) Richard is Jane's husband and he checks his horoscope every morning.
|
||||
(4) Anyone who checks their horoscope takes astrology seriously.
|
||||
```
|
||||
|
||||
The set is inconsistent because it is not the case that all the propositions can
|
||||
be true at once. Specifically: if (1), (3), and (4) are true, (2) cannot be.
|
||||
Alternatively, if (2), (3) and (4) are true (1) cannot be.
|
||||
|
||||
Let's illuminate the first instance of inconsistency. On the one hand we assert
|
||||
that taking astrology seriously is crazy and we assume that checking your
|
||||
horoscope means that you take astrology seriously. Richard is Jane's husband and
|
||||
he checks his horoscope each morning. By definition then, Richard is crazy and
|
||||
Jane is married to him, but if I believe this, I cannot believe that my sister
|
||||
would not marry a crazy person. My beliefs are inconsistent.
|
||||
|
||||
Now, the second instance of inconsistency. Jane, my sister, is married to
|
||||
Richard. None of my sisters have a crazy husband. Richard checks his horoscope
|
||||
each day and therefore takes astrology seriously. If this is the case, I cannot
|
||||
believe that taking astrology seriously is crazy because otherwise my sister
|
||||
cannot be married to Richard.
|
||||
|
||||
In deconstructing the inconsistencies in the scenario of Richard and Jane we
|
||||
have been concerned to point out that one proposition _implies_ or _follows_
|
||||
another proposition. Intuitively we have been invoking the logical notion of an
|
||||
**argument**. This is not so different from what we mean by an argument in
|
||||
ordinary life. If we are arguing with someone, we believe that they are wrong
|
||||
about something where 'something' is a proposition and 'wrong' means _false_.
|
||||
For example _the prime minister is a liar_. A more logical way to put this is
|
||||
that we believe their beliefs about a set of propositions are inconsistent. In
|
||||
order to make assertions about the relative consistency or inconsistency of a
|
||||
set of propositions we advance arguments. This is like seeking to change the
|
||||
person's viewpoint by showing that their belief _A_ conflicts with their other
|
||||
belief _B_ or that if _A_ is true, they cannot believe _B_.
|
||||
|
||||
In the example above each proposition in the set has equal footing; we have not
|
||||
distinguished one type of proposition from any other. When we construct an
|
||||
argument however, we distinguish the propositions by type. We say that one or
|
||||
more propositions are _premises_ and one proposition is the _conclusion_:
|
||||
|
||||
> An argument is a set of propositions comprising one or more premises and a
|
||||
> conclusion. The conclusion is taken to be supported by the premises.
|
||||
|
||||
Let's demonstrate how this works by making an implicit argument from previous
|
||||
example explicit:
|
||||
|
||||
```
|
||||
(P1) Anyone who checks their horoscope takes astrology seriously.
|
||||
(P2) Richard checks his horoscope.
|
||||
(C) Richard takes astrology seriously.
|
||||
```
|
||||
|
||||
This constitutes a logical argument because of how the propositions are
|
||||
arranged: we are asserting that the conclusion (C) is supported by premises P1
|
||||
and P2. We call such arguments **syllogisms**.
|
||||
|
||||
## Evaluation criteria for arguments
|
||||
|
||||
In what sense do the premises support the conclusion of an argument? What is the
|
||||
relation between these two types of proposition? The word 'support' is rather
|
||||
vague. In logic there are different ways to assess the qualility of an argument:
|
||||
**inductive strength** and **deductive validity**.
|
||||
|
||||
Consider the following argument:
|
||||
|
||||
```
|
||||
(P1) When a cat scratches itself it can mean it has fleas.
|
||||
(P2) Our tabby, Carrot has been scratching himself a lot lately.
|
||||
(C) Carrot has fleas.
|
||||
```
|
||||
|
||||
To ask ourselves whether this is a strong argument is to reflect on whether we
|
||||
have good grounds for believing the conclusion given the premises. My intuition
|
||||
that this is a reasonable argument but not a particularly strong one. It doesn't
|
||||
strain credulity but it is by no means watertight.
|
||||
|
||||
Contrast it with this argument:
|
||||
|
||||
```
|
||||
(P1) Every day the sun rises in the east.
|
||||
(P2) The sun rose in the east today.
|
||||
(C) The sun will rise in the east tomorrow.
|
||||
```
|
||||
|
||||
This strikes me as a stronger argument than the first. If you had to bet on
|
||||
Carrot having fleas or the sun rising in the East you would put your money on
|
||||
the latter although you would probably get better returns on the former. With
|
||||
arguments of this nature we are proceeding on the basis of likelihood. The
|
||||
technical term for this is _induction_ : given some background context of
|
||||
beliefs (for instance the typical behaviour of cats and the planet) there are
|
||||
stronger or weaker grounds for accepting the conclusion of an argument bases on
|
||||
its premises.
|
||||
|
||||
> An argument is inductively strong if and only if the conclusion is probably
|
||||
> true given the premises.
|
||||
|
||||
Although the arguments differ in their relative strength they are both inductive
|
||||
arguments. This is because they are each falsifiable. In the first case, this is
|
||||
obvious: Carrot might not have fleas and could be scratching for some other
|
||||
reason. Perhaps surprisingly, the second argument is also falsifiable. The
|
||||
magnetic field of the Earth could switch polarity meaning that while the Earth
|
||||
would not change its position relative to the sun, our compass would be inverted
|
||||
and therefore indicate that the sun rising in the west. This is very unlikely to
|
||||
happen imminently but it _will_ happen at some point in the next 10,000 years.
|
||||
Therefore the conclusion could prove false.
|
||||
|
||||
The next obvious is whether all arguments are like this. Is probability the best
|
||||
we can hope for? Fortunately not. Propositional logic is a deductive schema
|
||||
which means it aims for truths that are not falsifiable in the manner of the two
|
||||
examples above. It is this criterion of evaluation that we mainly interested in
|
||||
when we use logic as a formal discipline. This is the domain of deductive
|
||||
validity. Validity is our second key logical property.
|
||||
|
||||
> An argument is deductively valid if and only if it is not possible for the
|
||||
> premises to be true and the conclusion false.
|
||||
|
||||
## Validity and soundness
|
||||
|
||||
The following syllogism is an example of a valid argument:
|
||||
|
||||
```
|
||||
(P1) All fish live in the sea and only in the sea.
|
||||
(P2) Cod are fish.
|
||||
(C) Cod live in the sea.
|
||||
```
|
||||
|
||||
And here is an invalid argument:
|
||||
|
||||
```
|
||||
(P1) All fish live in the sea and only in the sea.
|
||||
(P2) Cod are fish.
|
||||
(C) Cod live on land.
|
||||
```
|
||||
|
||||
In the valid instance, there is no sense to the idea that we might accept each
|
||||
premise and yet deny the conclusion. In the invalid instance this is not the
|
||||
case: we can accept each premise and deny the conclusion. We can relate this
|
||||
back to the the notion of consistency. Recall that a set of propositions is
|
||||
consistent if and only if it is possible for each member to be true at once.
|
||||
With the first argument we cannot consistently accept the premises and deny the
|
||||
conclusion whereas in the case of the second argument we can quite consistently
|
||||
accept the premises and deny the conclusion since the propositions do not
|
||||
comprise a consistent set.
|
||||
|
||||
In contrast to the previous inductive arguments, with valid arguments the
|
||||
conclusion is supported purely in virtue of the terms used in the premises and
|
||||
the propositions they express. In order to assess the validity of an argument
|
||||
like the first it is not neccessary to aquaint oneself with cod and to study
|
||||
their behaviour so as to determine whether they do in fact live in the sea. All
|
||||
that is necessary is to understand the propositions expressed. Philosophers
|
||||
refer to statements of this sort as _analytic_. They are true or false 'by
|
||||
definition'. More specifically: the concept of the predicate is contained within
|
||||
the concept of the subject for example _all brothers are male_. In the case of
|
||||
the argument, we have defined at (P1) that fish are creatures that live in the
|
||||
sea so given this definition, the conclusion is bound to follow from the
|
||||
premises since it is just a specific instance of the general property already
|
||||
defined. Validity is therefore an entirely formal notion that exists over and
|
||||
above any facts of the matter. If I have defined 'fish' universally as
|
||||
sea-dwellers I cannot without inconsistency say that they are not sea-dwellers.
|
||||
|
||||
This is further exemplified with an argument like the following:
|
||||
|
||||
```
|
||||
(P1) Manchester is the capital of the UK.
|
||||
(P2) Manchester is north of Birmingham.
|
||||
(C) The capital of the UK is north of Birmingham.
|
||||
```
|
||||
|
||||
Is this a valid argument? To answer this question remember that invalidity means
|
||||
that it is possible for the premises to be true and the conclusion false. In the
|
||||
strict logical sense, this is valid argument since _were_ the premises true, the
|
||||
conclusion would also be true. The point is that validity is a function of
|
||||
truth-conditions not truth-values. The truth value of the first premise and the
|
||||
conclusion in the above argument happen to be false but this does not affect its
|
||||
validity. There is no necessity to London being the capital of the UK and not
|
||||
Manchester. We can imagine things being otherwise which is to say that we can
|
||||
entertain the truth-conditions of the proposition and make judgements in
|
||||
accordance with it being true or false quite independently of whether it is in
|
||||
actuality true/false.
|
||||
|
||||
We can take this back to the earlier example of the invalid argument about cod:
|
||||
in order to judge the argument invalid it was not necessary for us to look for
|
||||
cod that live on land and come back empty. Rather we just had to assume that if
|
||||
all fish live in the sea then it must be the case that if something is a fish,
|
||||
it is a sea-dweller. We made no commitment to fish actually living in the sea.
|
||||
|
||||
Does this mean that actual truth does not matter to logic? No, it just means
|
||||
that validity as a property is decoupled from truth as a property although we
|
||||
cannot of course have a grasp of the notion of validity without possessing a
|
||||
prior notion of truth. A proposition being true in fact is a property it may or
|
||||
not possess in addition to its membership within a valid sequence of reasoning.
|
||||
If an argument is both valid and its premises are true in fact we say that it is
|
||||
a **sound** argument. This is a stronger criterion of evaluation than validity
|
||||
alone.
|
||||
|
||||
> An argument is sound if and only if it is deductively valid and all of its
|
||||
> premises are true.
|
||||
|
||||
It follows from this definition of soundness that:
|
||||
|
||||
- an argument cannot be sound if it is not also valid
|
||||
- an argument can be valid without being sound
|
||||
- if an argument is sound its conclusion must be true
|
||||
|
||||
(The last point follows from the fact that soundness means the premises are true
|
||||
and validity requires that if the premises are true the conclusion must also be
|
||||
true.)
|
||||
|
||||
We have already seen examples of arguments that are valid but not sound in the
|
||||
Manchester example, let's close this section with an example where both premises
|
||||
and conclusion are true yet the argument is invalid, demonstrating that truth
|
||||
alone is not sufficient for soundness.
|
||||
|
||||
```
|
||||
(P1) London is the capital of the UK.
|
||||
(P2) The capital of the UK is in the southern part of the country.
|
||||
(P3) Cambridge is not the capital of the United Kingdom
|
||||
(C) London is south of Cambridge
|
||||
```
|
||||
|
||||
This argument is deductively invalid because we can consistently assert the
|
||||
premises but deny the conclusion. Specifically: there isn't anything about the
|
||||
premises that makes the denial of the conclusion inconsistent. From the point of
|
||||
view of the premises alone, London could be north of Cambridge whilst still
|
||||
being in the southern part of the country.
|
||||
|
||||
## Logical possibility
|
||||
|
||||
In distinguishing the properties of logical consistency and validity we have
|
||||
been making much tacit use of the notion of _possibility_. This is because when
|
||||
we consider the validity of an argument we are assessing truth-conditions and
|
||||
this consists in asking ourselves what could or could not be the case: were it
|
||||
such that _P_, then it would be the case that _Q_. It is important to understand
|
||||
what possibility means in the context of logic and how it differs from what we
|
||||
might mean ordinarily when we use the term.
|
||||
|
||||
It is evident from the case of arguments that are valid but not sound that logic
|
||||
operates with a specialised notion of possibility. For example it has to be the
|
||||
case that the proposition _Every woman can levitate_ is logically possible since
|
||||
the following argument is valid:
|
||||
|
||||
```
|
||||
(P1) Ellen is a woman.
|
||||
(P2) Every woman can levitate.
|
||||
(C) Ellen can levitate.
|
||||
```
|
||||
|
||||
But we know of course that women cannot levitate. When we assert that this is
|
||||
impossible we are relying on a stronger notion of possibility than logical
|
||||
possibility. It follows that the concept of possibility can have different
|
||||
degrees. The scope of the concept of possibility has been the concern of
|
||||
logicians and philosophers since at least the time of Plato and numerous
|
||||
different formulations exist. The notion that we mostly work with unreflectively
|
||||
in everyday life is nomological possibility. This means 'governed by the
|
||||
application of laws' where these laws pertain to our current understanding of
|
||||
the natural world as determined by physics. Levitation is therefore
|
||||
nomologically impossible but logically possible.
|
||||
|
||||
If logical possibility is not contrained by the laws of physics does it place
|
||||
any restrictions on what is possible? Logic applies a single restriction, the
|
||||
law of non-contradiction: a proposition cannot both be true and false at once.
|
||||
The following propositions are examples of a contradictory propositions.
|
||||
|
||||
```
|
||||
There is a dog that is not a dog.
|
||||
Today is Tuesday and today is not Tuesday.
|
||||
The cat that is dead is alive.
|
||||
```
|
||||
|
||||
From this we can derive the following property of logical possibility:
|
||||
|
||||
> A proposition is logically possible just if it does not imply a contradiction.
|
||||
|
||||
## Logical truth, falsity and indeterminacy
|
||||
|
||||
What are the truth-conditions of a contradictory proposition? We know that a
|
||||
logically possible proposition such as _every woman can levitate_ could be true
|
||||
or false. It has to be so because we are capable of constructing valid arguments
|
||||
where it features as a premise and a valid argument implies the possible truth
|
||||
of its premises.
|
||||
|
||||
In the case of a contradiction there are no conditions under which it could be
|
||||
judged to be true. For this reason, contradictions are classified as **logically
|
||||
false**. This is distinct from ordinary falsity where a proposition _could_ be
|
||||
true but happens to be false. Logically false propositions are universally false
|
||||
and could never be true. This is consistent with our previous observation of the
|
||||
law of non-contradiction: if a proposition cannot be both true and false at once
|
||||
we are saying that something _cannot be the case_ which is of course to say _is
|
||||
false_.
|
||||
|
||||
Logical falsity is therefore another property that a proposition may possess and
|
||||
it is a property that is possessed by all propositions that are contradictions:
|
||||
|
||||
> A proposition is logically false if and only if it is not possible for the
|
||||
> sentence to be true.
|
||||
|
||||
Complementing logical falsity is **logical truth**:
|
||||
|
||||
> A proposition is logically true if and only if it is not possible for the
|
||||
> sentence to be false.
|
||||
|
||||
We call logically true propositions tautologies. Some examples:
|
||||
|
||||
```
|
||||
An apple is an apple.
|
||||
Today is Tuesday or today is not Tuesday.
|
||||
The cat is dead or alive.
|
||||
```
|
||||
|
||||
The properties of logical truth and falsity are alike in their universality.
|
||||
Propositions that are logically true do not exclude any possibility (today is
|
||||
Tuesday or it is not Tuesday; there is no possible state outside of this)
|
||||
whereas logically false propositions exclude all possibilities (there is no
|
||||
scenario where today is both Tuesday and not Tuesday).
|
||||
|
||||
We class all propositions that are not contradictions or tautologies **logically
|
||||
indeterminate** propositions. This means that their truth-value is not assigned
|
||||
purely on the basis of the meanings of the terms of which they are comprised.
|
||||
_It is raining_ for example, is logically indeterminate because we cannot know
|
||||
its truth-value just by reflecting on the meaning of the predicate _is raining_.
|
||||
It may be true under certain conditions and false under others and in order to
|
||||
know the specific truth-value at a given moment, we must look to states of
|
||||
affairs beyond the sentence. The vast majority of propositions expressed in
|
||||
natural and formal languages are indeterminate in this manner.
|
||||
|
||||
## Summary
|
||||
|
||||
In this post we introduced propositions as descriptions of states of affairs
|
||||
that possess truth-conditions. We noted that propositions are expressed in
|
||||
language through the medium of declarative sentences and that not every
|
||||
expression in a language possesses a propositional form. Two key properties that
|
||||
pertain to sets of propositions were introduced and exemplified: consistency and
|
||||
validity. In addition we considered different evaluative criteria for logical
|
||||
arguments comparing inductive strength with deductive validity. We distinguished
|
||||
logical possibility from nomological possibility and explained how the law of
|
||||
non-contradiction places bounds on what is logically possible. Equipped with the
|
||||
concept of logical possibility we were able to introduce logical truth and
|
||||
falsity, analysing the truth-conditional form of tautologies and contradictions
|
||||
and noting that propositions that are neither logically true or false are
|
||||
logically indeterminate.
|
||||
|
||||
## Notes
|
||||
|
||||
<ol>
|
||||
<li>These all being standard instances of speech acts which are resistant to truth-conditional analyses as noted above.</li>
|
||||
<li>This example comes from (Bergmann, Moor and Nelson, 2014).</li>
|
||||
</ol>
|
||||
|
||||
## References
|
||||
|
||||
Bergmann, M., Moor, J. and Nelson, J. (2014). The logic book. Boston:
|
||||
Mcgraw-Hill/Connect Learn Succeed.
|
||||
|
||||
Wikipedia Contributors (2019). Speech-act. [online] Wikipedia. Available at:
|
||||
https://en.wikipedia.org/wiki/Speech_act.
|
||||
|
||||
|
||||
157
posts/jest-parameterization.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
title: "Jest parameterization"
|
||||
slug: /jest-parameterization/
|
||||
date: 2023-11-20
|
||||
tags: ["learning", "javascript", "unit-testing"]
|
||||
---
|
||||
|
||||
At work I am in the process of upgrading our AWS Lambdas to use the v.18 Node
|
||||
runtime (most of them are still using v.14). It has been a good opportunity to
|
||||
carry out refactoring and address technical debt.
|
||||
|
||||
Many of the lambdas lack unit tests whilst others have tests that haven't been
|
||||
maintained. Thus I've been spending time adding and optimising tests in Jest.
|
||||
|
||||
I've been harnessing parameterization when improving unit test coverage. The
|
||||
lambdas I'm currently working on are subroutines within a broader
|
||||
[AWS Step Function](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html)
|
||||
that constitutes the backend of one of our internal content management systems.
|
||||
Much of the functionality consists in generating and parsing properties from XML
|
||||
and JSON. As I am testing the same code under different conditions, the tests
|
||||
are highly repetitive and can be readily parameterized.
|
||||
|
||||
Here's one example:
|
||||
|
||||
```js
|
||||
describe("handler", () => {
|
||||
let mockApiGatewayEvent = {
|
||||
propertyId: "1234",
|
||||
isCrossPublished: false,
|
||||
}
|
||||
describe("exit conditions", () => {
|
||||
it("should throw an error if a user attempts to unpublish an alpha file", async () => {
|
||||
event = {
|
||||
...event,
|
||||
fileId: "alpha:1234",
|
||||
}
|
||||
await expect(handler(event)).rejects.toThrow(
|
||||
"Not allowed to unpublish file alpha:1234"
|
||||
)
|
||||
})
|
||||
it("should throw an error if user attempts to unpublish a beta file", async () => {
|
||||
event = {
|
||||
...event,
|
||||
fileId: "beta:1234",
|
||||
}
|
||||
await expect(handler(event)).rejects.toThrow(
|
||||
"Not allowed to unpublish file beta:1234"
|
||||
)
|
||||
})
|
||||
// and so on...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
I've anonymised the specifics of the data but the process is straightforward:
|
||||
I'm asserting that the correct error text is returned if a user attempts to
|
||||
delete a certain filetype. I reduced the verbiage of countless `it` blocks by
|
||||
utilising parameterization:
|
||||
|
||||
```js
|
||||
describe("exit conditions", () => {
|
||||
let mockApiGatewayEvent = {
|
||||
projectId: "1234",
|
||||
preview: false,
|
||||
}
|
||||
|
||||
it.each([["alpha:1234"], ["beta:1234"]])(
|
||||
"should throw an error if user attempts to unpublish %s file",
|
||||
async (fileId) => {
|
||||
mockApiGatewayEvent = {
|
||||
...mockApiGatewayEvent,
|
||||
fileId,
|
||||
}
|
||||
await expect(handler(mockApiGatewayEvent)).rejects.toThrow(
|
||||
`Not allowed to unpublish file ${fileId}`
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
Instead of multiple `it` clauses, there is a single `each` expression that loops
|
||||
through each file variant, executing the same test each time, changing only the
|
||||
file name that is output.
|
||||
|
||||
The `%s` symbol is a placeholder for string substitution. This lets me include
|
||||
the name of each variant in the test description. This is important because
|
||||
there is a risk of obscuring the specifics of each test iteration when using
|
||||
parameterization. It's essential to be able to trace a failure to the specific
|
||||
iteration.
|
||||
|
||||
In the example below, the process is functionally the same but there are more
|
||||
parameters in the mix:
|
||||
|
||||
```js
|
||||
describe("deletePageFromS3()", () => {
|
||||
beforeEach(() => {
|
||||
process.env.AWS_ENV = "live"
|
||||
s3ClientMock.reset()
|
||||
})
|
||||
|
||||
const parameters = [
|
||||
{
|
||||
previouslyPublished: false,
|
||||
isDraft: false,
|
||||
bucket: "bucket:alpha",
|
||||
key: "key:alpha",
|
||||
},
|
||||
{
|
||||
previouslyPublished: true,
|
||||
isDraft: false,
|
||||
bucket: "bucket:beta",
|
||||
key: "key:beta",
|
||||
},
|
||||
{
|
||||
previouslyPublished: false,
|
||||
isDraft: true,
|
||||
bucket: "bucket:gamma",
|
||||
key: "key:gamma",
|
||||
},
|
||||
{
|
||||
previouslyPublished: true,
|
||||
isDraft: true,
|
||||
bucket: "bucket:delta",
|
||||
key: "key:delta",
|
||||
},
|
||||
]
|
||||
|
||||
it.each(parameters)(
|
||||
"should return page for deletion, given: previouslyPublished is %s, isDraft is %s",
|
||||
async ({ previouslyPublished, isDraft, bucket, key }) => {
|
||||
await deleteFileFromS3("url", previouslyPublished, isDraft)
|
||||
const deleteObjectCommand = s3ClientMock.calls()[0].args[0]
|
||||
expect(deleteObjectCommand.input).toEqual({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
The process under test is a function that uses the AWS SDK to delete objects
|
||||
from an S3 bucket. I am checking that the (mocked) S3 client is called with the
|
||||
correct parameters.
|
||||
|
||||
This time I am passing in an array of objects to the `each` function rather than
|
||||
a multi-dimensional array. This makes it easier to destructure the specific
|
||||
properties in the individual test cases. Again I use `%s` to interpolate a
|
||||
subset of the parameters into each test description. `%s` applies to the each
|
||||
value in the `each` array in sequence so you just repeat it to individuate the
|
||||
different params.
|
||||
|
||||
This has been a brief sketch of some applied examples of parameterization. For a
|
||||
better account, see
|
||||
[Parameterized tests in JavaScript with Jest](https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html)
|
||||
by Rafał Borowiec.
|
||||
166
posts/local-dynamodb-setup-sam.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
title: "Getting a local DynamoDB instance working"
|
||||
slug: /local-dynamodb-setup-sam/
|
||||
date: 2024-06-08
|
||||
tags: ["log", "aws", "dynamodb"]
|
||||
---
|
||||
|
||||
I wasted most of an afternoon trying to get the following set-up:
|
||||
|
||||
- Two local instances of DynamoDB running in Docker
|
||||
- The ability to add table data to either instance via NoSQL Workbench and query
|
||||
it via an AWS lambda
|
||||
|
||||
If you are coming from SQLPro or DBeaver, Amazon's NoSQL workbench is a bit
|
||||
unintuitive. Creating the lambdas and the Docker containers was easy, it was
|
||||
getting them recognised by this client that was tricky. Anyway this is the
|
||||
process.
|
||||
|
||||
I want to set up two Docker instances of the `amazon/dynamodb-local` image,
|
||||
along with a bridging network so that the Docker container that SAM runs in can
|
||||
communicate with the containers that DynamoDB runs in. Hence `docker-compose` is
|
||||
the way to go:
|
||||
|
||||
```yml
|
||||
# docker-compose.yaml
|
||||
|
||||
version: "3.8"
|
||||
services:
|
||||
dev:
|
||||
image: amazon/dynamodb-local
|
||||
container_name: timetracking_dynamodb_dev
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./data:/home/dynamodblocal/data"
|
||||
networks:
|
||||
- sam-local
|
||||
stage:
|
||||
image: amazon/dynamodb-local
|
||||
container_name: timetracking_dynamodb_stage
|
||||
ports:
|
||||
- "8001:8001"
|
||||
volumes:
|
||||
- "./data:/home/dynamodblocal/data"
|
||||
networks:
|
||||
- sam-local
|
||||
|
||||
networks:
|
||||
sam-local:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
My two DDB instances are `dev` and `stage` running on ports 8000 and 8001
|
||||
respectively. Their shared network is called `sam local`.
|
||||
|
||||
After running `docker-compose up`. I run `docker network ls` and it confirms the
|
||||
network has been created (it prepends the network name with the repo name):
|
||||
|
||||

|
||||
|
||||
Next comes SAM. I've just created the default Typescript Lambda template using
|
||||
the SAM CLI. For now this will only expose a single API Gateway endpoint,
|
||||
`/fetch`, which I will use to GET my DynamoDB table data. The key info from the
|
||||
template:
|
||||
|
||||
```yml
|
||||
# template.yaml
|
||||
|
||||
Resources:
|
||||
TimeTrackingFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
CodeUri: src/
|
||||
Handler: index.handler
|
||||
Runtime: nodejs20.x
|
||||
Architectures:
|
||||
- x86_64
|
||||
Events:
|
||||
Fetch:
|
||||
Type: Api
|
||||
Properties:
|
||||
Path: /fetch
|
||||
Method: get
|
||||
```
|
||||
|
||||
I'll use the following command to start the local APIGateway server:
|
||||
|
||||
```sh
|
||||
sam build && sam local start-api --docker-network time-tracking_sam-local
|
||||
```
|
||||
|
||||
If I run `docker network inspect time-tracking_sam-local`, I can confirm that
|
||||
the SAM Docker instance (which AWS creates and which is effectively a black box
|
||||
to me) is on the same network as the two DDB instances that I set up earlier:
|
||||
|
||||

|
||||
|
||||
Next I want to view my containers in NoSQL workbench so I can create the table
|
||||
schema and start adding data. I'm just going to worry about the `dev` container
|
||||
here. I go to the _Operation builder_ and add a new connection:
|
||||
|
||||

|
||||
|
||||
This automatically generates an _Access key ID_ and _Secret access key_ even
|
||||
though this is a local service that won't interact with the production version
|
||||
in any way. You _must_ use these. Some guides say you can put anything when
|
||||
connecting locally but in my experience this is _not true_.
|
||||
|
||||
Using these keys, I write a skeletal lambda just to check the connection via the
|
||||
AWS SDK for DynamoDB:
|
||||
|
||||
```ts
|
||||
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb"
|
||||
|
||||
const client = new DynamoDBClient({
|
||||
region: "localhost",
|
||||
endpoint: "http://dev:8000",
|
||||
credentials: {
|
||||
accessKeyId: "xxxx",
|
||||
secretAccessKey: "xxxx",
|
||||
},
|
||||
})
|
||||
|
||||
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
const command = new ListTablesCommand({})
|
||||
const response = await client.send(command)
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
message: "Successfully retrieved tables",
|
||||
tableNames: response.TableNames,
|
||||
}),
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
The first problem because I wasn't providing a region. Why would I, this is
|
||||
local? But it needs to be `localhost`. And again, you must put the actual
|
||||
credential values that the Workbench gives you.
|
||||
|
||||
The next issue was that this function was returning an empty array when I
|
||||
queried the `/fetch` endpoint. Even though I had set up a table in Workbench:
|
||||
|
||||

|
||||
|
||||
You must click _Commit to Amazon DynamoDB_ even though this is counter-intuitive
|
||||
and suggests you are going to auto-provision paid services! You can then select
|
||||
the local instance that you are going to seed with the table from the model:
|
||||
|
||||

|
||||
|
||||
Now if you back to _Operation builder_, the table is associated with the given
|
||||
instance and the data is displayed:
|
||||
|
||||

|
||||
|
||||
On reflection, this makes a lot of sense as it decouples the data model from any
|
||||
particular instance (local or production) that uses the data and makes it easy
|
||||
to seed different tables to different database instances.
|
||||
|
||||
So finally, if I now query the `fetch` endpoint, I get the table name returned,
|
||||
demonstrating that all the different parts have joined up:
|
||||
|
||||

|
||||
756
posts/nand-to-tetris-unit-one.md
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
---
|
||||
title: "N2T Unit One: Boolean functions and gate logic"
|
||||
slug: /nand-to-tetris-unit-one/
|
||||
date: 2023-01-13
|
||||
tags: ["article", "learning", "computer-architecture"]
|
||||
---
|
||||
|
||||
I have recently started [Nand to Tetris](https://www.nand2tetris.org/course).
|
||||
This course teaches the foundations of hardware and computer achitecture and is
|
||||
based on the textbook _The Elements of Computing Systems_ by Noam Nisan and
|
||||
Shimon Schocken. As the course proceeds you build a functioning general-purpose
|
||||
computer that is eventually capable of running Tetris and any number of other
|
||||
programs. The hardware is built primarily through simulation software running on
|
||||
Hack, a simplified
|
||||
[hardware description language](https://en.wikipedia.org/wiki/Hardware_description_language)
|
||||
similar to Verilog and VHDL.
|
||||
|
||||
In this post I outline what I learned in the first unit. This broadly follows
|
||||
the curriculum but adds extra details I have acquired elsewhere to give the
|
||||
fullest account.
|
||||
|
||||
## Bits and functions
|
||||
|
||||
The workings of a classical computer can be reduced to a series of operations on
|
||||
the binary digits (bits) 0 and 1. A computational process can be represented as
|
||||
a function: data (a series of bits) enters the function in one state and exits
|
||||
in another state. This new state is a product of the function.
|
||||
|
||||

|
||||
|
||||
The most primitive bit operations are equivalent to the truth-conditions of the
|
||||
logical connectives of Boolean algebra. In logic, the Boolean values are _true_
|
||||
and _false_ but we will use 1 and 0, to represent these states as bits. There
|
||||
are multiple logical connectives but we will mostly focus on AND, OR, and NOT
|
||||
for simplicity.
|
||||
|
||||
The logic of each Boolean connective can be expressed as a function.
|
||||
|
||||
NOT can be represented as follows;
|
||||
|
||||
$$
|
||||
f(x) = \lnot (x)
|
||||
$$
|
||||
|
||||
NOT (¬) is a unary operator which means it takes one operand (_x_). We pass a
|
||||
single bit as the operand and the function inverts its value. We can utilise
|
||||
logical [truth tables]() to represent all possible inputs and outputs for the
|
||||
operator:
|
||||
|
||||
| x | f(x) = NOT(x) |
|
||||
| --- | ------------- |
|
||||
| 1 | 0 |
|
||||
| 0 | 1 |
|
||||
|
||||
If NOT receives 0 as an input it will return 1 as the output. If it receives 1
|
||||
as the input it will return 0 as the output.
|
||||
|
||||
AND ($\land$) and OR($\lor$) are binary operators: they receive two operands as
|
||||
input. Therefore we pass two bits to the function. As a bit can be one of two
|
||||
values (0 or 1), there are four possible inputs.
|
||||
|
||||
For AND this gives us:
|
||||
|
||||
| x | y | f(x) = x AND y |
|
||||
| --- | --- | -------------- |
|
||||
| 1 | 1 | 1 |
|
||||
| 0 | 1 | 0 |
|
||||
| 1 | 0 | 0 |
|
||||
| 0 | 0 | 0 |
|
||||
|
||||
AND returns 1 if both input bits are 1, otherwise it returns 0.
|
||||
|
||||
OR returns 1 if one or both bits are 1, otherwise it returns 0:
|
||||
|
||||
| x | y | f(x) = x OR y |
|
||||
| --- | --- | ------------- |
|
||||
| 1 | 1 | 1 |
|
||||
| 0 | 1 | 1 |
|
||||
| 1 | 0 | 1 |
|
||||
| 0 | 0 | 0 |
|
||||
|
||||
## Logic gates
|
||||
|
||||
The logical function of each of the Boolean operators is implemented at the
|
||||
level of computer hardware by logic gates. Each gate is represented with one or
|
||||
more input pins and a single output pin through which the bits enter and exit.
|
||||
The pins feed into and out of a chip which executes the function.
|
||||
|
||||
The diagram below shows the logic gates for AND, OR, and NOT.
|
||||
|
||||

|
||||
|
||||
Logic gates are an abstraction. In reality, you cannot _see_ a logic gate in a
|
||||
computer. If we were to look inside a chip that implements gate logic we would
|
||||
see an arrangement of transistors, capacitors and resistors connected by wires.
|
||||
These components are configured to mimic the behaviour of the logical operators.
|
||||
|
||||
_Nand to Tetris_ starts at the level of gates and does not discuss how the
|
||||
individual gates are realised in electronic circuits. However, I think it is
|
||||
useful to understand the basics of the electrical engineering so we might begin
|
||||
to grasp how it is possible to go from an inert block of metal and silicon to
|
||||
fully functioning computer.
|
||||
|
||||
### The electronic implementation of gate logic
|
||||
|
||||
We can start with the concept of a switch. Consider what is happening when we
|
||||
turn on a light using a wall switch.
|
||||
|
||||
When the switch is off, the electrical circuit that connects the bulb to the
|
||||
voltage source is broken. As a result, there is no potential difference between
|
||||
the terminals and the current cannot reach the bulb. When the switch is on, the
|
||||
circuit is complete and the current flows freely to the bulb.
|
||||
|
||||
This is represented by the following simple schematic:
|
||||
|
||||

|
||||
|
||||
This circuit embodies the logic of a NOT gate. Think of the light as 1 and the
|
||||
absence of light as 0. When the switch is on, 1 is the output and 0 is inverted.
|
||||
When the switch is off, 0 is the output and 1 is inverted.
|
||||
|
||||
This scenario can be developed to represent the logical behaviour of an AND
|
||||
gate. Imagine now that this room is a bit strange and there are two switches
|
||||
controlling the operation of the bulb. If both switches are off the bulb will
|
||||
not emit light. If one of the switches is on and the other is off the bulb will
|
||||
not emit light. The bulb will only emit light when _both_ switches are on.
|
||||
|
||||
The schematic for this scenario embodies the logic of the Boolean AND
|
||||
connective:
|
||||
|
||||

|
||||
|
||||
Switch-controlled circuits are functionally equivalent to what actually happens
|
||||
inside a computer when logical conditions are implemented via gates. Electrical
|
||||
charge is directed along different routes depending on the value of an on/off
|
||||
condition. However, in modern computers the actual component that controls the
|
||||
flow of current is not a switch.
|
||||
|
||||
Whilst we could construct primitive computers with switches, the average CPU has
|
||||
more than a million logic gates. Controlling this number of gates with
|
||||
mechanical switches would be practically impossible and even if it were
|
||||
achieved, it would result in extremely slow processing times.
|
||||
|
||||
This is an important point because the computational power of logic gates
|
||||
emerges from their behaviour at scale. Collections of gates are combined to
|
||||
express complex logical conditions that are a function of their individual
|
||||
parts. For this to be possible, the output of a collection of gates needs to be
|
||||
able to be fed into another collection and this it would be difficult to achieve
|
||||
in a mechancial switch-based system.
|
||||
|
||||
Instead of mechanical switches, computers use transistors. Transistors are
|
||||
semi-conductors: components that possess an electrical conductivity between that
|
||||
of a conductor and an insulator. This property means they can both impede and
|
||||
expede the flow of electrical charge.
|
||||
|
||||
There are different types of transistors but we will focus on basic Bipolar
|
||||
Junction Transistors:
|
||||
|
||||

|
||||
|
||||
Applying a small amount of current at the base terminal (B) of a BJT allows a
|
||||
larger current to flow from the collector (C) to the emitter (E). Removing
|
||||
current at the base terminal reduces the flow from collector to emitter. This is
|
||||
because the the emitter and collector are composed of a semi-conductor that has
|
||||
a surplus of electrons (negatively charged) whereas the base has a deficiency of
|
||||
electrons (positively charged). This state creates a modifiable potential
|
||||
difference, reducing or increasing the current based on the voltage.
|
||||
|
||||
The base terminal of a transistor is another way of implementating the gate-like
|
||||
behaviour we previously achieved with mechanical switches. However, there is an
|
||||
important difference. With a switch, the circuit is actually broken when it is
|
||||
in the "off" state and there is no current flowing at all. With a transistor,
|
||||
the current drops in the "off" state but a voltage remains.
|
||||
|
||||
Because a continuous circuit is an analogue system, the quantities of
|
||||
resistance, voltage and current are not discrete values, they will vary over a
|
||||
given range. Thus "off" corresponds to "low" voltage and "on" corresponds to
|
||||
"high" voltage. The specific stipulation will depend on the circuit design but
|
||||
it is typically the case that a state of 1 or "on" is within the range 2-5V
|
||||
whereas a state of 0 or "off" is within the range 0.0 - 0.8V.
|
||||
|
||||
## Boolean function synthesis
|
||||
|
||||
To recap, elementary computational processes can be represented as logical
|
||||
functions. A function consists in one or more Boolean operators processing bits.
|
||||
For each Boolean operator we can construct a chip that represents its truth
|
||||
conditions. We call these chips logic gates. Logic gates are built with
|
||||
transistors that either block or permit the flow of electric current. This makes
|
||||
them behave in a manner almost identical to mechanical switches.
|
||||
|
||||
Now that we know how the individual logic gates work and how they are
|
||||
implemented electronically, we will explore how they can be applied in
|
||||
combination to represent complex logical states that more closely resemble
|
||||
actual computer programs. This process is known as **Boolean function
|
||||
synthesis**.
|
||||
|
||||
We will construct a logic circuit that represents the truth conditions for the
|
||||
following state of affairs:
|
||||
|
||||
> The team plays on either Monday or Thursday and not at weekends
|
||||
|
||||
Let's call this _P_ for ease of reference.
|
||||
|
||||
This complex comprises several simpler atomic expressions:
|
||||
|
||||
<ol type="a" start="24">
|
||||
<li>The team plays on Monday</li>
|
||||
<li>The team plays on Thursday</li>
|
||||
<li>The team plays at weekends</li>
|
||||
</ol>
|
||||
|
||||
The first step is to construct a truth table. On the left-hand side we list all
|
||||
the possible truth values for each individual expression. On the right-hand
|
||||
side, we assign an overall truth value for their combination, based on whether
|
||||
or not they reflect the truth conditions for _P_.
|
||||
|
||||
| x | y | z | _P_ |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 1 | 1 | 0 |
|
||||
| 1 | 1 | 0 | 1 |
|
||||
| 1 | 0 | 1 | 0 |
|
||||
| 1 | 0 | 0 | 1 |
|
||||
| 0 | 1 | 1 | 0 |
|
||||
| 0 | 1 | 0 | 1 |
|
||||
| 0 | 0 | 1 | 0 |
|
||||
| 0 | 0 | 0 | 0 |
|
||||
|
||||
We are only interested in the cases where _P_ is true, so we can discount any
|
||||
lines that result in a truth value of 0 for the complex expresssion. This leaves
|
||||
us with:
|
||||
|
||||
| x | y | z | _P_ |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 1 | 0 | 1 |
|
||||
| 1 | 0 | 0 | 1 |
|
||||
| 0 | 1 | 0 | 1 |
|
||||
|
||||
Parsing each line, the truth table tells us that our complex expression (_P_) is
|
||||
true in the following scenarios:
|
||||
|
||||
- If the team plays on both Mondays and Thursdays but not at weekends
|
||||
- If the team plays on Mondays but not on Thursdays and not at weekends
|
||||
- If the team plays on Thursdays but not on Mondays and not at weekends
|
||||
|
||||
We can formalise each case:
|
||||
|
||||
- _(x AND y) AND NOT z_
|
||||
- _(x AND NOT y) AND NOT z_
|
||||
- _(NOT x and y) AND NOT z_
|
||||
|
||||
We now have three logical expressions that if constructed with logic gates would
|
||||
result in a partial representation of _P_. The representation would be partial
|
||||
because each individual expression only conveys a single aspect of the truth of
|
||||
_P_, not its totality. For example, if we constructed a circuit that represents
|
||||
_(x AND NOT y) AND NOT z_, this would only cover occasions where the team plays
|
||||
on Mondays but not on Thursdays (or the weekend). It wouldn't cover the case
|
||||
where the team plays on Thursdays but not Mondays (or the weekend).
|
||||
|
||||
We want a circuit that captures all possible instances where _P_ returns true.
|
||||
There are practical benefits to seeking a single implementation. In order to
|
||||
maximise our computational resources we want to use the minimum number of gates
|
||||
in the simplest configuration possible.
|
||||
|
||||
We start by concatenating each individual expression into a single disjunctive
|
||||
expression using logical OR:
|
||||
|
||||
$$
|
||||
((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z)
|
||||
$$
|
||||
|
||||
Next, we look for opportunities to simplify this complex expression. This is
|
||||
similar to simplifying equations in mathematical algebra. It is a heuristic
|
||||
process; there is no formal or automated procedure that will work in every case.
|
||||
|
||||
_NOT z_ occurs in each of the individual disjunctive expressions. Therefore we
|
||||
can reduce the repetition by using it only once:
|
||||
|
||||
$$
|
||||
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y) \land \lnot z
|
||||
$$
|
||||
|
||||
Now we need to consider how we can simplify the remaining expressions:
|
||||
|
||||
$$
|
||||
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y)
|
||||
$$
|
||||
|
||||
If we look closely we can see that this expression is displaying the truth
|
||||
conditions for OR. The truth conditions for _x_ and _y_ are:
|
||||
|
||||
- true if _x_ and _y_ are true
|
||||
- true if _x_ is true and _y_ is false
|
||||
- true if _x_ is false and _y_ is true
|
||||
|
||||
This recalls our earlier definition of OR:
|
||||
|
||||
| x | y | f(x) = x OR y |
|
||||
| --- | --- | ------------- |
|
||||
| 1 | 1 | 1 |
|
||||
| 0 | 1 | 1 |
|
||||
| 1 | 0 | 1 |
|
||||
| 0 | 0 | 0 |
|
||||
|
||||
Thus we can reduce:
|
||||
|
||||
$$
|
||||
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y)
|
||||
$$
|
||||
|
||||
to:
|
||||
|
||||
$$
|
||||
x \lor y
|
||||
$$
|
||||
|
||||
The reduction is now complete, allowing us to reduce:
|
||||
|
||||
$$
|
||||
((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z)
|
||||
$$
|
||||
|
||||
to:
|
||||
|
||||
$$
|
||||
(x \lor y) \land \lnot z
|
||||
$$
|
||||
|
||||
If we construct a truth table for the original expression (_P_) and its
|
||||
simplification (_P'_) we see that they are true under the same logical
|
||||
conditions which demonstrates their equivalence:
|
||||
|
||||
| x | y | z | _P_ | _P'_ |
|
||||
| --- | --- | --- | --- | ---- |
|
||||
| 1 | 1 | 0 | 1 | 1 |
|
||||
| 1 | 0 | 0 | 1 | 1 |
|
||||
| 0 | 1 | 0 | 1 | 1 |
|
||||
|
||||
### Constructing the digital circuit
|
||||
|
||||
Now that we have reduced _P_ to its simplest form using the connectives AND, OR
|
||||
and NOT we can construct a circuit using the logic gates for these connectives
|
||||
to represent the overall state of affairs expressed by _P_.
|
||||
|
||||
We will have three input bits which correspond to _x_, _y_, _z_, and a single
|
||||
output bit that will reflect the truth value of _P_ based on the inputs. The
|
||||
input bits will be fed into an arrangement of logic gates that that matches the
|
||||
logical connectives in _(x OR y) AND NOT z_.
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/nandtotetris-blog-post?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
We can confirm that the circuit implementation is an accurate representation of
|
||||
_P_ by toggling the input values to confirm that the output is only 1 when
|
||||
either _x_ or _y_ is true and _z_ is false.
|
||||
|
||||
### Further simplification with NAND
|
||||
|
||||
Our circuit uses three different types of logic gate. This is satisfactory but
|
||||
it would better if we could simplify the circuit even further and use a single
|
||||
gate rather than three. To do so we need to further reduce our logic and
|
||||
introduce another type of logic gate: NAND.
|
||||
|
||||
NAND stands for "NOT AND" and its truth conditions are the inversion of AND:
|
||||
|
||||
| x | y | f(x) = x NAND y |
|
||||
| --- | --- | --------------- |
|
||||
| 1 | 1 | 0 |
|
||||
| 0 | 1 | 1 |
|
||||
| 1 | 0 | 1 |
|
||||
| 0 | 0 | 1 |
|
||||
|
||||
NAND returns 1 whenever _x_ and _y_ are not both true. We will represent NAND in
|
||||
our formulae with the symbol:
|
||||
|
||||
$$
|
||||
\tilde\land
|
||||
$$
|
||||
|
||||
NAND is a _universal logic gate_. This means that by using NAND gates and only
|
||||
NAND gates, we can represent the truth function of every other logic gate (AND
|
||||
can be expressed with just NAND gates, as can OR and so on). It follows that we
|
||||
can create every possible logical circuit using NAND gates alone.
|
||||
|
||||
Let's demonstrate this by reformulating _(x OR y) AND NOT z_ with just NANDs:
|
||||
|
||||
$$
|
||||
( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) ) \tilde\land ( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) )
|
||||
$$
|
||||
|
||||
This is quite difficult to parse, so let's look at the circuit representation
|
||||
and derive its equivalence to _(x OR y) AND NOT z_:
|
||||
|
||||
<iframe src="https://circuitverse.org/simulator/embed/nand-simplification?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
You will notice that there is repeated forking pattern to most of the inputs.
|
||||
This occurs when the same input value is used for both input pins, equivalent to
|
||||
_x NAND x_ in the equation:
|
||||
|
||||

|
||||
|
||||
When a NAND is wired to receive the same value for each input, it embodies the
|
||||
truth conditions for NOT: in the diagram above when _a_ is 1 the output is 0 and
|
||||
when _a_ is 0 the output is 1.
|
||||
|
||||
If we feed two of these sub-circuits into a NAND, we observe that the output is
|
||||
consistent with the truth conditions for OR:
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/or_with_nand?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
This is equivalent to the _(x NAND x) NAND (y NAND y)_ section of our NAND
|
||||
equation which we can see is equivalent to _x OR y_.
|
||||
|
||||
The final part of the NAND equation is dedicated to: _NOT z_. This is achieved
|
||||
by using another forking NAND and applying it to _z_, this is then joined with
|
||||
the existing fragment via a NAND to give:
|
||||
|
||||
$$
|
||||
( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) )
|
||||
$$
|
||||
|
||||
Which is then itself forked into a NAND to give the final output.
|
||||
|
||||
This is harder to parse than the implementation that used three different
|
||||
operators but the point is just to demonstrate that such a reduction is possible
|
||||
and that complex abstract states can be constructed from the concatenation of
|
||||
primitive electronic components.
|
||||
|
||||
## Hardware Description Language
|
||||
|
||||
Digital circuits can be designed using a Hardware Description Language (HDL) and
|
||||
simulation software. An HDL is a declarative programming language used to
|
||||
describe the behaviour and structure of digital circuits. In _Nand To Tetris_
|
||||
the HDL is Hack, a simplified HDL for teaching purposes.
|
||||
|
||||
An HDL file uses specialised syntax to describe the function and implementation
|
||||
of a given chip. When it is fed into a simulator, we can test the chip's outputs
|
||||
against a variety of inputs to check it is working as intended.
|
||||
|
||||
Below is an HDL specification file for the NAND logic gate written in Hack:
|
||||
|
||||
```
|
||||
CHIP Nand {
|
||||
IN a,b;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
And(a=a,b=b,out=w);
|
||||
Not(in=w1,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
The code contains two sections:
|
||||
|
||||
- the interface (`CHIP`, `IN`, `OUT`)
|
||||
- the implementation (`PARTS`)
|
||||
|
||||
The interface names the chip and designates its input and output pins. In the
|
||||
example, the interface specifies two input pins (`a` and `b`) and a single
|
||||
output pin (`out`).
|
||||
|
||||
The interface abstracts the actual implementation of the chip. It only tells us
|
||||
the inputs and output, not how the output is generated from the input. This is
|
||||
provided by the implementation section which details the internal workings of
|
||||
the chip.
|
||||
|
||||
The NAND implementation invokes two other gates, AND and NOT. We are simply
|
||||
taking the output of AND and inverting it with NOT. The HDL specification
|
||||
describes the following circuit:
|
||||
|
||||

|
||||
|
||||
Having defined the gate we can load it into the simulator and test its
|
||||
behaviour.
|
||||
|
||||

|
||||
|
||||
We can change the values of the input pins and observe how this affects both the
|
||||
output and the interim outputs of the implementation (ie. _w1_).
|
||||
|
||||
To be more efficient we can create a test file that runs through all our
|
||||
expected outputs:
|
||||
|
||||
```
|
||||
# Nand.tst
|
||||
|
||||
load Nand.hdl,
|
||||
output-file Nand.out,
|
||||
compare-to Nand.cmp,
|
||||
output-list a%B3.1.3 b%B3.1.3 out%B3.1.3;
|
||||
|
||||
set a 0,
|
||||
set b 0,
|
||||
eval,
|
||||
output;
|
||||
|
||||
set a 0,
|
||||
set b 1,
|
||||
eval,
|
||||
output;
|
||||
|
||||
set a 1,
|
||||
set b 0,
|
||||
eval,
|
||||
output;
|
||||
|
||||
set a 1,
|
||||
set b 1,
|
||||
eval,
|
||||
output;
|
||||
```
|
||||
|
||||
We feed the test file into the simulator along with the following comparison
|
||||
file and it will compute whether the chip conforms to our expectations:
|
||||
|
||||
```
|
||||
# Nand.cmp
|
||||
|
||||
| a | b | out |
|
||||
| 0 | 0 | 1 |
|
||||
| 0 | 1 | 1 |
|
||||
| 1 | 0 | 1 |
|
||||
| 1 | 1 | 1 |
|
||||
```
|
||||
|
||||
## Coursework
|
||||
|
||||
The task for the first unit was to use Hack to create the set of logic gates and
|
||||
chips that will later be utilised in the construction of the computer. You are
|
||||
provided with NAND as a primitive and from this you build the other gates. Once
|
||||
a working gate has been constructed from NAND you are permitted to use it in the
|
||||
construction of subsequent gates. For example if you have made an OR gate solely
|
||||
out of NANDs, you may then use OR along with NAND to create XOR.
|
||||
|
||||
Below I have listed the HDL files for each gate along with a simulation of the
|
||||
circuit implementation.
|
||||
|
||||
### Gates
|
||||
|
||||
#### NOT
|
||||
|
||||
```
|
||||
CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
Nand(a=in,b=in,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
|
||||
<iframe src="https://circuitverse.org/simulator/embed/n2t-not?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
#### AND
|
||||
|
||||
```
|
||||
CHIP And {
|
||||
IN a, b;
|
||||
OUT out;CHIP DMux {
|
||||
IN in, sel;
|
||||
OUT a, b;
|
||||
|
||||
PARTS:
|
||||
Not(in=sel,out=nsel);
|
||||
And(a=in,b=nsel,out=a);
|
||||
And(a=in,b=sel,out=b);
|
||||
}
|
||||
Not(in=w1,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/n2t-and?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
#### OR
|
||||
|
||||
```
|
||||
CHIP Or {
|
||||
IN a, b;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
Nand(a=a,b=a,out=w1);
|
||||
Nand(a=b,b=b,out=w2);
|
||||
Nand(a=w1,b=w2,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/n2t-or?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div And(a=in,b=sel,out=w1);
|
||||
Or(a=w1,b=sel,out=out);
|
||||
Not(in=out,out=a);>
|
||||
|
||||
#### XOR
|
||||
|
||||
```
|
||||
CHIP Xor {
|
||||
IN a, b;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
And(a=a,b=notb,out=w1);
|
||||
Not(in=b,out=notb);
|
||||
And(a=b,b=nota,out=w2);
|
||||
Not(in=a,out=nota);
|
||||
Or(a=w1,b=w2,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/n2t-xor?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
### Chips
|
||||
|
||||
As well as the basic logic gates, the first unit introduced additional chips
|
||||
that are essential for constructing a working computer. These chips represent
|
||||
more complex states that the logical operators but proceed on the same
|
||||
functional and modular basis: input values are processed internally to produce
|
||||
output values and the implementation can utilise previously constructed logic
|
||||
gates.
|
||||
|
||||
#### MUX (Multiplexer)
|
||||
|
||||
A multiplexer selects one of several input pins and forwards the selection to a
|
||||
single output pin. There are three pins: two input bits (`A`, `B`) and a
|
||||
selection bit (`SEL`). When `SEL` is applied the output bit is toggled between
|
||||
`A` and `B`. Multiplexers are essential to the construction of large digital
|
||||
circuits as they implement data selection and switching on the basis of logical
|
||||
conditions.
|
||||
|
||||
```
|
||||
CHIP Mux {
|
||||
IN a, b, sel;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
Not(in=sel,out=w1);
|
||||
And(a=w1,b=a,out=w2);
|
||||
And(a=sel,b=b,out=w3);
|
||||
Or(a=w2,b=w3,out=out);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/mux_n2t?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
#### DMUX (Demultiplexer)
|
||||
|
||||
As the name suggests, a demultiplexer reverses the functionality of a
|
||||
multiplexer. It receives a single input, and based on the `SEL` value channels
|
||||
it to either an `A` or `B` output.
|
||||
|
||||
```
|
||||
CHIP DMux {
|
||||
IN in, sel;
|
||||
OUT a, b;
|
||||
|
||||
PARTS:
|
||||
Not(in=sel,out=nsel);
|
||||
And(a=in,b=nsel,out=a);
|
||||
And(a=in,b=sel,out=b);
|
||||
}
|
||||
```
|
||||
|
||||
<div style="display:flex;margin-top:1.5rem">
|
||||
<iframe src="https://circuitverse.org/simulator/embed/dmux_v2_n2t?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
|
||||
</div>
|
||||
|
||||
### Multi-bit chips
|
||||
|
||||
Multi-bit chips are variants of the chips and gates already produced. The logic
|
||||
of a multi-bit AND is the same as the logic for a normal AND gate. They differ
|
||||
only in the number of bits they can receive and output.
|
||||
|
||||
In a real computer, passing single 1s and 0s into chips would be inefficient
|
||||
since very little information can be represented or encoded in a single bit.
|
||||
When we build the computer we will be passing values with a bit-length of 8-bits
|
||||
(a byte) as a miniumum (e.g. 10101100) and we need chips that can handle bits of
|
||||
this length.
|
||||
|
||||
For illustration, here is the HDL implementation of an AND-16:
|
||||
|
||||
```
|
||||
CHIP And16 {
|
||||
IN a[16], b[16];
|
||||
OUT out[16];
|
||||
|
||||
PARTS:
|
||||
And(a=a[0],b=b[0],out=out[0]);
|
||||
And(a=a[1],b=b[1],out=out[1]);
|
||||
And(a=a[2],b=b[2],out=out[2]);
|
||||
And(a=a[3],b=b[3],out=out[3]);
|
||||
And(a=a[4],b=b[4],out=out[4]);
|
||||
And(a=a[5],b=b[5],out=out[5]);
|
||||
And(a=a[6],b=b[6],out=out[6]);
|
||||
And(a=a[7],b=b[7],out=out[7]);
|
||||
And(a=a[8],b=b[8],out=out[8]);
|
||||
And(a=a[9],b=b[9],out=out[9]);
|
||||
And(a=a[10],b=b[10],out=out[10]);
|
||||
And(a=a[11],b=b[11],out=out[11]);
|
||||
And(a=a[12],b=b[12],out=out[12]);
|
||||
And(a=a[13],b=b[13],out=out[13]);
|
||||
And(a=a[14],b=b[14],out=out[14]);
|
||||
And(a=a[15],b=b[15],out=out[15]);
|
||||
}
|
||||
```
|
||||
|
||||
Instead of a single-bit AND gate that takes two single-bit inputs and produces a
|
||||
single-bit output, the 16-bit AND takes two 16-bit inputs and produces a single
|
||||
16-bit output. Each bit of the output is determined by the AND operation which
|
||||
is executed on each of the input bits.
|
||||
|
||||
Don't be confused by the base-10 numbers: we are still working with binary
|
||||
values however we use denary digits to individuate each bit in the 16-bit
|
||||
number. For example if `a = 10101111`, `a[4]` refers to the fourth bit in `a`
|
||||
counting from the right-hand side (`0`).
|
||||
|
||||
In addition to `And16` I created multi-bit variants of OR, NOT, MUX and DMUX.
|
||||
|
||||
### Multi-way chips
|
||||
|
||||
I also produced _multi-way_ variants of some of the main gates and chips. These
|
||||
versions accept more than the standard one or two input pins but execute the
|
||||
same logic. For example instead of a standard 2-pin input AND gate, a 3-pin
|
||||
input AND gate would take three inputs and produce a `1` output when all three
|
||||
inputs are `1`.
|
||||
|
||||
An example of a multi-way chip that I constructed is OR-8-WAY. This chip outputs
|
||||
1 when any of its 8 inputs is 1. If all inputs are 0, it outputs 0:
|
||||
|
||||
```
|
||||
CHIP Or8Way {
|
||||
IN in[8];
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
Or(a=in[0],b=in[1],out=a);
|
||||
Or(a=a,b=in[2],out=b);
|
||||
Or(a=b,b=in[3],out=c);
|
||||
Or(a=c,b=in[4],out=d);
|
||||
Or(a=d,b=in[5],out=e);
|
||||
Or(a=e,b=in[6],out=f);
|
||||
Or(a=f,b=in[7],out=out);
|
||||
}
|
||||
|
||||
```
|
||||
36
posts/neuron-eolas-frontend.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
title: "Frontend for Eòlas"
|
||||
slug: /neuron-eolas-frontend/
|
||||
date: 2024-10-24
|
||||
tags: ["projects", "log", "eolas"]
|
||||
---
|
||||
|
||||

|
||||
|
||||
I'm now publishing [my zettelkasten](https://github.com/thomasabishop/eolas)
|
||||
publicly at
|
||||
[https://thomasabishop.github.io/eolas/](https://thomasabishop.github.io/eolas/).
|
||||
I wanted to be able to access my notes from different devices and also have them
|
||||
in a nicer format with syntax highlighting and LaTeX support.
|
||||
|
||||
I'm using the [Neuron](https://neuron.zettel.page/) static site generator which
|
||||
is designed specifically zettelkasten-type projects. It requires that the links
|
||||
and some other metadata are in a specific format. However, I wanted the
|
||||
publishing process to be decoupled from my actual notes and not have to make
|
||||
arbitrary changes to suit Neuron.
|
||||
|
||||
So I wrote a simple Python application
|
||||
([neuron-zk-generator](https://github.com/thomasabishop/neuron-zk-generator))
|
||||
that copies the source notes into a specific directory and applies all necessary
|
||||
formatting to this copy. This generator executes locally when I push to the
|
||||
remote and creates a Neuron directory which is then built (via a GitHub Action)
|
||||
and deployed via GitHub pages.
|
||||
|
||||

|
||||
|
||||
<div style="text-align:center">
|
||||
It applies some basic CSS changes and also generates dynamic content for the
|
||||
home page, displaying the build ID, publication date, a page count and the most
|
||||
recently updated files along with an index:
|
||||
|
||||

|
||||
27
posts/new-job.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: "New job"
|
||||
slug: /new-job/
|
||||
date: 2024-11-12
|
||||
tags: ["personal"]
|
||||
---
|
||||
|
||||
After two years and a couple of months I am leaving the BBC for ITV. I enjoyed
|
||||
my time at the BBC. I was fortunate to have a really supportive and personable
|
||||
manager who was great to work with and who took a real interest in my
|
||||
professional development.
|
||||
|
||||

|
||||
|
||||
I worked with a variety of stakeholders on applications used internally within
|
||||
the Corporation. I was able to work across the frontend and the backend in a
|
||||
"full-stack" capacity, enhancing my understanding of AWS serverless, Docker,
|
||||
GraphQL and other backend technologies.
|
||||
|
||||
In my new role I will be working with a similar tech stack also on internal
|
||||
solutions, however this will be purely backend which better suits my technical
|
||||
interests.
|
||||
|
||||
After the BBC, ITV is the second biggest broadcaster in the UK, so I think there
|
||||
will be a lot of overlap. It will be interesting to see how the technical
|
||||
culture differs and I am looking forward to working in a new environment on new
|
||||
projects.
|
||||
127
posts/note-taking-routine.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
title: "Note-taking routine"
|
||||
slug: /note-taking-routine/
|
||||
date: 2024-03-01
|
||||
tags: ["log", "productivity", "zettelkasten", "personal", "eolas"]
|
||||
---
|
||||
|
||||

|
||||
|
||||
I keep my notes in a repository called [Eólas](). Eólas (_awlus_) is an Irish
|
||||
word meaning knowledge or information, especially knowledge gained by experience
|
||||
or practice.
|
||||
|
||||
I have designed it as a technical knowledge management system or "second-brain"
|
||||
comprising notes from my autodidactic study of software engineering and computer
|
||||
science.
|
||||
|
||||
It is a [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten) work in
|
||||
progress. It previously had a hierarchical structure made up of topic-based
|
||||
subdirectories however I have recently converted this into a single flat
|
||||
directory structure organised by tags. I'm in the process of partitioning longer
|
||||
notes into smaller informational units.
|
||||
|
||||
I use the [zk](https://github.com/zk-org/zk) CLI package to help with indexing
|
||||
and task automation alongside its [zk-nvim](https://github.com/zk-org/zk-nvim)
|
||||
Neovim wrapper (my main editor). I occassionally utilise
|
||||
[Obsidian](https://obsidian.md/) alongside Neovim for when I want to view my
|
||||
notes as a knowledge graph or read them alongside their rich content (images,
|
||||
videos etc).
|
||||
|
||||

|
||||
|
||||
## Commands
|
||||
|
||||
On my local machine I have aliased several commands to help me manage the
|
||||
knowledge base:
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Command</th>
|
||||
<th>Output</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>z</code></td>
|
||||
<td><code>cd $HOME/repos/eolas</code></td>
|
||||
<td>Access Zettelkasten</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>zn</code></td>
|
||||
<td><code>zk new --title ...</code></td>
|
||||
<td>Create new entry from template</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> zk</code></td>
|
||||
<td><code>:ZkNotes</code></td>
|
||||
<td>Access Zettelkasten from anywhere within <code>nvim</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> zi</code></td>
|
||||
<td><code>:ZkIndex</code></td>
|
||||
<td>Index Zettelkasten within <code>nvim</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> zt</code></td>
|
||||
<td><code>:ZkTags</code></td>
|
||||
<td>View tags via <a href="https://github.com/nvim-telescope/telescope.nvim">Telescope</a> within <code>nvim</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> ztt</code></td>
|
||||
<td><code>:ObsidianTags</code></td>
|
||||
<td>View tags in a Vim buffer via within <code>nvim</code> using <a
|
||||
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> zl</code></td>
|
||||
<td><code>:ZkLinks</code></td>
|
||||
<td>View links in current entry via Telescope within <code>nvim</code>, using <a
|
||||
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><leader> zb</code></td>
|
||||
<td><code>:ZkBacklinks</code></td>
|
||||
<td>View backlinks to current entry via Telescope within <code>nvim</code>, using <a
|
||||
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Frontmatter
|
||||
|
||||
When I run the `zn` command this generates a new Zettelkasten entry with the
|
||||
following frontmatter template:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: o8yzcrtv
|
||||
title: test
|
||||
tags: []
|
||||
created: Saturday, February 17, 2024 | 17:44
|
||||
---
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
The [scripts](https://github.com/thomasabishop/eolas/tree/master/scripts)
|
||||
directory contains several Bash and Python scripts I use for general
|
||||
housekeeping, such as formatting image URLs, removing unused assets, and
|
||||
autosaving.
|
||||
|
||||
## Autosave
|
||||
|
||||
I use a
|
||||
[bash script](https://github.com/thomasabishop/eolas/blob/master/scripts/auto_save.sh)
|
||||
to create autosave functionality via Git. This script runs every 15 minutes via
|
||||
a `cron` timer. It tidies up the directory (removes unused images, ensures all
|
||||
file names use underscores rather than spaces and hyphens etc) and commits and
|
||||
pushes to GitHub.
|
||||
|
||||
## Usage
|
||||
|
||||
I want to be able to access my notes instantly whatever I am working on so I
|
||||
typically have them open, alongside my knowledge graph and GPT-4 client in a
|
||||
secret [Hyprland](https://hyprland.org/) window overlay (pictured above).
|
||||
70
posts/recent-courses.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
title: "Recent courses"
|
||||
slug: /recent-courses/
|
||||
date: 2023-05-03
|
||||
tags: ["log", "personal"]
|
||||
---
|
||||
|
||||
Below are the recent courses I have completed.
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Course</th>
|
||||
<th>Provider</th>
|
||||
<th>Date completed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/graphql_essential_training_191122.pdf">GraphQL Essential Training</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>19-11-2022</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/apollo_associate_graph_developer_251122.pdf">Graph Developer Associate</a></td>
|
||||
<td>Apollo GraphQL</td>
|
||||
<td>25-11-2022</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_aws_for_developers_031222.pdf">Learning AWS for Developers</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>03-12-2022</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/mysql_essential_training_110123.pdf">MySQL Essential Training</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>11-01-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/nodejs_essential_training_230123.pdf">NodeJS Essential Training</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>23-01-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/python_programming_150223.pdf">Python Programming (3 Day)</a></td>
|
||||
<td>BBC Internal Training</td>
|
||||
<td>15-02-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_bash_scripting_160323.pdf">Learning Bash Scripting</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>16-03-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/git_intermediate_techniques_060423.pdf">Git Intermediate Techniques</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>06-04-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_aws_lambda_210423.pdf">Learning AWS Lambda</a></td>
|
||||
<td>LinkedIn Learning</td>
|
||||
<td>21-04-2023</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/docker_training_260423.pdf">Essential Docker (3 Day)</a></td>
|
||||
<td>BBC Internal Training</td>
|
||||
<td>26-04-2023</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
694
posts/recommended-articles-integration.md
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
---
|
||||
title: "AWS Lambda: recommended articles"
|
||||
slug: /recommended-articles-integration
|
||||
date: 2023-07-07
|
||||
tags: ["log", "project", "aws", "productivity"]
|
||||
---
|
||||
|
||||
I've just added a new feature to the blog: a page that lists links to articles
|
||||
that I have found interesting. Here I will walk through the development process
|
||||
as it is a good example of setting up a simple AWS Lambda and will be useful for
|
||||
future reference.
|
||||
|
||||
## Implementation
|
||||
|
||||

|
||||
|
||||
<div style="margin-top: 1rem"></div>
|
||||
|
||||
I use Pocket to manage my reading list. When I save an article that I want to
|
||||
share to the blog I tag it with `website`. When querying the Pocket API I use
|
||||
this tag to distinguish articles I wish to share from my other saves. I deploy a
|
||||
Lambda function (written in TypeScript) that is accessible through an
|
||||
[AWS API Gateway](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html)
|
||||
endpoint. This function simply sends a fetch request to the Pocket API and
|
||||
returns the response via the Gateway endpoint.
|
||||
|
||||
The Lambda is just a wrapper around the API request - I am not storing any data
|
||||
in a database. Using a Lambda means that I can access my Pocket API credentials
|
||||
securely on the backend and also sidestep the issue of Pocket not allowing CORS
|
||||
requests from a frontend.
|
||||
|
||||
## Backend
|
||||
|
||||
I will use the AWS
|
||||
[Single Application Model](https://aws.amazon.com/serverless/sam/) (SAM) for
|
||||
development. This will allow me to develop and provision the Lambda locally and
|
||||
then deploy my specifications as a template to AWS via the terminal. Once
|
||||
deployed this template will be used by AWS CloudFormation to create a manage the
|
||||
resources I have specified. This simplifies and programatises much of the work
|
||||
involved in creating and running a serverless application on AWS, since you
|
||||
don't have to fiddle too much with different AWS services in the AWS web
|
||||
console.
|
||||
|
||||
### Create Lambda template
|
||||
|
||||
I use the SAM CLI to bootstrap a NodeJS Lambda written in TypeScript:
|
||||
|
||||
```sh
|
||||
sam init --runtime nodejs16.x
|
||||
```
|
||||
|
||||
### Update the defaults
|
||||
|
||||
This creates a basic _hello world_ template that I will adapt for my project. I
|
||||
will also change the naming conventions and file structure as I dislike the
|
||||
defaults.
|
||||
|
||||
My directory stucture is as follows:
|
||||
|
||||
```
|
||||
.
|
||||
├── pocket-api-lambda/
|
||||
│ └── src/
|
||||
│ └── query-pocket/
|
||||
│ ├── index.ts
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ └── tests/
|
||||
└── template.yml
|
||||
|
||||
```
|
||||
|
||||
### Create an IAM role for the Lambda function
|
||||
|
||||
Next I will provision for AWS to create a dedicated IAM role for my function.
|
||||
This is an executive role that will allow my function to access other services
|
||||
on my behalf. At deploy time, AWS will create the role and assign it an Amazon
|
||||
Resource Name (ARN). This is will uniquely identify the function's role within
|
||||
AWS.
|
||||
|
||||
Within the `Resources` object of the YAML template I add:
|
||||
|
||||
```yml
|
||||
QueryPocketFunctionRole:
|
||||
Type: AWS::IAM::Role
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
Policies:
|
||||
- PolicyName: lambda-execution-policy
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "logs:CreateLogGroup"
|
||||
- "logs:CreateLogStream"
|
||||
- "logs:PutLogEvents"
|
||||
Resource: "*"
|
||||
```
|
||||
|
||||
This gives the function basic execution rights. I also need to add the IAM role
|
||||
to the `Outputs` object in the template:
|
||||
|
||||
```yml
|
||||
QueryPocketFunctionIamRole:
|
||||
Description: "IAM Role created for Query Pocket function"
|
||||
Value: !GetAtt QueryPocketFunctionRole.Arn
|
||||
```
|
||||
|
||||
This is necessary for AWS to create a dedicated ARN for the function when the
|
||||
application is deployed.
|
||||
|
||||
### Storing API credentials in AWS Secret Manager
|
||||
|
||||
In order to access the Pocket API you need to send HTTP POST requests to the
|
||||
endpoint with the following credentials supplied in the body:
|
||||
|
||||
```json
|
||||
{
|
||||
"consumer_key": "[consumer_key_value_here]",
|
||||
"access_token": "[access_token_value_here]"
|
||||
}
|
||||
```
|
||||
|
||||
Rather than hard code these credentials in my Lambda code, I will follow best
|
||||
practice and store them in
|
||||
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) which the Lambda
|
||||
will retrieve at runtime. This adds an extra layer of security and encrypts the
|
||||
credentials.
|
||||
|
||||
First I add the two values to Secrets Manager:
|
||||
|
||||

|
||||
|
||||
Then I add a resource permission to the existing `Policies` array for the
|
||||
`QueryPocketFunctionIamRole` to allow the Lambda to access the secret,
|
||||
specifying the ARN of the secret just created:
|
||||
|
||||
```yml
|
||||
- PolicyName: secrets-manager-access
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- secretsmanager:GetSecretValue
|
||||
Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_REF]
|
||||
```
|
||||
|
||||
> I have put redacted private info by using square brackets: this isn't SAM
|
||||
> syntax
|
||||
|
||||
### Accessing credentials
|
||||
|
||||
Now that the function is permitted to access the given secret. I will write a
|
||||
function that will retrieve the credentials from Secrets Manager, which I can
|
||||
invoke from the Lambda:
|
||||
|
||||
```typescript
|
||||
const getPocketCredentials = async (): Promise<PocketCredentials> => {
|
||||
const secretsManager = new AWS.SecretsManager()
|
||||
const response = await secretsManager
|
||||
.getSecretValue({ SecretId: process.env.SECRET_ARN as string })
|
||||
.promise()
|
||||
const secretValues = JSON.parse(response.SecretString as string)
|
||||
|
||||
if (secretValues) {
|
||||
return {
|
||||
accessToken: secretValues.POCKET_ACCESS_TOKEN,
|
||||
consumerKey: secretValues.POCKET_CONSUMER_KEY,
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to return Pocket credentials")
|
||||
}
|
||||
}
|
||||
|
||||
type PocketCredentials = {
|
||||
accessToken: string
|
||||
consumerKey: string
|
||||
}
|
||||
```
|
||||
|
||||
This function invokes the `SecretsManager` module of the `aws-sdk` to retrieve
|
||||
the secret and then exposes each key for destructuring within the main body of
|
||||
the Lambda function. In order to do so, it needs access to the ARN for the given
|
||||
secret which I have set as an environment variable in my `template.yml`:
|
||||
|
||||
```yml
|
||||
...
|
||||
Resources:
|
||||
QueryPocketFunction:
|
||||
...
|
||||
Environment:
|
||||
Variables:
|
||||
SECRET_ARN:
|
||||
arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN]
|
||||
```
|
||||
|
||||
This is great for production but when I am working locally, I don't want to keep
|
||||
making requests to Secrets Manager as this is unnecessary and could become
|
||||
costly. Instead I will utilise environment variables for the different
|
||||
deployment context and reserve calls to Secrets Manager for when the Lambda is
|
||||
executing in production. When working locally, I will source the Pocket
|
||||
credentials from an env file.
|
||||
|
||||
First I create this file at `env/local.env.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"QueryPocketFunction": {
|
||||
"NODE_ENV": "development",
|
||||
"POCKET_CREDENTIALS": "{\"POCKET_ACCESS_TOKEN\": \"[ACCESS_TOKEN_HERE]\",\"POCKET_CONSUMER_KEY\": \"[CONSUMER_KEY_VALUE_HERE]\"}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This sets the Node runtime environment to `development` and ensures that the
|
||||
`POCKET_CREDENTIALS` are sourced from this file rather than Secrets Manager. I
|
||||
will add this file to the `.gitignore` so that it remains local. You'll notice
|
||||
that even though this is JSON, I pass `POCKET_CREDENTIALS` as a string rather
|
||||
than a nested object. This is because environment variables are a feature of the
|
||||
operating system's shell and most OSs treat them as simple strings rather than
|
||||
complex data structures.
|
||||
|
||||
Next I will update the `Environment` field in the `template.yml` so that it sets
|
||||
the Node runtime to `production` when it is deployed. I will also add an empty
|
||||
variable for `POCKET_CREDENTIALS` so that it can be set with the Secrets Manager
|
||||
values in the production context.
|
||||
|
||||
```yml
|
||||
...
|
||||
Resources:
|
||||
QueryPocketFunction:
|
||||
...
|
||||
Environment:
|
||||
Variables:
|
||||
SECRET_ARN:
|
||||
arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN]
|
||||
NODE_ENV: production
|
||||
POCKET_CREDENTIALS: ""
|
||||
```
|
||||
|
||||
Now I need to update the earlier `getPocketCredentials` function to distinguish
|
||||
between deployment environments and source the credentials from each location
|
||||
depending on the runtime:
|
||||
|
||||
```ts
|
||||
const getPocketCredentials = async (): Promise<PocketCredentials> => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const secretsManager = new AWS.SecretsManager()
|
||||
const response = await secretsManager
|
||||
.getSecretValue({ SecretId: process.env.SECRET_ARN as string })
|
||||
.promise()
|
||||
const secretValues = JSON.parse(response.SecretString as string)
|
||||
|
||||
if (secretValues) {
|
||||
return {
|
||||
accessToken: secretValues.POCKET_ACCESS_TOKEN,
|
||||
consumerKey: secretValues.POCKET_CONSUMER_KEY,
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to return Pocket credentials")
|
||||
}
|
||||
} else {
|
||||
const localCredentials = JSON.parse(
|
||||
process.env.POCKET_CREDENTIALS as string
|
||||
)
|
||||
if (localCredentials) {
|
||||
return {
|
||||
accessToken: localCredentials.POCKET_ACCESS_TOKEN,
|
||||
consumerKey: localCredentials.POCKET_CONSUMER_KEY,
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to return Pocket credentials")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now when I run `sam build && sam local start-api` it will run the production
|
||||
version, sourcing the credentials from Secrets Manager. When I run the same
|
||||
command but specify the local environment variables, it will bypass Secrets
|
||||
Manager and get the credentials from the JSON file:
|
||||
|
||||
```sh
|
||||
sam build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json
|
||||
```
|
||||
|
||||
This command is a bit unwieldy to paste everytime, so I will write a Makefile
|
||||
that stores it:
|
||||
|
||||
```
|
||||
.PHONY: clean build
|
||||
|
||||
clean:
|
||||
rm -rf .aws-sam
|
||||
|
||||
build:
|
||||
sam build
|
||||
|
||||
start-local:
|
||||
make build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json
|
||||
|
||||
```
|
||||
|
||||
Now I can just run `make start-local` to get the local API endpoint up and
|
||||
running.
|
||||
|
||||
### Lambda handler function
|
||||
|
||||
Finally I will write the actual handler function:
|
||||
|
||||
```ts
|
||||
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
||||
let response: APIGatewayProxyResult
|
||||
const endpoint = `https://getpocket.com/v3/get`
|
||||
|
||||
try {
|
||||
const { accessToken, consumerKey } = await getPocketCredentials()
|
||||
const tag = event.queryStringParameters?.tag
|
||||
const responseHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
consumer_key: consumerKey,
|
||||
access_token: accessToken,
|
||||
state: "all",
|
||||
tag: tag,
|
||||
}
|
||||
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF8",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}
|
||||
|
||||
const request: Response = await fetch(endpoint, options)
|
||||
const requestJson: any = await request.json()
|
||||
|
||||
response = {
|
||||
statusCode: 200,
|
||||
headers: responseHeaders,
|
||||
body: JSON.stringify({
|
||||
data: requestJson,
|
||||
}),
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error(err)
|
||||
response = {
|
||||
statusCode: 500,
|
||||
header: responseHeaders
|
||||
body: JSON.stringify({
|
||||
message: err instanceof Error ? err.message : "some error happened",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
```
|
||||
|
||||
I'm sending a POST request to the Pocket API endpoint. If successful, I return
|
||||
the data as the response, else I return an error code. You'll notice I am
|
||||
retrieving `queryStringParameter` from the `event` object. This means I can pass
|
||||
in different tags later, in case I want to retrieve different articles.
|
||||
|
||||
Locally, I would call the endpoint using the following URL:
|
||||
`http://127.0.0.1:3000/query-pocket/get-articles-by-tag?tag=website`
|
||||
|
||||
### The template file in full
|
||||
|
||||
Now the configuration is complete and the Lambda is written let's take a look at
|
||||
the final `template.yml`:
|
||||
|
||||
```yml
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
Description: >
|
||||
pocket-api-lambda
|
||||
Globals:
|
||||
Function:
|
||||
Timeout: 10
|
||||
MemorySize: 256
|
||||
Resources:
|
||||
QueryPocketFunctionRole:
|
||||
Type: AWS::IAM::Role
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
Policies:
|
||||
- PolicyName: lambda-execution-policy
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "logs:CreateLogGroup"
|
||||
- "logs:CreateLogStream"
|
||||
- "logs:PutLogEvents"
|
||||
Resource: "*"
|
||||
- PolicyName: secrets-manager-access
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- secretsmanager:GetSecretValue
|
||||
Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID]
|
||||
QueryPocketFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
CodeUri: src/query-pocket/
|
||||
Role: !GetAtt QueryPocketFunctionRole.Arn
|
||||
Handler: index.handler
|
||||
Runtime: nodejs16.x
|
||||
Architectures:
|
||||
- x86_64
|
||||
Events:
|
||||
QueryPocketApi:
|
||||
Type: Api
|
||||
Properties:
|
||||
Path: /query-pocket/get-articles-by-tag
|
||||
Method: get
|
||||
Environment:
|
||||
Variables:
|
||||
SECRET_ARN: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID]
|
||||
NODE_ENV: production
|
||||
POCKET_CREDENTIALS: ""
|
||||
Metadata:
|
||||
BuildMethod: esbuild
|
||||
BuildProperties:
|
||||
Minify: true
|
||||
Target: "es2020"
|
||||
Sourcemap: true
|
||||
EntryPoints:
|
||||
- index.ts
|
||||
Outputs:
|
||||
QueryPocketApi:
|
||||
Description:
|
||||
"API Gateway endpoint URL for Prod stage for Query Pocket function"
|
||||
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/query-pocket/get-articles-by-tag"
|
||||
QueryPocketFunction:
|
||||
Description: "Query Pocket Lambda Function ARN"
|
||||
Value: !GetAtt QueryPocketFunction.Arn
|
||||
QueryPocketFunctionIamRole:
|
||||
Description: "IAM Role created for Query Pocket function"
|
||||
Value: !GetAtt QueryPocketFunctionRole.Arn
|
||||
```
|
||||
|
||||
To summarise, and cover the parts of the template I have not already explained:
|
||||
|
||||
- I am creating provisions for three entities:
|
||||
|
||||
- A Lambda function
|
||||
- An API Gateway enpoint that will trigger the function
|
||||
- An IAM role for the Lambda function
|
||||
|
||||
- In addition I am setting permissions so that my Lambda function can access a
|
||||
value stored in Secrets Manager, along with associated environment variables.
|
||||
|
||||
- I have a single endpoint: `/query-pocket/get-articles-by-tag`. I have given it
|
||||
it's own path rather than just using the root `/query-pocket` because I may
|
||||
add additional endpoints in the future
|
||||
|
||||
- My Lambda function is defined in `/query-pocket/index.ts`
|
||||
|
||||
Now when I start the server locally and call the endpoint from Postman I can see
|
||||
the data returned:
|
||||
|
||||

|
||||
|
||||
### Deploying the function
|
||||
|
||||
I start by validating my template to ensure that I do not introduce problems at
|
||||
the outset of the deployment:
|
||||
|
||||
```
|
||||
sam validate
|
||||
/home/thomas/repos/lambdas/pocket-api-lambda/template.yaml is a valid SAM Template
|
||||
```
|
||||
|
||||
Then I run a build:
|
||||
|
||||
```
|
||||
sam build
|
||||
```
|
||||
|
||||
Next I package the application. CloudFront can only receive one file as an
|
||||
input. When we package the application we create this single file. The packaging
|
||||
proces will first archive all of the project artefacts into a zip file and then
|
||||
upload that to an [AWS S3](https://aws.amazon.com/s3/) bucket. A reference to
|
||||
this S3 entity is then provided to CloudFormation and used to retrieve the
|
||||
requisite artefacts.
|
||||
|
||||
```
|
||||
sam package
|
||||
--template-file template.yaml
|
||||
--output-template-file pkg.yaml
|
||||
--region eu-west-2
|
||||
```
|
||||
|
||||
This outputs the following, confirming the creation of the zipped resources:
|
||||
|
||||
```
|
||||
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xc8swn6xu4km
|
||||
A different default S3 bucket can be set in samconfig.toml
|
||||
Or by specifying --s3-bucket explicitly.
|
||||
Uploading to 67c241b6f81524ee092adab3ba9998c5 50691677 / 50691677 (100.00%)
|
||||
|
||||
Successfully packaged artifacts and wrote output template to file pkg.yaml.
|
||||
Execute the following command to deploy the packaged template
|
||||
sam deploy --template-file /home/thomas/repos/lambdas/pocket-api-lambda/pkg.yaml --stack-name <YOUR STACK NAME>
|
||||
```
|
||||
|
||||
Now that we have our packaged application, the last step is to deploy it:
|
||||
|
||||
```
|
||||
sam deploy --guided
|
||||
```
|
||||
|
||||
This presents me with options for the deployment defaults. The most important
|
||||
one is `Allow SAM CLI IAM role creation`. By affirming this, AWS will create the
|
||||
`QueryPocketFunctionIamRole` and generate an ARN for it.
|
||||
|
||||
This takes a few minutes but the CLI will keep you up to date with the
|
||||
deployment process. Then, if successful, it will confirm the resources it has
|
||||
created:
|
||||
|
||||

|
||||
|
||||
If I go to CloudFormation in the AWS I will see that the `pocket-api-lambda`
|
||||
stack now exists:
|
||||
|
||||

|
||||
|
||||
And a new function is added to AWS Lambda with the API Gateway endpoint as the
|
||||
trigger:
|
||||
|
||||

|
||||
|
||||
And in API Gateway the API has also been set up:
|
||||
|
||||

|
||||
|
||||
## Frontend
|
||||
|
||||
My frontend is built with the GatsbyJS library and so is React-based.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Early I took to trouble to run both development and production versions of the
|
||||
API. When I am working locally on my site I want to be querying the local
|
||||
endpoint rather than the production version since this will accrue fees and also
|
||||
requires me to deploy after every change.
|
||||
|
||||
I will use an environment variable to distinguish the local and production
|
||||
endpoints. I do this using the [dotenv](https://www.npmjs.com/package/dotenv)
|
||||
Node package and create the following two files:
|
||||
|
||||
```sh
|
||||
# .env.production
|
||||
GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=https://[HASH].execute-api.eu-west-2.amazonaws.com/Prod/query-pocket/get-articles-by-tag
|
||||
```
|
||||
|
||||
```sh
|
||||
# .env.development
|
||||
GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=http://127.0.0.1:3000/query-pocket/get-articles-by-tag
|
||||
```
|
||||
|
||||
By prepending the variable name with `GATSBY`, Gatsby will know to inject these
|
||||
values at runtime. This way, and by adding my `.env` files to the `.gitignore`,
|
||||
I can avoid hardcoding my endpoints and can use a single reference in my React
|
||||
component.
|
||||
|
||||
When working locally I can run `NODE_ENV=development npm run start` to source
|
||||
the local endpoint variable and `NODE_ENV=production npm run start` to source
|
||||
the production endpoint variable. In the deployed production context however,
|
||||
`.env.production` will not be available because it is an ignored file. Thus to
|
||||
source the variables when `npm run build` is run remotely, it is necessary to
|
||||
store them as secrets that can be accessed in my CI/CD pipeline. I use a
|
||||
[GitHub Action](https://systemsobscure.blog/how-I-deploy-this-site/) to build my
|
||||
site and deploy it to AWS when I push changes, so I add the following line to my
|
||||
`main.file` build script:
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV
|
||||
echo "GATSBY_METRICS_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_METRICS_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
### React component
|
||||
|
||||
Below is the React component:
|
||||
|
||||
```jsx
|
||||
const ENDPOINT = process.env.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT
|
||||
|
||||
const ArticleListing = ({ article }) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<a href={article?.resolved_url} target="_blank">
|
||||
{article?.resolved_title}
|
||||
</a>
|
||||
</td>
|
||||
<td>{formatUnixTimestamp(article?.time_added)}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RecommendedArticlesPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await axios.get(`${ENDPOINT}?tag=website`)
|
||||
setData(response?.data?.data?.list)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const articles = Object.keys(data).map((key) => data[key])
|
||||
return (
|
||||
<Main>
|
||||
<PageHeader headerTitle="Recommended articles" />
|
||||
<p>
|
||||
Articles written by others that I have learned from or which present
|
||||
interesting viewpoints.
|
||||
</p>
|
||||
|
||||
<table className="articles-table">
|
||||
<thead className={loading ? "loading" : ""}>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Date added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{articles?.map((article) => (
|
||||
<ArticleListing key={article?.item_id} article={article} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Here I source my environment variable from the Node runtime context and use it
|
||||
to query the API via the React `useEffect` hook that runs once on load. Then I
|
||||
loop through the data to populate the rows of an HTML table.
|
||||
|
||||
## Addendum: authentication
|
||||
|
||||
I have set up a basic AWS Gateway API with two execution contexts (local and
|
||||
production) that trigger a Lambda function. In closing I want to note that a
|
||||
considerable oversight is the lack of authentication for the remote endpoint. In
|
||||
its current state anyone with access to the production URL can call the API.
|
||||
|
||||
In this context it's not that big of a deal since the information is not
|
||||
sensitive or particularly interesting however if I was following best practice I
|
||||
would need to provide a way for AWS to authenticate requests. I did attempt this
|
||||
using an IAM role and an AWS Cognito user pool however I was unable to get this
|
||||
working despite my best efforts. This is something I will return to in a future
|
||||
post.
|
||||
23
posts/requiem-for-gruvbox-95.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: "Requiem for Gruvbox95"
|
||||
slug: /requiem-for-gruvbox-95/
|
||||
date: 2024-07-31
|
||||
tags: ["projects", "gruvbox", "beige"]
|
||||
---
|
||||
|
||||

|
||||
|
||||
Back in April I was working on a full system rice. It was going to be called
|
||||
_Gruvbox95_ and would basically apply the aesthetics of Windows95/98 to the
|
||||
Hyprland window manager but with the seductive beige of the Gruvbox light theme.
|
||||
|
||||
Alas I was ultimately defeated by box-shadow and my own limitations but I came
|
||||
across a screenshot when I was clearing out my phone.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
38
posts/revision-bash-script.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: "Bash script: random revision topic"
|
||||
slug: /revision-bash-script
|
||||
date: 2022-12-28
|
||||
tags: ["log", "productivity", "bash"]
|
||||
---
|
||||
|
||||
I keep all my study notes in Markdown format in a
|
||||
[single repository]('https://github.com/thomasabishop/computer_science').
|
||||
Because I study a lot of varied topics it can sometimes seem that I am just
|
||||
writing notes and forgetting about them. This is particularly true of topics
|
||||
that I don't draw on in my everyday work.
|
||||
|
||||
As a corrective I have written a script at the root of my notes repository that
|
||||
selects a random revision topic from the categories I specify. When I start
|
||||
studying each morning (I study from 6am-8am before work) I run the script and
|
||||
spend ten minutes consolidating the topic it selects. Sometimes this will just
|
||||
mean re-reading the notes but often I will rewrite or add ideas that I have
|
||||
gleaned in the period since I originally studied the topic, extending my
|
||||
understanding of the subject matter.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Choose source directories...
|
||||
DIRS_TO_PARSE="../Computer_Architecture ../Electronics_and_Hardware ../Operating_Systems ../Programming_Languages/Shell ../Logic"
|
||||
|
||||
# Return array of all files belonging to source dirs...
|
||||
for ele in $DIRS_TO_PARSE; do
|
||||
FILE_MATCHES+=( $(find $ele -name "\*.md" -type f) )
|
||||
done
|
||||
|
||||
# Generate a random integer between 0 and the match array length...
|
||||
RANDOM_FILE_INDEX=$(( $RANDOM % ${#FILE_MATCHES[@]} + 0 ))
|
||||
|
||||
# Return file matching that index...
|
||||
echo "Revise this topic: ${FILE_MATCHES[$RANDOM_FILE_INDEX]}"
|
||||
```
|
||||
27
posts/save-articles-lambda.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: "AWS Lambda: save articles"
|
||||
slug: /save-articles-lambda
|
||||
date: 2023-09-07
|
||||
tags: ["log", "project", "productivity", "aws", "python"]
|
||||
---
|
||||
|
||||
I've just finished a new
|
||||
[AWS Lambda](https://github.com/thomasabishop/lambdas/tree/main/save-articles)
|
||||
written in Python. It retrieves entries from my saved articles in Pocket, parses
|
||||
the key metadata and saves it to a Google Sheet.
|
||||
|
||||
I've done this because I find the Pocket app to be really uncongenial and
|
||||
confusing to use. However I've used Pocket for a long time and the alternatives
|
||||
are worse or have bad APIs.
|
||||
|
||||
Now I keep my interactions with Pocket minimal. I use the Pocket browser
|
||||
extension only to save and tag articles. My lambda function retrieves articles
|
||||
by tag and saves them to a Google Sheet. This allows me to access the articles
|
||||
from anywhere without using a database.
|
||||
|
||||
Each tag has a corresponding sheet that lists the articles in reverse
|
||||
chronological order. The lambda executes on a cron timer once a day.
|
||||
|
||||
Here are my saved technical articles, for example:
|
||||
|
||||

|
||||
319
posts/self-hosting-1-initial-setup.md
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
---
|
||||
title: "Self-hosting: initial setup"
|
||||
slug: /self-hosting-1-initial-setup/
|
||||
date: 2025-02-23
|
||||
tags: ["projects", "self-hosting"]
|
||||
---
|
||||
|
||||
With the way the Internet (and the world) is going, the benefits of self-hosting
|
||||
the services you rely on is becoming increasingly apparent.
|
||||
|
||||
I have been using AWS to run a limited set microservices on the Free Tier but
|
||||
doing anything interesting with an actual server and database becomes expensive
|
||||
quickly. While I hope to eventually run my own physical server, for now I have
|
||||
opted to maintain a remote virtual private server. The skills I gain here will
|
||||
obviously transfer to the physical context.
|
||||
|
||||
I intend to run a series of third-party FOSS applications on different
|
||||
subdomains via Docker containers. I will also be deploying my own software using
|
||||
Docker.
|
||||
|
||||
This post goes over the initial setup and configuration of the server.
|
||||
|
||||
## Choosing a provider
|
||||
|
||||
Jurisdiction, location and price were the main considerations. I want the server
|
||||
to be based in a country with good privacy laws but not so far away that latency
|
||||
would be noticeable. I also don't want to pay that much initially given that I
|
||||
am just starting out.
|
||||
|
||||

|
||||
|
||||
I opted for the German company, Hetzner. Being in the EU means their digital
|
||||
privacy laws are better than the UK and Germany's recent history has created
|
||||
more of a culture of resistance to surveillance than other EU states.
|
||||
|
||||
I chose their CX22 Cloud package which provides the following for just under €4
|
||||
a month:
|
||||
|
||||
- 2 virtual CPUs (Intel)
|
||||
- 4 GB RAM
|
||||
- 40 GB SSD
|
||||
- IPv4 and free IPv6
|
||||
|
||||
This is modest but given that it's virtual, I can scale up later as necessary.
|
||||
|
||||

|
||||
|
||||
## Creating a server
|
||||
|
||||
Once you've paid you get access to the Hetzner Cloud Console and you just click
|
||||
"Add server" and specify the location, hostname and OS. I opted for Nuremberg as
|
||||
it's nearest and decided on Debian as my Linux distro.
|
||||
|
||||
I set up a basic firewall using the Hetzner console, allowing incoming traffic
|
||||
on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). This will apply at the Network
|
||||
Layer. In a moment I'll explain how I also created a firewall at the Application
|
||||
Layer, over SSH.
|
||||
|
||||
## Connecting
|
||||
|
||||
Once the server was spun up the obvious next step was to connect to it and start
|
||||
configuring from the inside. To do this I needed SSH access. This was
|
||||
straightforward, I just added my local machine's public key via the Hetzner
|
||||
console and then connected with:
|
||||
|
||||
```sh
|
||||
ssh root@<server_ip4_address>
|
||||
```
|
||||
|
||||
## Create non-root user
|
||||
|
||||
Once I had access as root the first thing I did was update my packages:
|
||||
|
||||
```sh
|
||||
apt update
|
||||
apt upgrade
|
||||
```
|
||||
|
||||
Next I created a non-root user with `sudo` privileges. This is the account I
|
||||
will use for administrating the server which is obviously safer than doing stuff
|
||||
as root:
|
||||
|
||||
```sh
|
||||
apt install sudo
|
||||
add user <my_username>
|
||||
usermod -aG sudo <my_username>
|
||||
```
|
||||
|
||||
I then switched to this user:
|
||||
|
||||
```sh
|
||||
su - <my_username>
|
||||
```
|
||||
|
||||
I'm connecting to the server over SSH but I also want to login as `my_username`
|
||||
using SSH keys instead of a password as this is less secure.
|
||||
|
||||
I set up the necessary SSH directory and permissions for `my_username`:
|
||||
|
||||
```sh
|
||||
mkdir /home/<my_username>/.ssh
|
||||
chown <my_username>:<my_username> /home/<my_username>/.ssh
|
||||
chmod 700 /home/<my_username>/.ssh
|
||||
```
|
||||
|
||||
Then I transferred my local machine's public key, that I added to the server via
|
||||
the Hetzner Console earlier, to the known hosts for `my_username`.
|
||||
|
||||
```sh
|
||||
cp /root/.ssh/authorized_keys /home/<my_username>/.ssh/
|
||||
chown <my_username>:<my_username> /home/<my_username>/.ssh/authorized_keys
|
||||
chmod 600 /home/<my_username>/.ssh
|
||||
```
|
||||
|
||||
To confirm, I closed the connection and then connected to the server again but
|
||||
this time as `my_username`:
|
||||
|
||||
```sh
|
||||
ssh <my_username>@<server_ip4_address>
|
||||
```
|
||||
|
||||
Having confirmed SSH login I installed a lightweight version of vim
|
||||
([vim-tiny](https://www.baeldung.com/linux/vim-tiny-properties)) to make
|
||||
text-editing easier.
|
||||
|
||||
```sh
|
||||
sudo apt install vim-tiny
|
||||
```
|
||||
|
||||
## Disable root login and access
|
||||
|
||||
An important server-hardening routine is to prevent password logins as root.
|
||||
|
||||
To do this I edited the root SSH config:
|
||||
|
||||
```sh
|
||||
sudo vim /etc/sshd_config
|
||||
```
|
||||
|
||||
And added the following:
|
||||
|
||||
```
|
||||
PasswordAuthentication no
|
||||
```
|
||||
|
||||
Then I restarted the SSH daemon:
|
||||
|
||||
```
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
Then when I attempted `ssh root@<server_ip4_address>` I was met with "Permission
|
||||
denied", as intended.
|
||||
|
||||
As I side note, it's actually not possible for me to login as root at all, which
|
||||
is best practice. I haven't set a root password, so if I attempt to login as
|
||||
root when logged in as `my_username` (with `su -`), all attempts will fail.
|
||||
|
||||
When I need to run processes as root, I will do this exclusively by assuming
|
||||
`sudo` and to do this, I need to enter the password for `my_username`.
|
||||
|
||||
## Firewall
|
||||
|
||||
Earlier, I set up a firewall from outside of the server, using the Hetzner
|
||||
console. I'm now going to do the same thing but from within the server as
|
||||
`my_username`. It will share the same rules. This means I have a firewall at the
|
||||
Network Layer (Hetzner) blocking requests before they reach the server and a
|
||||
firewall operating at the Application Layer for those requests that get to the
|
||||
server.
|
||||
|
||||
This doesn't add much in terms of security, especially as the rules are
|
||||
identical, but having a peripheral firewall on the Network and a firewall on the
|
||||
server provides a degree of "defence in depth". It will also potentially reduce
|
||||
the load on the server if blocked requests are deflected at the Network Layer
|
||||
before reaching the server.
|
||||
|
||||
It would be better to block by IP, and only open the SSH port to requests from
|
||||
my local network (HTTP/HTTPS is obviously public and open to all). This isn't an
|
||||
option because my my broadband package doesn't include a static IP. One way
|
||||
around this would be to deploy another Hetzner server and set it up as a VPN. I
|
||||
would then restrict port 22 to requests from that server's IP and connect to the
|
||||
VPN server first. This might be a project further down the line.
|
||||
|
||||
To set up the host firewall I used `ufw` (Uncomplicated Firewall):
|
||||
|
||||
```sh
|
||||
sudo apt install ufw
|
||||
```
|
||||
|
||||
This is a wrapper for the venerable `iptables` that simplifies the process.
|
||||
|
||||
I applied the same basic rules as earlier:
|
||||
|
||||
```sh
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw allow 22/tcp
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
To confirm:
|
||||
|
||||
```sh
|
||||
sudo ufw status
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
To Action From
|
||||
-- ------ ----
|
||||
22/tcp ALLOW Anywhere
|
||||
80/tcp ALLOW Anywhere
|
||||
443/tcp ALLOW Anywhere
|
||||
22/tcp (v6) ALLOW Anywhere (v6)
|
||||
80/tcp (v6) ALLOW Anywhere (v6)
|
||||
443/tcp (v6) ALLOW Anywhere (v6)
|
||||
|
||||
```
|
||||
|
||||
(This automatically applies the rules for IPv6 as well as IPv4.)
|
||||
|
||||
## Fail2Ban
|
||||
|
||||
As an additional security measure I installed `fail2ban`. This software is
|
||||
designed to work alongside your firewall to detect and block brute-force login
|
||||
attempts. You apply it to services running on specific ports at the Application
|
||||
Layer.
|
||||
|
||||
Debian does not come with the `rsyslog` package installed and `fail2ban` works
|
||||
best with this logging program rather than `systemd` and its `journalctl` logs.
|
||||
I tried to get it working with `systemd` but kept facing errors I couldn't
|
||||
resolve, so I just installed `rsyslog` along with `fail2ban`:
|
||||
|
||||
```sh
|
||||
sudo apt install fail2ban
|
||||
sudo apt install rsyslog
|
||||
|
||||
sudo systemctl start rsyslog
|
||||
sudo systemctl enable rsyslog
|
||||
sudo systemctl start fail2ban
|
||||
sudo systemctl enable fail2ban
|
||||
```
|
||||
|
||||
I didn't change any of the `fail2ban` defaults. On Debian it runs on the `sshd`
|
||||
server automatically and this is the only service I have running currently.
|
||||
|
||||
I waited a while and then checked the status:
|
||||
|
||||
```sh
|
||||
sudo fail2ban-client status sshd
|
||||
```
|
||||
|
||||
Which gave me:
|
||||
|
||||
```
|
||||
Status for the jail: sshd
|
||||
|- Filter
|
||||
| |- Currently failed: 2
|
||||
| |- Total failed: 55
|
||||
| `- File list: /var/log/auth.log
|
||||
`- Actions
|
||||
|- Currently banned: 0
|
||||
|- Total banned: 6
|
||||
`- Banned IP list:
|
||||
|
||||
```
|
||||
|
||||
The IP addresses of actors that make repeated failed login attempts are put in a
|
||||
temporary "jail" by `fail2ban` which means they are blocked for a limited time
|
||||
period. The data above tells me that there have been 55 total attempted failed
|
||||
logins and this has resulted in 6 IPs being put in jail! However this is just
|
||||
normal background noise in serverland and nothing to particularly worry about.
|
||||
|
||||
When I take a look in `var/log/auth.log`, I see entries like:
|
||||
|
||||
```
|
||||
Disconnected from authenticating user root 218.92.0.236 port 22272 [preauth]
|
||||
|
||||
```
|
||||
|
||||
This indicates a malicious actor trying gain access as root but being blocked by
|
||||
the earlier root protections.
|
||||
|
||||
There are also entries like this:
|
||||
|
||||
```
|
||||
Connection closed by invalid user postgres 195.211.190.228 port 47454 [preauth]
|
||||
|
||||
```
|
||||
|
||||
Which shows a speculative attempt to try and connect to a non-existing
|
||||
PostgreSQL service.
|
||||
|
||||
These will typically be bots trying their luck at scale rather than actual
|
||||
people.
|
||||
|
||||
Alongside the malicious attempts I see my own legimate logins and invocations of
|
||||
root as `sudo`:
|
||||
|
||||
```
|
||||
pam_unix(sudo:session): session opened for user root(uid=0) by <my_username>(uid=1000)
|
||||
```
|
||||
|
||||
This was enough for one Sunday! In my next post I'll detail how I set up DNS and
|
||||
SSL for the new server.
|
||||
|
||||
## Resources
|
||||
|
||||
In researching how to do the above, I created or expanded the following entries
|
||||
in my Zettelkasten:
|
||||
|
||||
- [Firewalls](https://thomasabishop.github.io/eolas/Firewalls)
|
||||
- [UFW firewall management](https://thomasabishop.github.io/eolas/UFW_firewall_management)
|
||||
- [Disable non-root SSH access](https://thomasabishop.github.io/eolas/Disable_non-root_ssh_access)
|
||||
- [IP addresses](https://thomasabishop.github.io/eolas/IP_addresses)
|
||||
- [Internet fundamentals](https://thomasabishop.github.io/eolas/Internet_fundamentals)
|
||||
- [The Application Layer of the Internet Protocol](https://thomasabishop.github.io/eolas/Application_Layer_of_Internet_Protocol)
|
||||
118
posts/self-hosting-2-dns-tls.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
title: "Self-hosting: setting up DNS and TLS"
|
||||
slug: /self-hosting-2-dns-tls/
|
||||
date: 2025-03-03
|
||||
tags: ["projects", "self-hosting"]
|
||||
---
|
||||
|
||||
In my [previous post](./self-hosting-1-initial-setup.md) I described how I
|
||||
purchased my VPS package, initialised a server and applied some basic security
|
||||
safeguards. In this post I'll record how I set up DNS and the certification
|
||||
necessary to enable encrypted client-server communication over HTTPS.
|
||||
|
||||
## DNS
|
||||
|
||||
As it stands, my server exists on the Internet but can only be reached via its
|
||||
IPv4 address. This is sufficient to connect via SSH but eventually I want to
|
||||
host HTTP services and for this, I'll need a domain name.
|
||||
|
||||
I purchased the domain `systemsobscure.net` from Namecheap. I'm not mad about
|
||||
using a US company but it is indeed cheap and has a better record when it comes
|
||||
to privacy than its main competitor, Cloudflare.
|
||||
|
||||
Once purchased, I needed to set up the A Records that associate my server's IP
|
||||
address with the domain name:
|
||||
|
||||

|
||||
|
||||
`@` sets the root domain. `*` is a wildcard that represents all subdomains at
|
||||
the root level. This means I can dedicate specific services to different
|
||||
subdomains on the same server, e.g _grafana.systemsobscure.net_,
|
||||
_wallabag.systemsobscure.net_ etc.
|
||||
|
||||
I've done this for both my IPv4 address and IPv6 addresses (pixelated).
|
||||
|
||||
The DNS resolution propagated very quickly and now instead of using the IP, I
|
||||
can SSH with the domain name, e.g:
|
||||
|
||||
```sh
|
||||
ssh my_username@systemsobscure.net
|
||||
```
|
||||
|
||||
## TLS certification
|
||||
|
||||
Next I needed to generate a TLS certificate for the domain. Certificates provide
|
||||
a mechanism for independently verifying domain ownership. This serves as a
|
||||
safeguard against impersonation and man-in-the-middle attacks. (Imagine if
|
||||
someone was able to impersonate the web address of your online banking provider;
|
||||
they could intercept and steal your account details.)
|
||||
|
||||
Establishing trust through a certificate is a prerequisite for the initiation of
|
||||
encrypted communication between client and server. Once the client has validated
|
||||
the server's certificate, it can use the server's public cryptographic key
|
||||
(supplied with the certificate) to create a shared cipher that will encrypt the
|
||||
subsequent HTTP messages.
|
||||
|
||||
The process essentially works as follows. A client will request a resource on my
|
||||
server. The server will offer its TLS certificate which contains its public key.
|
||||
The client will confirm the certificate is valid and issued by a reputable
|
||||
Certificate Authority. It will then use the public key to send the server an
|
||||
encrypted message. This will be decrypted by the server (using its private key)
|
||||
and used to create the shared key that will encrypt communication between the
|
||||
two hosts.
|
||||
|
||||
All of this happens silently at the Transport Layer - a layer lower in the
|
||||
network stack that the actual applications running over HTTPS (TLS stands for
|
||||
_Transport Layer Security_). You only become aware of it when your browser
|
||||
alerts you to the fact that a website has an invalid certificate or is using
|
||||
unencrypted HTTP.
|
||||
|
||||
Thankfully, this algorithmic complexity is not the immediate concern of the
|
||||
server administrator! You can use a tool called `certbot` to generate the
|
||||
certificate and prove your domain ownership in a matter of minutes.
|
||||
|
||||
[certbot](https://certbot.eff.org/) is a free and open-source ACME client. ACME
|
||||
stands for _Automatic Certificate Management Environment_. It generates the
|
||||
initial certificate and automatically renews it after 90 days.
|
||||
|
||||
I installed `certbot`:
|
||||
|
||||
```sh
|
||||
sudo apt install certbot
|
||||
```
|
||||
|
||||
Then generated the certificate:
|
||||
|
||||
```sh
|
||||
sudo certbot certonly --stanalone -d systemsobscure.net
|
||||
```
|
||||
|
||||
This command does quite a lot. It creates a temporary web server on port 80 and
|
||||
then sends a request to [Let's Encrypt](https://letsencrypt.org/) (the body
|
||||
responsible for generating the certificates), asking for a "challenge" to prove
|
||||
domain ownership.
|
||||
|
||||
Let's Encrypt will send a token to `certbot` and `certbot` will dutifully place
|
||||
the token in a file that is served at a URL over port 80. Let's Encrypt will
|
||||
then make an HTTP request to this URL ( e.g
|
||||
`http://systemsobscure.net/.well-known/acme-challenge/the-token`) confirming
|
||||
ownership and conferring the certificate.
|
||||
|
||||
`certbot` saves the certificate (and several associated cryptographic tokens) to
|
||||
the `/etc/letsencrypt/live` directory.
|
||||
|
||||

|
||||
|
||||
As I currently don't have any services running on HTTP, the certificates won't
|
||||
actually be used yet, but they are ready to go. In my next post I will finally
|
||||
create my first self-hosted service which can now be served using my dedicated
|
||||
domain name over HTTPS!
|
||||
|
||||
## Resources
|
||||
|
||||
In researching how to do the above, I created or expanded the following entries
|
||||
in my Zettelkasten:
|
||||
|
||||
- [Certificates and Certificate Authorities](https://thomasabishop.github.io/eolas/Certificate_authorities)
|
||||
- [Let's Encrypt](https://thomasabishop.github.io/eolas/Let's_Encrypt)
|
||||
- [HTTPS](https://thomasabishop.github.io/eolas/HTTPS)
|
||||
479
posts/self-hosting-3-reverse-proxy-grafana.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
---
|
||||
title: "Self-hosting: setting up reverse proxy and first service"
|
||||
slug: /self-hosting-3-reverse-proxy-grafana/
|
||||
date: 2025-03-12
|
||||
tags: ["projects", "self-hosting"]
|
||||
---
|
||||
|
||||
In my [previous post](https://systemsobscure.blog/self-hosting-2-dns-tls/) in
|
||||
the [series](link_to_tag) I explained how I configured the DNS settings for the
|
||||
server and set up its TLS certificate. I am now in a position to start hosting
|
||||
services.
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
There are three core components to my self-hosting architecture:
|
||||
|
||||
- a public subdomain
|
||||
- a reverse proxy
|
||||
- software running in Docker containers
|
||||
|
||||
I will use this structure for the majority of my services. Let me explain each
|
||||
part.
|
||||
|
||||
### Subdomain
|
||||
|
||||
This is the easiest bit to understand. A subdomain is just an address on a
|
||||
larger server that groups together a set of related processes or resources. For
|
||||
example, on my server, _service-a_ and _service-b_ would be accessible at the
|
||||
following subdomains:
|
||||
|
||||
- `service-a.systemsobscure.net`
|
||||
- `service-b.systemsobscure.net`
|
||||
|
||||
To access a given resource over the public Internet, it needs to be reachable
|
||||
via a URL - subdomains provide this in a clear, hierarchical manner.
|
||||
|
||||
Back when I set up the
|
||||
[DNS records](https://systemsobscure.blog/self-hosting-2-dns-tls/) for the
|
||||
server, I created an A Record with the wildcard character (`*`). This means that
|
||||
I can create multiple subdomains and have them be served off of the main
|
||||
`systemsobscure` domain.
|
||||
|
||||
### Reverse proxy
|
||||
|
||||
The subdomain is the public address of the resource or service. Clients will
|
||||
send HTTP requests to this address and to handle them, I need a way to map the
|
||||
public subdomain to the resource on the server that the client has requested.
|
||||
This is the role of the reverse proxy.
|
||||
|
||||
A reverse proxy serves as a buffer between the server and the incoming client
|
||||
requests. It inspects the request and directs it to the port of the relevant
|
||||
running process.
|
||||
|
||||
Reverse proxies have other uses and advantages - they can be used as load
|
||||
balancers and as a security measure to block certain traffic. However my use
|
||||
will be mostly administrative - directing incoming requests to the right
|
||||
services.
|
||||
|
||||
### Docker
|
||||
|
||||
[Docker](https://www.docker.com/) is fairly complex but I want to go into a bit
|
||||
of detail because I think it is one of the few genuinely innovative technologies
|
||||
of the last couple of decades. It's remarkable how simple it is to use and how
|
||||
quickly you can provision complex resources with just a few lines of
|
||||
configuration.
|
||||
|
||||
Docker exploits a capacity native to the Linux kernel: containerisation.
|
||||
Containers allow you to isolate running processes from their specific runtime
|
||||
environment into self-contained virtual runtimes.
|
||||
|
||||
All operating system processes ultimately share the same computational
|
||||
resources: memory, disk-space, processor etc. This means that any one process
|
||||
can monopolise those resources at the expense of the others. For example, you
|
||||
might be running software that has hit a bug and as a result it freezes and your
|
||||
mouse movement slows down.
|
||||
|
||||
Prior to the advent of containerisation, server management mostly consisted in
|
||||
balancing the competing resource needs of different processes. With containers,
|
||||
you group a set of related processes into an isolated group and assign them a
|
||||
specific amount of virtual memory, disk-space etc. This grouping is partitioned
|
||||
from other processes on the native OS and the container is ignorant of the
|
||||
specific machine it is running on. This means it is confined to its container
|
||||
and cannot unduly affect other system processes. It can also be activated and
|
||||
deactivated, as needed.
|
||||
|
||||
You can provision software just as selectively as hardware, using specific
|
||||
runtimes and dependencies as required. This practically eradicates the common
|
||||
issue of conflicts arising between, say, the version of Python you have on your
|
||||
local machine, and the version required by a third-party software. Moreover, as
|
||||
containers are _portable_ - they can, in principle, be shared between machines -
|
||||
you can share software as a container and be confident that it will run on any
|
||||
machine that can leverage containerisation.
|
||||
|
||||
This is where Docker comes in. It's a particular implementation of container
|
||||
technology that is designed to simplify and standardise the creation and
|
||||
exchange of containerised software.
|
||||
|
||||
A Docker _image_ is a blueprint for creating a specific container. For example,
|
||||
you might use a MySQL image to create a database within your application. The
|
||||
image contains everything necessary to run the application: binaries, libraries,
|
||||
resources, and additional dependencies.
|
||||
|
||||
You can combine several images into a single container. In this scenario, you
|
||||
might also include an OS image to manage the different components. These are
|
||||
typically stripped-down versions of common Linux distributions.
|
||||
|
||||
Docker images are defined in a declarative file (Dockerfile) that specifies the
|
||||
software to be used, the directory within the container where it should execute,
|
||||
and, usually, an initialisation command.
|
||||
|
||||
To demonstrate, the Dockerfile below sets up a basic Python application using
|
||||
the public `python:3.8` image. It transfers source files from a directory on the
|
||||
local machine into to the container, installs dependencies and then starts the
|
||||
application:
|
||||
|
||||
```
|
||||
FROM python:3.8
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN pip install -r requirements.txt
|
||||
CMD ["python", "./my_script.py"]
|
||||
```
|
||||
|
||||
Docker maintains a [public registry](https://hub.docker.com/) of images that you
|
||||
can download and use via the Docker CLI.
|
||||
|
||||
Hopefully my intended architecture is starting to become apparent: I will run
|
||||
software on my server using Docker images. Requests will arrive at a given
|
||||
subdomain and the reverse proxy will channel them the port where the Docker
|
||||
container is running.
|
||||
|
||||
That's enough background, let's get started...
|
||||
|
||||
## Grafana
|
||||
|
||||
The first service I am going to host is
|
||||
[Grafana](https://en.wikipedia.org/wiki/Grafana). This is a good software to
|
||||
start with because it will allow me to easily access server logs and build
|
||||
dashboards that display performance and capacity metrics about the server and
|
||||
the services I'm running.
|
||||
|
||||
I am going to host Grafana at the `grafana.systemsobscure.net` subdomain and, as
|
||||
explained, I will run it as a Docker container.
|
||||
|
||||
### Code management
|
||||
|
||||
All of my services will live in a single monorepo on GitHub. This simplifies
|
||||
deployment. I will configure the software in this repository on my local machine
|
||||
and test it on a local server. Then, when I want to deploy , I'll simply push my
|
||||
changes to the remote and pull them down to the production server over SSH.
|
||||
|
||||
To manage each service I will use a Docker Compose file. This is just a more
|
||||
elaborate Dockerfile that you use to manage multi-container Docker applications
|
||||
that require more advanced functionality than a single container can provide.
|
||||
For example, when you have multiple containers, they may need access to a shared
|
||||
storage device and shared network in order to communicate. You define all this
|
||||
in the Docker Compose then use a single command (`docker compose up`) to start
|
||||
all the processes.
|
||||
|
||||
The directory structure of the monorepo is as follows:
|
||||
|
||||
```
|
||||
├── proxy
|
||||
│ └── nginx
|
||||
│ ├── conf.d
|
||||
│ │ └── grafana.conf
|
||||
│ └── docker-compose.yml
|
||||
└── services
|
||||
└── grafana
|
||||
├── docker-compose.yml
|
||||
├── prometheus
|
||||
│ └── prometheus.yml
|
||||
├── promtail
|
||||
│ └── promtail-config.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Each service will have its own subdirectory (for example `grafana/` ) containing
|
||||
a Docker Compose file that configures the software. Certain images within the
|
||||
Docker Compose (eg. `prometheus`, `promtail`) may require custom configuration
|
||||
in addition to the Docker Compose file - this will be handled in a dedicated
|
||||
config file, e.g. `prometheus/prometheus.yml`.
|
||||
|
||||
### Building the Grafana Docker container
|
||||
|
||||
I don't want to get too bogged down in the details of how I set Grafana up, as
|
||||
my objective in this pose is to demonstrate the generic architecture. However a
|
||||
few parts require clarification.
|
||||
|
||||
Grafana itself is a sort of parent to different tracking and logging tools. It
|
||||
provides an integrated interface for a wide variety of tools and you only
|
||||
install the ones you wish to use.
|
||||
|
||||
Each of these sub-services require their own Docker image in addition to the
|
||||
Grafana Docker image. I will be using the following:
|
||||
|
||||
- Loki
|
||||
- To display server logs
|
||||
- Promtail
|
||||
- For collecting logs on the host server
|
||||
- Prometheus
|
||||
- Backend service that presents the Grafana frontend with data and which can
|
||||
be queried
|
||||
- Node Exporter
|
||||
- A service that can be used by Prometheus to gather hardware and OS metrics
|
||||
about the host environment
|
||||
|
||||
In `/services/grafana/docker-compose.yml`, I create an entry for each service
|
||||
(including Grafana itself) specifying the image I want to use:
|
||||
|
||||
```yml
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:latest
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
```
|
||||
|
||||
Certain containers are going to require their own Docker volumes. A Docker
|
||||
volume is a form of persistent virtual storage within the container for storing
|
||||
application data. This will be necessary for Prometheus, Loki, and Grafana so I
|
||||
add the following to the Docker Compose:
|
||||
|
||||
```yml
|
||||
volumes:
|
||||
prometheus_data:
|
||||
loki_data:
|
||||
grafana_data:
|
||||
```
|
||||
|
||||
This establishes the volumes, then for each image I specify a `volumes` field
|
||||
that maps the volume to the requisite directory within the container:
|
||||
|
||||
```yml
|
||||
grafana:
|
||||
image: grafana/grafana:latestki
|
||||
container_name: grafana
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
```
|
||||
|
||||
The Grafana application will look for its data at `/var/lib/grafana` at runtime,
|
||||
hence the volume is mapped to this location.
|
||||
|
||||
In addition to volumes, I need to provision Docker networks. I need a network
|
||||
because the individual applications will need to be able to communicate with
|
||||
each other and the `grafana` container - which is the parent of the other
|
||||
applications - will need to communicate with processes outside of itself.
|
||||
Namely, the reverse proxy that will connect client requests to the Grafana
|
||||
service.
|
||||
|
||||
The following section of the Compose file specifies two networks for this
|
||||
purpose: `default` which runs internally within the the container, and `web`
|
||||
which is external to the container which `grafana` and other separate Docker
|
||||
containers will be able to hook into:
|
||||
|
||||
```yml
|
||||
networks:
|
||||
default:
|
||||
web:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then in my `grafana` declaration, I specify its membership of these networks:
|
||||
|
||||
```yml
|
||||
grafana:
|
||||
image: grafana/grafana:latestki
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
networks:
|
||||
- default
|
||||
- web
|
||||
```
|
||||
|
||||
You'll notice I also include an environment variable for the password. This will
|
||||
be injected automatically via the presence of a `.env` file on the server
|
||||
containing the password.
|
||||
|
||||
Additionally I specify port `3000` for Grafana to run on. This will be important
|
||||
later when I connect it to the reverse proxy.
|
||||
|
||||
### Deploying the container
|
||||
|
||||
I'm now ready to pull the changes from the server and test out the container in
|
||||
the live environment.
|
||||
|
||||
Once I've SSH'd into the server I install Docker:
|
||||
|
||||
```sh
|
||||
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
|
||||
I clone my monorepo on the server and then `cd` into the `/services/grafana`
|
||||
directory and run the Docker Compose start script:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The containers comprising the Docker Compose start fine:
|
||||
|
||||

|
||||
|
||||
I also confirm that the two networks associated with Grafana (`web` and
|
||||
`default`) are running as expected:
|
||||
|
||||

|
||||
|
||||
## nginx
|
||||
|
||||
So far I have succeeded in getting my `grafana` container up and running on port
|
||||
3000 but I have no way yet of accessing its frontend. To do this I need to set
|
||||
up and configure the reverse proxy.
|
||||
|
||||
I'll use [nginx](https://nginx.org/en/) for this and I'll also run this as a
|
||||
Docker service, configured in the `proxy/` directory of the monorepo that I
|
||||
demonstrated earlier.
|
||||
|
||||
As with Grafana, everything necessary is detailed in the Docker Compose:
|
||||
|
||||
```yml
|
||||
# proxy/nginx/docker-compose.yml
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./conf.d:/etc/nginx/conf.d
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
```
|
||||
|
||||
Key points to note:
|
||||
|
||||
- `nginx` will have access to the shared `web` Docker network that we saw
|
||||
earlier when configuring Grafana. This will enable it to communicate with
|
||||
other Docker services.
|
||||
- I pass through the actual location of my TLS certificates on the server
|
||||
(`/etc/letsencrypt`) to the nginx container, specifying that they are read
|
||||
only. This is a bit different to Grafana where we were mapping virtual Docker
|
||||
volumes to locations within the container.
|
||||
- I map both the server's HTTP (`80`) and HTTPS (`443`) ports through to the
|
||||
equivalent ports in the `nginx` container.
|
||||
|
||||
For each Docker service that `nginx` will proxy, I need to provide a
|
||||
configuration file within the `nginx/conf.d` directory. (These too are copied
|
||||
into the container.)
|
||||
|
||||
The core process is quite simple. You specify:
|
||||
|
||||
- The port you want `nginx` to listen to
|
||||
- The specific subdomain being requested on that port
|
||||
- The location of the running server process to which you want to direct the
|
||||
incoming request
|
||||
|
||||
To demonstrate with the Grafana config file:
|
||||
|
||||
```
|
||||
# proxy/nginx/conf.d/grafana.conf
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name grafana.systemsobscure.net;
|
||||
|
||||
location / {
|
||||
proxy_pass http://grafana:3000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the basic idea. In fact it's a bit more complicated because we need to
|
||||
enforce HTTPS and make it such that `grafana.systemsobscure.net` is only served
|
||||
over HTTPS.
|
||||
|
||||
To do this I specify the location of my TLS certificate and set the necessary
|
||||
response header:
|
||||
|
||||
```
|
||||
# proxy/nginx/conf.d/grafana.conf
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name grafana.systemsobscure.net;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/systemsobscure.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/systemsobscure.net/privkey.pem;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This is fine so long as people always use `https://grafana.systemsobscure.net`
|
||||
but of course they won't always do this, so we need to redirect any requests to
|
||||
the HTTP port (80) to the HTTPS service. This is achieved with a simple 301
|
||||
redirect in the same file:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name grafana.systemsobscure.net;
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now any requests to `grafana.systemsobscure.net` will automatically be served at
|
||||
`https://grafana.systemsobscure.net`.
|
||||
|
||||
The final step is to start the `nginx` container:
|
||||
|
||||
```sh
|
||||
cd proxy/nginx
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
With the Grafana container running and the reverse-proxy configured we can
|
||||
finally navigate to `https://grafana.systemsobscure.net` and check out the live
|
||||
service!
|
||||
|
||||

|
||||
|
||||
Once I've logged in, I use a Grafana template code to create a default template
|
||||
that outputs the system metrics provided by the Node Exporter service:
|
||||
|
||||

|
||||
|
||||
I can also go to Loki and look at the server connection logs and track the
|
||||
activity of the `fail2ban` software I installed in the last post:
|
||||
|
||||

|
||||
|
||||
Later, when I have additional services, I'll add additional dashboards and Loki
|
||||
queries so I can analyse everything that is going on.
|
||||
|
||||
## Resources
|
||||
|
||||
In researching how to do the above, I created or expanded the following entries
|
||||
in my Zettelkasten:
|
||||
|
||||
- [Proxies](https://thomasabishop.github.io/eolas/Proxies)
|
||||
- [Containerization](https://thomasabishop.github.io/eolas/Containerization)
|
||||
- [Docker images](https://thomasabishop.github.io/eolas/Docker_images)
|
||||
- [Creating a Docker image](https://thomasabishop.github.io/eolas/Creating_a_Docker_image)
|
||||
- [Docker containers](https://thomasabishop.github.io/eolas/Docker_containers)
|
||||
- [Docker storage](https://thomasabishop.github.io/eolas/Docker_storage)
|
||||
- [Docker Compose and Dockerfile difference](https://thomasabishop.github.io/eolas/Docker_compose_and_Dockerfile_difference)
|
||||
114
posts/slack-notification-center.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
title: "Using Slack as a desktop notification center"
|
||||
slug: /slack-notification-center/
|
||||
date: 2024-04-10
|
||||
tags: ["log", "project", "productivity", "javascript"]
|
||||
---
|
||||
|
||||
My current desktop environment is very bare-bones and consists basically of a
|
||||
window manager and launcher. One difficulty of this is having to manage
|
||||
notifications manually.
|
||||
|
||||
Triggering notifications is fine, I use
|
||||
[swaync](https://github.com/ErikReider/SwayNotificationCenter). The problem is
|
||||
logging them in one place so I can see at a glance what has been going on over a
|
||||
given time period. There are solutions for the Wayland compositor that I use but
|
||||
the list of notifications won't persist beyond the current session. They are
|
||||
also quite ugly with limited options for style customisation.
|
||||
|
||||
When I talk about notifcations, I mainly mean alerts triggered by the automated
|
||||
scripts I have running on my system: backups, auto-commits to my Zettelkasten,
|
||||
time-tracking etc. I don't care about email notifications, Spotify updates etc
|
||||
and suppress these.
|
||||
|
||||
I thought a good way to solve this would be to forgo a desktop-specific
|
||||
notification center and instead use Slack. Each notification type has a
|
||||
dedicated channel which I can post to via a specific webhook URL. On the
|
||||
free-plan the data will persist for up to three months which is more than
|
||||
sufficient for my purposes.
|
||||
|
||||
This solution has the added benefit that I can access my notification center
|
||||
from other devices. I can also display notifications from more than one machine
|
||||
and everything will be syndicated in one place. Additionally, I can leverage
|
||||
Slack's extensions like the GitHub bot. Finally, using Slack's block
|
||||
architecture means that I have lots of options when it comes to outputting the
|
||||
notification contents.
|
||||
|
||||
The image below shows the notification center with the channel for this blog
|
||||
displayed:
|
||||
|
||||

|
||||
|
||||
(The channel is called "Ripley" because I am an Alien fan! "processes" is the
|
||||
parent app to which the different webhooks belong)
|
||||
|
||||
I invoke the NodeJS script below when I want to set up a notification:
|
||||
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
|
||||
const process = require("process")
|
||||
const { exec } = require("child_process")
|
||||
const axios = require("axios")
|
||||
|
||||
const notificationSound =
|
||||
"mpv /home/thomas/dotfiles/gruvbox-95/sounds/st-notification.mp3"
|
||||
|
||||
const slackNotifier = async (channel, { message = "", block = null } = {}) => {
|
||||
try {
|
||||
const webhooks = {
|
||||
test: process.env.SLACK_WEBHOOK_TEST,
|
||||
backups: process.env.SLACK_WEBHOOK_BACKUPS,
|
||||
eolas: process.env.SLACK_WEBHOOK_EOLAS,
|
||||
systems_obscure: process.env.SLACK_WEBHOOK_SYSTEMS_OBSCURE,
|
||||
time_tracking: process.env.SLACK_WEBHOOK_TIME_TRACKING,
|
||||
}
|
||||
|
||||
const webhookUrl = webhooks[channel]
|
||||
|
||||
let payload
|
||||
|
||||
if (message) {
|
||||
payload = { text: message, channel: channel }
|
||||
} else if (block) {
|
||||
payload = { blocks: block, channel: channel }
|
||||
} else {
|
||||
throw new Error("Either a message or a block must be provided.")
|
||||
}
|
||||
|
||||
const response = await axios.post(webhookUrl, payload)
|
||||
|
||||
if (response.status === 200) {
|
||||
exec(notificationSound)
|
||||
console.log(`Message successfully sent to ${channel}`)
|
||||
} else {
|
||||
console.error(
|
||||
`Slack API returned non-200 response: ${response.status}`,
|
||||
response.data
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to send message to ${channel}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const [, , channel, message] = process.argv
|
||||
slackNotifier(channel, { message }).catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = { slackNotifier }
|
||||
```
|
||||
|
||||
An example of it being invoked (from within a script that manages my hourly
|
||||
backups):
|
||||
|
||||
```sh
|
||||
./slack-notifier/src/index.js 'backups' '💾 Error: backup drive not mounted.'
|
||||
|
||||
```
|
||||
|
||||
One drawback is that there is sometimes latency between the execution of the
|
||||
script which contains the notificaton trigger and the appearance of the
|
||||
notification in Slack. This is because the former happens instantly on my
|
||||
machine and the latter depends on the network. I can live with this though.
|
||||
103
posts/some-nice-typescript.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
title: Some nice TypeScript
|
||||
slug: /some-nice-typescript/
|
||||
date: 2023-09-26
|
||||
tags: ["log", "typescript"]
|
||||
---
|
||||
|
||||
I wanted to share a snippet of a TypeScript function I wrote recently. Although
|
||||
it's not a particularly interesting scenario, I like it because it is simple and
|
||||
elegant, if I say so myself.
|
||||
|
||||
At the moment I am working on an AWS Lambda that queries the API of the Toggl
|
||||
time-tracking service to retrieve a list of time entries for a given date range.
|
||||
The API returns an array of time entries with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3136598124,
|
||||
"workspace_id": 2360906,
|
||||
"project_id": 193325937,
|
||||
"task_id": null,
|
||||
"billable": false,
|
||||
"start": "2023-09-21T20:10:02+00:00",
|
||||
"stop": "2023-09-21T20:39:54Z",
|
||||
"duration": 1792,
|
||||
"description": "pytest blog post",
|
||||
"tags": [],
|
||||
"tag_ids": [],
|
||||
"duronly": true,
|
||||
"at": "2023-09-21T20:39:54+00:00",
|
||||
"server_deleted_at": null,
|
||||
"user_id": 3700888,
|
||||
"uid": 3700888,
|
||||
"wid": 2360906,
|
||||
"pid": 193325937
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the `project_id` is a number rather than the human-readable string
|
||||
that appears in the Toggl UI. When I return the data to the frontend I want to
|
||||
have the name of the project rather than this number. So I wrote the following
|
||||
transformer:
|
||||
|
||||
```ts
|
||||
interface IProject {
|
||||
id: number
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type TProjectMap = Record<IProject["id"], IProject["name"]>
|
||||
|
||||
const getProjects = async (): Promise<TProjectMap> => {
|
||||
const workspace = process.env.TOGGL_WORKSPACE_ID
|
||||
const togglClient = new TogglClient()
|
||||
const projects: IProject[] = await togglClient.get(
|
||||
`workspaces/${workspace}/projects`
|
||||
)
|
||||
return parseProjects(projects)
|
||||
}
|
||||
|
||||
const parseProjects = (projects: IProject[]): TProjectMap => {
|
||||
return projects.reduce((projectMap: TProjectMap, project: IProject) => {
|
||||
projectMap[project.id] = project.name
|
||||
return projectMap
|
||||
}, {})
|
||||
}
|
||||
```
|
||||
|
||||
`getProjects` obviously calls the Toggl API to retrieve the list of projects for
|
||||
the workspace. The `parseProjects` function takes the array of projects and
|
||||
returns an object with the project IDs as keys and the project names as values.
|
||||
In my main controller function, I will then be able to transform the project IDs
|
||||
like so:
|
||||
|
||||
```ts
|
||||
const projects = await getProjects()
|
||||
const projectName = projects[193325937]
|
||||
console.log(projectName)
|
||||
// "Practical study"
|
||||
```
|
||||
|
||||
What I like:
|
||||
|
||||
- The time-entry returns several properties that I am not interested in - I only
|
||||
want `id` and `name`. I could manually type the other properties to ensure
|
||||
full type-safety but life is short and I can't be bothered. One get-out would
|
||||
be to use `any` for the other properties but this is obviously self-defeating
|
||||
in TS. Instead I use `unknown` and then type-assert the return value of
|
||||
`parseProjects` to `TProjectMap` which is a record of `number` keys and
|
||||
`string` values. This means that if I try to access a property of the
|
||||
`projectMap` object that is not a number, I will get a type error with the
|
||||
added benefit that I make it transparent to the reader that I am solely
|
||||
interested in the `id` and `name` keys.
|
||||
|
||||
- Rather than using a `forEach` loop or a `map` function combined with a
|
||||
`filter`, I make good use of `reduce`, _reduce_ being a very semantic account
|
||||
of what I am seeking to do.
|
||||
|
||||
More generally, I feel like I have purposefully harnessed the explanatory value
|
||||
of the type system to make the code more readable and self-documenting. This is
|
||||
a good example of where TypeScript can reduce the need for boilerplate and
|
||||
comments, in addition to the obvious benefits of type-safety.
|
||||
61
posts/suppressing-logs-errors-jest.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
title: "Suppressing logs and errors in Jest"
|
||||
slug: /suppressing-logs-errors-jest/
|
||||
date: 2024-09-10
|
||||
tags: ["javascript", "unit-testing"]
|
||||
---
|
||||
|
||||
It annoys me when I am working on a project and a previous developer has left
|
||||
logs and/or thrown errors in their unit tests. To be clear: I mean when the
|
||||
developer is testing that an error is thrown in the right circumstances, not a
|
||||
test failure arising from regression. The former makes the latter harder to
|
||||
detect by polluting the output.
|
||||
|
||||
This can be so easily prevented.
|
||||
|
||||
Take the following function:
|
||||
|
||||
```js
|
||||
function sillyFunction(int) {
|
||||
console.info(`Now handling ${int}`)
|
||||
if (int > 2) throw new Error(`Error: int ${int} is greater than two`)
|
||||
else return
|
||||
}
|
||||
```
|
||||
|
||||
To avoid pointlessly logging to the console and to confirm the error without
|
||||
actually throwing it:
|
||||
|
||||
```js
|
||||
import { jest } from "@jest/globals"
|
||||
|
||||
describe("sillyFunction", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, "info").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
console.info.mockRestore()
|
||||
})
|
||||
|
||||
it("throws error if `int` is less than 2", () => {
|
||||
expect(() => {
|
||||
sillyFunction(3)
|
||||
}).toThrow("Error: int 3 is greater than two")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
The `spyOn` method silences the output of `console.error` by returning nothing.
|
||||
If we wish, we can still confirm that the log is acting as expected during the
|
||||
runtime of the test by again using a spy:
|
||||
|
||||
```js
|
||||
const consoleInfoSpy = jest.spyOn(console, "info")
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith("Now handling x")
|
||||
```
|
||||
|
||||
The `toThrow` method catches the error before it hits the console and allows us
|
||||
to interrogate it. If it was an asynchronous function under test, we would need
|
||||
to use the matcher `rejects.toThrow`. This waits for the promise to resolve
|
||||
before checking if it has been rejected.
|
||||
44
posts/the-world-knew-him-not.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: "And the world knew him not (lossless compression)"
|
||||
slug: /the-world-knew-him-not/
|
||||
date: 2023-02-16
|
||||
tags: ["random", "reading"]
|
||||
---
|
||||
|
||||
Last night I was reading 'Three Versions of Judas' from Juan Luis Borges'
|
||||
masterpiece _Fictions_ (1944). The structure is typical Borges: a cool precis of
|
||||
a series of fabricated texts, replete with tokens of mock facticity (publication
|
||||
dates, references to other commentaries, footnotes on errata and so forth).
|
||||
|
||||
The story describes the work of an obscure theologian who developed a
|
||||
revisionist account of the role of Judas in the Passion, broadly in keeping with
|
||||
the (real) Gnostic
|
||||
[Gospel of Judas](https://en.wikipedia.org/wiki/Gospel_of_Judas). Far from being
|
||||
the perennial embodiment of self-interest and betrayal, Judas is reimagined as
|
||||
an agent of the Holy Spirit. He becomes the essential catalyst in the
|
||||
fullfilment of Christ's prophesy and thus the redemption of humanity.
|
||||
|
||||
The argument is compelling and centers on why it would be necessary to _betray_
|
||||
Jesus in the first place. His activities were well known to both the Jewish and
|
||||
Roman authorities. If the same Gospels that cast Judas as the betrayer are to be
|
||||
believed, Jesus was constantly performing miracles, challenging social and
|
||||
religious hierarchies and being a major-league nuisance. They could've come for
|
||||
him at any point.
|
||||
|
||||
According to the scholar, Judas was the most significant disciple because he had
|
||||
a presentiment of the Crucifixion and its cosmic significance, and was compelled
|
||||
to "betray" Christ and shoulder eternal infamy out of this knowledge. His
|
||||
suicide by hanging was a pale mirroring of Christ's own death. Hence the kiss
|
||||
(an act of love) and discarding the silver at the temple (a repudiation of a
|
||||
simplistic reading of the betrayal). Hence also, Jesus's to Judas at the Last
|
||||
Supper: "what you are going to do, do quickly".
|
||||
|
||||
Anyway, this elaborate preamble has been merely to draw attention to a passage
|
||||
in John that Borges quotes. Maybe it is just that I am a lapsed Catholic but I
|
||||
think it is very beautiful and profound: a lossless compression of the entirety
|
||||
of Christianity to a single sentence:
|
||||
|
||||
> He was in the world, and the world was made by him, and the world knew him
|
||||
> not.
|
||||
|
||||
— John 1: 10, King James Version
|
||||
88
posts/two-strategies-for-handling-exceptions-in-python.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
title: "Two strategies for handling exceptions in Python"
|
||||
slug: /two-strategies-for-handling-exceptions-in-python/
|
||||
date: 2023-10-03
|
||||
tags: ["learning", "python"]
|
||||
---
|
||||
|
||||
The following function illustrates two strategies for handling exceptions in
|
||||
Python:
|
||||
|
||||
```py
|
||||
def parse_articles(articles: Dict[str, Any]) -> List[List]:
|
||||
|
||||
if not articles:
|
||||
raise ValueError("No articles to parse")
|
||||
|
||||
articles_list = []
|
||||
|
||||
for article in articles.values():
|
||||
try:
|
||||
time_added, given_title, resolved_url = (
|
||||
article["time_added"],
|
||||
article["given_title"],
|
||||
article["resolved_url"],
|
||||
)
|
||||
except KeyError as e:
|
||||
logging.warning(
|
||||
f"Article missing {e} property. Skipping article: {given_title}."
|
||||
)
|
||||
continue
|
||||
|
||||
articles_list.append([time_added, given_title, resolved_url])
|
||||
|
||||
return articles_list
|
||||
```
|
||||
|
||||
The function is a basic transformer: it receives a dictionary and loops through
|
||||
a subset of its properties which are also dictionaries, returning selected keys
|
||||
from each as a multidimensional array.
|
||||
|
||||
The first exception handler checks whether the `articles` dictionary is
|
||||
populated or `None`. If either are the case, the function exits and returns a
|
||||
`ValueError`. This approach _propagates_ the error and is deliberately and
|
||||
transparently obstructive: the function exits before it can execute the main
|
||||
code. If this exception is not adequately handled by the caller, it will cause a
|
||||
runtime error.
|
||||
|
||||
The second instance of exception handling occurs in the loop. If any of the
|
||||
three specified properties (`time_added`, `given_title`, `resolved_url`) are
|
||||
absent from the `articles` dictionary, a `KeyError` exception will be thrown.
|
||||
The outcome is quite different from the first exception however. The function
|
||||
will not exit immediately, it simply won't append this set of properties to the
|
||||
list, and will move on to the next iteration of the loop.
|
||||
|
||||
This behaviour, known as _graceful degradation_ is afforded by the _try, except_
|
||||
scaffolding. It literally allows us to `try` an execution before an exception
|
||||
can be raised. And if an exception is raised, we can augment the code with a
|
||||
`finally` block to return something regardless of the exception, such as an
|
||||
empty list.
|
||||
|
||||
In the scenario above, `finally` isn't used since the `continue` ensures that
|
||||
the execution proceeds to the next iteration of the loop. In addition, even if
|
||||
none of the `articles` contain the requisite keys, an empty list will be still
|
||||
be returned.
|
||||
|
||||
Both strategies, propagation and graceful degradation, have their merits.
|
||||
|
||||
Propagation is useful because it identifies the error immediately and blocks the
|
||||
code execution, meaning that failures do not occur silently or go unchecked,
|
||||
forcing the caller to implement a handling routine. This is particulaly useful
|
||||
for genuine exceptions - cases that _should not_ occur in regular execution. If
|
||||
they occur, they are so severe that they should be addressed immediately, not
|
||||
deferred. One such scenario might be a memory-intensive script or lambda running
|
||||
in a severless cloud provider: you would not want it to waste compute time on a
|
||||
process that is not going to return the expected value.
|
||||
|
||||
The obvious benefit of graceful degradation is that it doesn't halt the program
|
||||
straight away. This is most suitable for cases where the exception in question
|
||||
is fairly common. In the context of the code example this would be not returning
|
||||
articles that lack the requisite keys. Providing most of the articles are
|
||||
parsable, the fact that some are skipped is unlikely to be a terminal fault in
|
||||
the application.
|
||||
|
||||
Finally, the two methods differ in the amount of work required of the developer.
|
||||
Propagation requires explicit error handling from the function or method that
|
||||
receives the error. In contrast, graceful degradation allows for silent
|
||||
failure - the error is not immediately apparent and may only be detected in logs
|
||||
at a later stage.
|
||||
197
posts/waybar-customisation.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
---
|
||||
title: "Waybar customisation"
|
||||
slug: /waybar-customisation/
|
||||
date: 2024-03-03
|
||||
tags: ["log", "productivity", "python", "linux"]
|
||||
---
|
||||
|
||||
On my local machine I am currently replacing my desktop environment (Gnome) with
|
||||
a tile-based window manager ([Hyprland](https://github.com/hyprwm/Hyprland)).
|
||||
I'm trying to strip out the bloat and have a lightweight and highly customised
|
||||
Arch workhorse.
|
||||
|
||||
A key component is a useful status bar that displays metrics and provides a
|
||||
means of quickly executing common tasks. Previously, when I was using X, I opted
|
||||
for Polybar but now that I am on Wayland I'm using
|
||||
[waybar](https://github.com/Alexays/Waybar). So far, I've added two custom
|
||||
modules: one for displaying code metrics and another for managing my
|
||||
time-tracking.
|
||||
|
||||

|
||||
|
||||
<div style="text-align:center;">
|
||||
<i >Full status bar. Click to enlarge.</i>
|
||||
</div>
|
||||
|
||||
## Code metrics module
|
||||
|
||||
I use WakaTime to collate metrics on my coding activity (time coding,
|
||||
programming languages, projects etc.) I already have a dashboard on this site
|
||||
that displays some of this data but I thought it would be useful to see my stats
|
||||
from the status bar as I am working.
|
||||
|
||||
Below is an image of the module. The permanent display outputs the coding
|
||||
duration for the current day. When you hover you see a summary of the main
|
||||
languages used and the main projects I've been working on.
|
||||
|
||||

|
||||
|
||||
It's a straightforward Python script that queries the WakaTime API and parses
|
||||
the data and forwards it to the module:
|
||||
|
||||
```py
|
||||
#! /usr/local/bin/python3
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
import textwrap
|
||||
|
||||
WAKATIME_API_KEY = os.getenv("WAKATIME_API_KEY")
|
||||
WAKATIME_ENDPOINT = "https://wakatime.com/api/v1/users/current/status_bar/today"
|
||||
|
||||
|
||||
def get_data(url):
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(
|
||||
f"Failed to fetch data from API. Status code: {response.status_code}"
|
||||
)
|
||||
|
||||
|
||||
def generate_tooltip(time, languages, projects):
|
||||
return textwrap.dedent(
|
||||
f"""\
|
||||
Time coding: {time}
|
||||
Languages: {languages}
|
||||
Projects: {projects}"""
|
||||
)
|
||||
|
||||
|
||||
def format_metric(metrics):
|
||||
return ", ".join(
|
||||
[f'{metric["name"]} ({metric["percent"]}%)' for metric in metrics[:3]]
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
output = {}
|
||||
try:
|
||||
data = get_data(WAKATIME_ENDPOINT + "?api_key=" + WAKATIME_API_KEY)
|
||||
digital_time = data["data"]["grand_total"]["digital"]
|
||||
human_time = data["data"]["grand_total"]["text"]
|
||||
langs = data["data"]["languages"]
|
||||
projects = data["data"]["projects"]
|
||||
tooltip = generate_tooltip(
|
||||
human_time, format_metric(langs), format_metric(projects)
|
||||
)
|
||||
output["text"] = digital_time
|
||||
output["tooltip"] = tooltip
|
||||
|
||||
except Exception as e:
|
||||
output["text"] = "Error"
|
||||
|
||||
print(json.dumps(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Here's the module declaration in the Waybar config:
|
||||
|
||||
```
|
||||
"custom/wakatime": {
|
||||
"exec": "source $HOME/dotfiles/.env && python3 $HOME/.config/waybar/resources/custom_modules/wakatime_waybar.py",
|
||||
"format": " {}",
|
||||
"return-type": "json",
|
||||
"interval": 600
|
||||
},
|
||||
```
|
||||
|
||||
## Time-tracking module
|
||||
|
||||
I've recently started using [time warrior](https://timewarrior.net/) to track my
|
||||
extra-curricular coding and study. This is a command-line time-tracker so it
|
||||
integrates really well with Waybar.
|
||||
|
||||
The module highlights green when there is an active timer. This helps to remind
|
||||
me to stop the timer! When you click the current timer stops and when you
|
||||
right-click it resumes. This saves me going into the terminal when I stopping
|
||||
and starting an ongoing piece of work.
|
||||
|
||||

|
||||
|
||||
<div style="text-align:center;">
|
||||
<i >Time warrior module in its active state.</i>
|
||||
</div>
|
||||
|
||||
Again I've used Python for the scripting:
|
||||
|
||||
```py
|
||||
#! /usr/local/bin/python3
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
|
||||
def invoke_shell(proc):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
proc,
|
||||
shell=True,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
return e.stderr.strip()
|
||||
|
||||
|
||||
def timer_active() -> bool:
|
||||
status = invoke_shell("timew get dom.active")
|
||||
if status == "1":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def generate_tooltip():
|
||||
tooltip = invoke_shell("timew summary :week")
|
||||
return tooltip
|
||||
|
||||
|
||||
def main():
|
||||
output = {}
|
||||
try:
|
||||
if timer_active():
|
||||
output["text"] = " Timer"
|
||||
output["class"] = "active"
|
||||
else:
|
||||
output["text"] = " Timer"
|
||||
output["class"] = "inactive"
|
||||
except Exception as e:
|
||||
output["text"] = "Error"
|
||||
|
||||
print(json.dumps(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
And here is the module declaration in the Waybar config:
|
||||
|
||||
```
|
||||
"custom/timewarrior": {
|
||||
"exec": "python3 $HOME/.config/waybar/resources/custom_modules/time_warrior_waybar.py",
|
||||
"format": "{}",
|
||||
"on-click": "timew stop && notify-send 'Time Warrior' 'Timer stopped'",
|
||||
"on-click-right": "timew continue && notify-send 'Time Warrior' 'Timer resumed'",
|
||||
"return-type": "json",
|
||||
"interval": 5
|
||||
},
|
||||
```
|
||||
BIN
public/blog-screenshot.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
71
scripts/generate-post-index.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import fs from "fs"
|
||||
import matter from "gray-matter"
|
||||
import { marked } from "marked"
|
||||
import { createHighlighter } from "shiki"
|
||||
import { transformerColorizedBrackets } from "@shikijs/colorized-brackets"
|
||||
import { processBlogImages } from "./process-blog-imgs.js"
|
||||
import { transformImagePaths } from "./transform-img-paths.js"
|
||||
|
||||
const renderer = {
|
||||
image({ href, text }) {
|
||||
return `<figure><img src=${href} /><figcaption>${text}</figcaption></figure>`
|
||||
},
|
||||
}
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ["github-dark-dimmed"],
|
||||
langs: [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"html",
|
||||
"css",
|
||||
"json",
|
||||
"markdown",
|
||||
"bash",
|
||||
"python",
|
||||
"yaml",
|
||||
],
|
||||
})
|
||||
|
||||
// Copy blog images to /public and compress
|
||||
processBlogImages()
|
||||
|
||||
const files = fs.readdirSync("posts").filter((x) => x.endsWith(".md"))
|
||||
|
||||
const posts = files.map((file) => {
|
||||
const raw = fs.readFileSync(`posts/${file}`, "utf8")
|
||||
const { data, content: markdown } = matter(raw)
|
||||
|
||||
// Convert img urls to source from /public whilst markdown
|
||||
const transformedMarkdown = transformImagePaths(markdown)
|
||||
|
||||
// Appy syntax highlighting
|
||||
let html = transformedMarkdown.replace(
|
||||
/```(\w+)?\n([\s\S]*?)```/g,
|
||||
(match, lang, code) => {
|
||||
return highlighter.codeToHtml(code.trim(), {
|
||||
lang: lang || "text",
|
||||
theme: "github-dark-dimmed",
|
||||
transformers: [transformerColorizedBrackets()],
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Convert img tags to figure tags
|
||||
marked.use({ renderer })
|
||||
|
||||
console.info(`Processing ${file}...`)
|
||||
|
||||
return {
|
||||
slug: file.replace(".md", ""),
|
||||
title: data.title,
|
||||
date: data.date,
|
||||
tags: data.tags,
|
||||
html: marked(html),
|
||||
}
|
||||
})
|
||||
|
||||
fs.writeFileSync("./public/post-index.json", JSON.stringify(posts, null, 2))
|
||||
console.info(`✅ Generated ${posts.length} posts.`)
|
||||
52
scripts/process-blog-imgs.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import sharp from "sharp"
|
||||
|
||||
const processBlogImages = async () => {
|
||||
const srcDir = "posts/img"
|
||||
const destDir = "public/posts/img"
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.error("⚠️ No posts/img directory found")
|
||||
return
|
||||
}
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Copy all images
|
||||
const files = fs.readdirSync(srcDir)
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
|
||||
// Use for...of loop instead of forEach to work with async/await
|
||||
for (const file of files) {
|
||||
const ext = path.extname(file).toLowerCase()
|
||||
if (imageExtensions.includes(ext)) {
|
||||
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
|
||||
const outputPath = path.join(destDir, file)
|
||||
|
||||
if (ext === ".svg") {
|
||||
// SVGs just get copied
|
||||
fs.copyFileSync(inputPath, outputPath)
|
||||
console.info(`📸 Copied ${file}`)
|
||||
} else {
|
||||
// Process other images
|
||||
await sharp(inputPath)
|
||||
.resize(1200, 800, {
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.jpeg({ quality: 85, progressive: true })
|
||||
.png({ compressionLevel: 8 })
|
||||
.webp({ quality: 85 })
|
||||
.toFile(outputPath)
|
||||
console.info(`🖼️ Processed ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Processed images from ${srcDir}`)
|
||||
}
|
||||
|
||||
export { processBlogImages }
|
||||
9
scripts/transform-img-paths.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const transformImagePaths = (markdown) => {
|
||||
// Transform ./img/image-name to /posts/img/image-name
|
||||
return markdown.replace(
|
||||
/!\[([^\]]*)\]\(\.\/img\/([^)]+)\)/g,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
export { transformImagePaths }
|
||||
105
src/components/Header.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// @ts-nocheck
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTheme } from "@/context/ThemeProvider"
|
||||
import { MoonStar } from "lucide-react"
|
||||
import { Link } from "react-router"
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
|
||||
import { Toggle } from "@/components/ui/toggle"
|
||||
const Menu = () => {
|
||||
return (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{/* Desktop menu - hidden on mobile, visible on md+ */}
|
||||
<div className="hidden md:flex md:space-x-1">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
className={navigationMenuTriggerStyle()}
|
||||
>
|
||||
<Link to="/posts/page/1">Posts</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
className={navigationMenuTriggerStyle()}
|
||||
>
|
||||
<Link to="/about">About</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</div>
|
||||
|
||||
{/* Mobile dropdown - visible only on small screens */}
|
||||
<NavigationMenuItem className="md:hidden">
|
||||
<NavigationMenuTrigger>Menu</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/docs" className="block px-2 py-1.5">
|
||||
Posts
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/docs" className="block px-2 py-1.5">
|
||||
About
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
return (
|
||||
<header className="w-full h-12 flex items-center justify-center border-b fixed top-0 z-20 bg-background">
|
||||
<div className="w-full px-2 md:px-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" asChild>
|
||||
<Link to="/">
|
||||
<span className="text-lg tracking-normal font-semibold">
|
||||
Systems Obscure
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Menu />
|
||||
<Toggle
|
||||
pressed={theme === "dark"}
|
||||
onPressedChange={() =>
|
||||
setTheme(theme === "dark" ? "light" : "dark")
|
||||
}
|
||||
>
|
||||
<MoonStar />
|
||||
<span className="hidden sm:block">Dark theme</span>
|
||||
</Toggle>
|
||||
|
||||
{/*
|
||||
<div className="hidden md:block">
|
||||
<Button variant="ghost">
|
||||
<GitMerge /> Forgejo
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export { Header }
|
||||
46
src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
168
src/components/ui/navigation-menu.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
}
|
||||
127
src/components/ui/pagination.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
45
src/components/ui/toggle.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
55
src/containers/PostListing.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// @ts-nocheck
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Link } from "react-router"
|
||||
|
||||
import { convertDate } from "@/utils/convertDate"
|
||||
|
||||
const PostListing = ({ posts, title, showAllButton }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="block no-underline"
|
||||
>
|
||||
<Card
|
||||
key={post.slug}
|
||||
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0"
|
||||
>
|
||||
<CardHeader className="">
|
||||
<CardTitle className="leading-snug font-semibold ">
|
||||
{post.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="text-sm">{convertDate(post.date)}</span>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{showAllButton && (
|
||||
<Button asChild variant="secondary" className="w-full mt-4">
|
||||
<Link to="/posts/page/1">View all</Link>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostListing
|
||||
74
src/context/ThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import * as React from "react"
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
return
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme)
|
||||
setTheme(theme)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext)
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
|
||||
return context
|
||||
}
|
||||
49
src/hooks/usePosts.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// @ts-nocheck
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const SESSION_STORAGE_KEY = "posts"
|
||||
|
||||
const sortPosts = (posts) => {
|
||||
return posts.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
}
|
||||
|
||||
const usePosts = () => {
|
||||
const [posts, setPosts] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
const storedPosts =
|
||||
typeof window !== "undefined" &&
|
||||
sessionStorage.getItem(SESSION_STORAGE_KEY)
|
||||
if (storedPosts) {
|
||||
setPosts(JSON.parse(storedPosts))
|
||||
} else {
|
||||
fetch("/post-index.json")
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error fetching posts. Status ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((data) => {
|
||||
const sortedPosts = sortPosts(data)
|
||||
sessionStorage.setItem(
|
||||
SESSION_STORAGE_KEY,
|
||||
JSON.stringify(sortedPosts)
|
||||
)
|
||||
setPosts(sortedPosts)
|
||||
})
|
||||
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch posts: ", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fetchPosts()
|
||||
}, [])
|
||||
|
||||
return { posts }
|
||||
}
|
||||
|
||||
export { usePosts }
|
||||
BIN
src/images/portrait-compressed.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
8
src/index.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
@import "./styles/_variables.css";
|
||||
@import "./styles/shadcn-overrides.css";
|
||||
@import "./styles/shadcn-theme.css";
|
||||
@import "./styles/syntax-highlighting.css";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
34
src/main.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { StrictMode, useEffect } from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { BrowserRouter, Routes, Route, useLocation } from "react-router"
|
||||
import { HomePage } from "@/pages/home"
|
||||
import { AboutPage } from "@/pages/about"
|
||||
import BlogTemplate from "./templates/BlogTemplate"
|
||||
import "./index.css"
|
||||
import { PostsPage } from "./pages/posts"
|
||||
import TagTemplate from "./templates/TagTemplate"
|
||||
|
||||
export default function ScrollToTop() {
|
||||
const { pathname } = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(1, 1)
|
||||
}, [pathname])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/posts/page/:page" element={<PostsPage />} />
|
||||
<Route path="/posts/:slug" element={<BlogTemplate />} />
|
||||
<Route path="/tags/:tag" element={<TagTemplate />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
)
|
||||
90
src/pages/about.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import MainTemplate from "@/templates/MainTemplate"
|
||||
import portrait from "../images/portrait-compressed.jpg"
|
||||
|
||||
const AboutPage = () => {
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
About
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<figure className="w-full flex flex-col items-center mb-6">
|
||||
<img
|
||||
alt="A portrait of the blog author"
|
||||
src={portrait}
|
||||
className="w-0 flex"
|
||||
/>
|
||||
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
||||
Pictured with the WITCH computer at the{" "}
|
||||
<a
|
||||
href="https://www.tnmoc.org/"
|
||||
target="_blank"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
National Museum of Computing
|
||||
</a>
|
||||
, Bletchley Park.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
Another software engineer with a blog!{" "}
|
||||
</p>
|
||||
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
I am a self-taught engineer from London. This blog is a technical
|
||||
scrapbook. I document the details of my technical life so I can have a
|
||||
record of progress when I look back. Doing this publicly motivates me to
|
||||
take care with my writing and to be as clear as possible.{" "}
|
||||
</p>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
Currently I work for{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
ITV
|
||||
</a>{" "}
|
||||
{""}
|
||||
as a backend software engineer. Before that, I worked as a full-stack
|
||||
engineer at the{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/BBC"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
BBC
|
||||
</a>{" "}
|
||||
and as a frontend engineer at{" "}
|
||||
<a
|
||||
href="https://www.arria.com/"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
Arria NLG
|
||||
</a>{" "}
|
||||
and in several web developer roles.{" "}
|
||||
</p>
|
||||
|
||||
<h3 className="mt-5 mb-0 scroll-m-20 text-xl font-semibold tracking-tight">
|
||||
Code
|
||||
</h3>
|
||||
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
I self-host my own Git forge at{" "}
|
||||
<a
|
||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
forgejo.systemsobscure.net
|
||||
</a>{" "}
|
||||
rather than use Microsoft GitHub. You can view my personal projects
|
||||
there.
|
||||
</p>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export { AboutPage }
|
||||
18
src/pages/home.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import MainTemplate from "@/templates/MainTemplate"
|
||||
import PostListing from "@/containers/PostListing"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
|
||||
const HomePage = () => {
|
||||
const { posts } = usePosts()
|
||||
return (
|
||||
<MainTemplate>
|
||||
<PostListing
|
||||
title="Recent posts"
|
||||
posts={posts.slice(0, 5)}
|
||||
showAllButton
|
||||
/>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export { HomePage }
|
||||
131
src/pages/posts.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// @ts-nocheck
|
||||
import { useMemo, useEffect } from "react"
|
||||
import MainTemplate from "@/templates/MainTemplate"
|
||||
import { useParams, useNavigate } from "react-router"
|
||||
import { convertDate } from "@/utils/convertDate"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Link } from "react-router"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
|
||||
const PostsPage = () => {
|
||||
const { page } = useParams()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { posts } = usePosts()
|
||||
const postsPerPage = 10
|
||||
|
||||
const currentPage = Number(page) || 1
|
||||
const totalPages = Math.ceil(posts.length / postsPerPage)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage < 1 || currentPage > totalPages) {
|
||||
navigate(`/posts/page/1`, { replace: true })
|
||||
}
|
||||
}, [currentPage, totalPages, navigate])
|
||||
|
||||
const currentPosts = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * postsPerPage
|
||||
const endIndex = startIndex + postsPerPage
|
||||
return posts.slice(startIndex, endIndex)
|
||||
}, [posts, currentPage, postsPerPage])
|
||||
|
||||
const hasNextPage = currentPage < totalPages
|
||||
const hasPrevPage = currentPage > 1
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (hasNextPage) {
|
||||
navigate(`/posts/page/${currentPage + 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
const goToPrevPage = () => {
|
||||
if (hasPrevPage) {
|
||||
navigate(`/posts/page/${currentPage - 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
All posts
|
||||
</h2>
|
||||
</div>
|
||||
<div className="min-h-[calc(100vh-200px)] flex flex-col">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
|
||||
{currentPosts.map((post) => (
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="block no-underline"
|
||||
>
|
||||
<Card
|
||||
key={post.slug}
|
||||
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="leading-snug font-semibold">
|
||||
{post.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="text-sm">{convertDate(post.date)}</span>
|
||||
<div>
|
||||
{post.tags.map((tag, i) => (
|
||||
<Badge
|
||||
className="ml-2 cursor-pointer"
|
||||
key={i}
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate(`/tags/${tag}`)
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-4">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToPrevPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToNextPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export { PostsPage }
|
||||
71
src/styles/_variables.css
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
:root {
|
||||
--radius: 0.3rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--font-monospaced: "Jetbrains Mono", monospace;
|
||||
--font-sansserif: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
45
src/styles/shadcn-overrides.css
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
* {
|
||||
border-color: var(--border);
|
||||
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sansserif);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"],
|
||||
.btn,
|
||||
a[href]:not([aria-disabled="true"]),
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shadow,
|
||||
.shadow-sm,
|
||||
.shadow-md,
|
||||
.shadow-lg,
|
||||
.shadow-xl,
|
||||
.shadow-2xl,
|
||||
[class*="shadow"] {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.card,
|
||||
.dialog,
|
||||
.popover,
|
||||
.dropdown-menu {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
min-width: 300px;
|
||||
}
|
||||
40
src/styles/shadcn-theme.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius));
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
37
src/styles/syntax-highlighting.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
code {
|
||||
font-family: var(--font-monospaced);
|
||||
}
|
||||
|
||||
p code {
|
||||
color: var(--foreground);
|
||||
background: var(--color-accent);
|
||||
font-size: 14px;
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.shiki {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--radius);
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
/* line-height: 1.3; */
|
||||
/* counter-reset: line; */
|
||||
font-family: var(--font-monospaced) !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* .shiki .line { */
|
||||
/* counter-increment: line; */
|
||||
/* } */
|
||||
|
||||
/* .shiki .line::before { */
|
||||
/* content: counter(line); */
|
||||
/* color: #393a34; */
|
||||
/* margin-right: 1rem; */
|
||||
/* min-width: 2rem; */
|
||||
/* text-align: right; */
|
||||
/* display: inline-block; */
|
||||
/* user-select: none; */
|
||||
/* } */
|
||||
77
src/templates/BlogTemplate.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// @ts-nocheck
|
||||
import MainTemplate from "./MainTemplate"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Link, useParams } from "react-router"
|
||||
import { convertDate } from "@/utils/convertDate"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
|
||||
const BlogTemplate = () => {
|
||||
const { slug } = useParams()
|
||||
const { posts } = usePosts()
|
||||
const post = posts?.find((x) => x.slug === slug)
|
||||
|
||||
return (
|
||||
<MainTemplate>
|
||||
{!post ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-5">
|
||||
<h2 className="text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
{post?.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex md:flex-row md:justify-between flex-col mb-8">
|
||||
<span className="text-muted-foreground">
|
||||
{convertDate(post?.date)}
|
||||
</span>
|
||||
<div className="flex gap-1 mt-3 md:mt-0">
|
||||
{post?.tags?.map((tag, i) => (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link key={i} to={`/tags/${tag}`}>
|
||||
{tag}
|
||||
</Link>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
[&>h2]:text-xl [&>h2]:border-b [&>h2]:pb-2 [&>h2]:font-semibold [&>h2]:first:mt-0 [&>h2:not(:first-child)]:mt-8
|
||||
[&>h3]:text-xl [&>h3]:sm:text-1xl [&>h3]:font-semibold [&>h3:not(:first-child)]:mt-5
|
||||
[&>h4]:text-lg [&>h4]:sm:text-xl [&>h4]:font-semibold [&>h4:not(:first-child)]:mt-4
|
||||
[&>p]:leading-7 [&>p:not(:first-child)]:mt-6
|
||||
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
|
||||
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground [&>blockquote]:text-sm
|
||||
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
|
||||
[&>table]:w-full [&>table]:my-4
|
||||
[&>table>thead>tr]:m-0 [&>table>thead>tr]:border-t [&>table>thead>tr]:p-0 [&>table>thead>tr:even]:bg-muted
|
||||
[&>table>thead>tr>th]:border [&>table>thead>tr>th]:px-4 [&>table>thead>tr>th]:py-2 [&>table>thead>tr>th]:text-left [&>table>thead>tr>th]:font-bold [&>table>thead>tr>th[align=center]]:text-center [&>table>thead>tr>th[align=right]]:text-right
|
||||
[&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted
|
||||
[&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right
|
||||
[&>code]:relative [&>code]:rounded [&>code]:bg-muted [&>code]:px-[0.3rem] [&>code]:py-[0.2rem] [&>code]:font-mono [&>code]:text-sm [&>code]:font-normal
|
||||
[&>table>tbody>tr:nth-child(even)]:bg-muted
|
||||
[&>table>thead>tr:nth-child(even)]:bg-muted
|
||||
[&_table_code]:text-sm font-normal
|
||||
[&>pre]:mt-6 [&>pre]:mb-6
|
||||
[&>p+pre]:mt-6
|
||||
[&>pre+p]:mt-6
|
||||
[&>ul+pre]:mt-6
|
||||
[&>li]:leading-[1.5]
|
||||
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-muted [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
|
||||
[&>p]:mt-6 [&>p]:leading-[1.5]
|
||||
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground
|
||||
[&>figure]:w-full [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto
|
||||
[&>figure>img]:w-full
|
||||
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-muted-foreground [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
|
||||
[&>figure>figcaption>a]:text-primary [&>figure>figcaption>a:hover]:text-primary/80
|
||||
"
|
||||
dangerouslySetInnerHTML={{ __html: post?.html }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogTemplate
|
||||
32
src/templates/MainTemplate.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// @ts-nocheck
|
||||
import { Header } from "@/components/Header"
|
||||
import { ThemeProvider } from "@/context/ThemeProvider"
|
||||
import { useTheme } from "@/context/ThemeProvider"
|
||||
|
||||
const MainTemplate = (props) => {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<MainContent>{props.children}</MainContent>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const MainContent = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
const classes =
|
||||
theme === "light"
|
||||
? "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
|
||||
: "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Header />
|
||||
<main className="flex-1 w-full px-2 md:px-4 flex justify-center pt-16">
|
||||
<div className="w-full max-w-3xl lg:max-w-3xl xl:max-w-3xl px-4 py-3">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MainTemplate
|
||||
20
src/templates/TagTemplate.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// @ts-nocheck
|
||||
import { useParams } from "react-router"
|
||||
import MainTemplate from "./MainTemplate"
|
||||
import PostListing from "@/containers/PostListing"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
|
||||
const TagTemplate = () => {
|
||||
const { tag } = useParams()
|
||||
const { posts } = usePosts()
|
||||
|
||||
const filteredPosts = posts.filter((post) => post.tags.includes(tag))
|
||||
|
||||
return (
|
||||
<MainTemplate>
|
||||
<PostListing title={`Posts tagged: #${tag}`} posts={filteredPosts} />
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagTemplate
|
||||
35
src/utils/convertDate.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
|
||||
const days = [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
]
|
||||
|
||||
const convertDate = (isoStamp: string) => {
|
||||
const unixSeconds = new Date(isoStamp)
|
||||
const weekday = days[unixSeconds.getDay()]
|
||||
const day = unixSeconds.getDate()
|
||||
const month = months[unixSeconds.getMonth()]
|
||||
const year = unixSeconds.getFullYear()
|
||||
return `${weekday} ${day} ${month} ${year}`
|
||||
}
|
||||
|
||||
export { convertDate }
|
||||
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
37
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
22
tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
34
vite.config.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// import tailwindcss from "@tailwindcss/vite"
|
||||
// import { defineConfig } from "vite"
|
||||
// import react from "@vitejs/plugin-react"
|
||||
|
||||
// export default defineConfig({
|
||||
// plugins: [react(), tailwindcss()],
|
||||
// resolve: {
|
||||
// alias: {
|
||||
// "@": "/src",
|
||||
// },
|
||||
// },
|
||||
// esbuild: {
|
||||
// tsconfigRaw: {
|
||||
// compilerOptions: {
|
||||
// skipLibCheck: true,
|
||||
// noEmit: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react-swc" // Changed this line
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
},
|
||||
},
|
||||
// Remove esbuild config entirely when using SWC
|
||||
})
|
||||