Initial commit
This commit is contained in:
commit
695d7f22bd
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
107
CLAUDE.md
Normal file
107
CLAUDE.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
I am using the lezer grammar [System Guide](https://lezer.codemirror.net/docs/guide/) [api](https://lezer.codemirror.net/docs/ref/).
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# bun-react-tailwind-template
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To start a development server:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
To run for production:
|
||||
|
||||
```bash
|
||||
bun start
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.22. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
149
build.ts
Normal file
149
build.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env bun
|
||||
import plugin from "bun-plugin-tailwind";
|
||||
import { existsSync } from "fs";
|
||||
import { rm } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`
|
||||
🏗️ Bun Build Script
|
||||
|
||||
Usage: bun run build.ts [options]
|
||||
|
||||
Common Options:
|
||||
--outdir <path> Output directory (default: "dist")
|
||||
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
|
||||
--sourcemap <type> Sourcemap type: none|linked|inline|external
|
||||
--target <target> Build target: browser|bun|node
|
||||
--format <format> Output format: esm|cjs|iife
|
||||
--splitting Enable code splitting
|
||||
--packages <type> Package handling: bundle|external
|
||||
--public-path <path> Public path for assets
|
||||
--env <mode> Environment handling: inline|disable|prefix*
|
||||
--conditions <list> Package.json export conditions (comma separated)
|
||||
--external <list> External packages (comma separated)
|
||||
--banner <text> Add banner text to output
|
||||
--footer <text> Add footer text to output
|
||||
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
|
||||
--help, -h Show this help message
|
||||
|
||||
Example:
|
||||
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
||||
|
||||
const parseValue = (value: string): any => {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
|
||||
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
||||
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
|
||||
|
||||
if (value.includes(",")) return value.split(",").map(v => v.trim());
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function parseArgs(): Partial<Bun.BuildConfig> {
|
||||
const config: Partial<Bun.BuildConfig> = {};
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === undefined) continue;
|
||||
if (!arg.startsWith("--")) continue;
|
||||
|
||||
if (arg.startsWith("--no-")) {
|
||||
const key = toCamelCase(arg.slice(5));
|
||||
config[key] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
|
||||
const key = toCamelCase(arg.slice(2));
|
||||
config[key] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let key: string;
|
||||
let value: string;
|
||||
|
||||
if (arg.includes("=")) {
|
||||
[key, value] = arg.slice(2).split("=", 2) as [string, string];
|
||||
} else {
|
||||
key = arg.slice(2);
|
||||
value = args[++i] ?? "";
|
||||
}
|
||||
|
||||
key = toCamelCase(key);
|
||||
|
||||
if (key.includes(".")) {
|
||||
const [parentKey, childKey] = key.split(".");
|
||||
config[parentKey] = config[parentKey] || {};
|
||||
config[parentKey][childKey] = parseValue(value);
|
||||
} else {
|
||||
config[key] = parseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
console.log("\n🚀 Starting build process...\n");
|
||||
|
||||
const cliConfig = parseArgs();
|
||||
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
|
||||
|
||||
if (existsSync(outdir)) {
|
||||
console.log(`🗑️ Cleaning previous build at ${outdir}`);
|
||||
await rm(outdir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
|
||||
.map(a => path.resolve("src", a))
|
||||
.filter(dir => !dir.includes("node_modules"));
|
||||
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints,
|
||||
outdir,
|
||||
plugins: [plugin],
|
||||
minify: true,
|
||||
target: "browser",
|
||||
sourcemap: "linked",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
},
|
||||
...cliConfig,
|
||||
});
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
const outputTable = result.outputs.map(output => ({
|
||||
File: path.relative(process.cwd(), output.path),
|
||||
Type: output.kind,
|
||||
Size: formatFileSize(output.size),
|
||||
}));
|
||||
|
||||
console.table(outputTable);
|
||||
const buildTime = (end - start).toFixed(2);
|
||||
|
||||
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
|
||||
17
bun-env.d.ts
vendored
Normal file
17
bun-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Generated by `bun init`
|
||||
|
||||
declare module "*.svg" {
|
||||
/**
|
||||
* A path to the SVG file
|
||||
*/
|
||||
const path: `${string}.svg`;
|
||||
export = path;
|
||||
}
|
||||
|
||||
declare module "*.module.css" {
|
||||
/**
|
||||
* A record of class names to their corresponding CSS module classes
|
||||
*/
|
||||
const classes: { readonly [key: string]: string };
|
||||
export = classes;
|
||||
}
|
||||
57
bun.lock
Normal file
57
bun.lock
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun-react-template",
|
||||
"dependencies": {
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"bun-plugin-tailwind": "^0.0.15",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"tailwindcss": "^4.1.11",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="],
|
||||
|
||||
"@lezer/generator": ["@lezer/generator@1.8.0", "", { "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" }, "bin": { "lezer-generator": "src/lezer-generator.cjs" } }, "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg=="],
|
||||
|
||||
"@lezer/highlight": ["@lezer/highlight@1.2.1", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA=="],
|
||||
|
||||
"@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||
|
||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
}
|
||||
}
|
||||
4
bunfig.toml
Normal file
4
bunfig.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
[serve.static]
|
||||
plugins = ["bun-plugin-tailwind"]
|
||||
env = "BUN_PUBLIC_*"
|
||||
29
package.json
Normal file
29
package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "bun-react-template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --hot src/server.tsx",
|
||||
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"bun-plugin-tailwind": "^0.0.15",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"@lezer/generator": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
7
src/app.tsx
Normal file
7
src/app.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const App = () => {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto p-8 text-center relative z-10">
|
||||
HI!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/bin/shrimp
Executable file
35
src/bin/shrimp
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#! /usr/bin/env bun
|
||||
|
||||
import { parser } from '../parser/shrimp.js'
|
||||
import { evaluate } from '../evaluator/evaluator.js'
|
||||
|
||||
const log = (...args: any[]) => console.log(...args)
|
||||
log.error = (...args: any[]) => console.error(...args)
|
||||
|
||||
const repl = async () => {
|
||||
log()
|
||||
log('\033[38;5;219m»……¬¯\033[0m Shrimp REPL \033[38;5;219m¯¬……«\033[0m')
|
||||
log()
|
||||
process.stdout.write('> ')
|
||||
|
||||
const context = new Map<string, any>()
|
||||
for await (const chunk of Bun.stdin.stream()) {
|
||||
const input = new TextDecoder().decode(chunk).trim()
|
||||
|
||||
try {
|
||||
const tree = parser.parse(input)
|
||||
const output = evaluate(input, tree, context)
|
||||
log(output)
|
||||
} catch (error) {
|
||||
log.error(`${error.message}`)
|
||||
}
|
||||
|
||||
process.stdout.write('> ')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
repl()
|
||||
} catch (error) {
|
||||
log.error(`Fatal Error: ${error.message}`)
|
||||
}
|
||||
120
src/evaluator/evaluator.ts
Normal file
120
src/evaluator/evaluator.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { nodeToString } from '@/evaluator/treeHelper'
|
||||
import { Tree, type SyntaxNode } from '@lezer/common'
|
||||
|
||||
type Context = Map<string, any>
|
||||
|
||||
function getChildren(node: SyntaxNode): SyntaxNode[] {
|
||||
const children = []
|
||||
let child = node.firstChild
|
||||
while (child) {
|
||||
children.push(child)
|
||||
child = child.nextSibling
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
export const evaluate = (input: string, tree: Tree, context: Context) => {
|
||||
// Just evaluate the top-level children, don't use iterate()
|
||||
let result = undefined
|
||||
let child = tree.topNode.firstChild
|
||||
while (child) {
|
||||
result = evaluateNode(child, input, context)
|
||||
child = child.nextSibling
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const evaluateNode = (node: SyntaxNode, input: string, context: Context): any => {
|
||||
const value = input.slice(node.from, node.to)
|
||||
|
||||
try {
|
||||
switch (node.name) {
|
||||
case 'Number': {
|
||||
return parseFloat(value)
|
||||
}
|
||||
|
||||
case 'Identifier': {
|
||||
if (!context.has(value)) {
|
||||
throw new Error(`Undefined identifier: ${value}`)
|
||||
}
|
||||
return context.get(value)
|
||||
}
|
||||
|
||||
case 'BinOp': {
|
||||
let [left, op, right] = getChildren(node)
|
||||
|
||||
left = assertNode(left, 'LeftOperand')
|
||||
op = assertNode(op, 'Operator')
|
||||
right = assertNode(right, 'RightOperand')
|
||||
|
||||
const leftValue = evaluateNode(left, input, context)
|
||||
const opValue = input.slice(op.from, op.to)
|
||||
const rightValue = evaluateNode(right, input, context)
|
||||
|
||||
switch (opValue) {
|
||||
case '+':
|
||||
return leftValue + rightValue
|
||||
case '-':
|
||||
return leftValue - rightValue
|
||||
case '*':
|
||||
return leftValue * rightValue
|
||||
case '/':
|
||||
return leftValue / rightValue
|
||||
default:
|
||||
throw new Error(`Unsupported operator: ${opValue}`)
|
||||
}
|
||||
}
|
||||
|
||||
case 'Assignment': {
|
||||
const [identifier, expr] = getChildren(node)
|
||||
|
||||
const identifierNode = assertNode(identifier, 'Identifier')
|
||||
const exprNode = assertNode(expr, 'Expression')
|
||||
|
||||
const name = input.slice(identifierNode.from, identifierNode.to)
|
||||
const value = evaluateNode(exprNode, input, context)
|
||||
context.set(name, value)
|
||||
return value
|
||||
}
|
||||
|
||||
case 'Function': {
|
||||
const [params, body] = getChildren(node)
|
||||
|
||||
const paramNodes = getChildren(assertNode(params, 'Parameters'))
|
||||
const bodyNode = assertNode(body, 'Body')
|
||||
|
||||
const paramNames = paramNodes.map((param) => {
|
||||
const paramNode = assertNode(param, 'Identifier')
|
||||
return input.slice(paramNode.from, paramNode.to)
|
||||
})
|
||||
|
||||
return (...args: any[]) => {
|
||||
if (args.length !== paramNames.length) {
|
||||
throw new Error(`Expected ${paramNames.length} arguments, but got ${args.length}`)
|
||||
}
|
||||
|
||||
const localContext = new Map(context)
|
||||
paramNames.forEach((param, index) => {
|
||||
localContext.set(param, args[index])
|
||||
})
|
||||
|
||||
return evaluateNode(bodyNode, input, localContext)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported node type: ${node.name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error evaluating node "${value}"\n${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const assertNode = (node: any, expectedName: string): SyntaxNode => {
|
||||
if (!node) {
|
||||
throw new Error(`Expected "${expectedName}", but got undefined`)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
36
src/evaluator/treeHelper.ts
Normal file
36
src/evaluator/treeHelper.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { type SyntaxNodeRef } from '@lezer/common'
|
||||
|
||||
export const nodeToString = (nodeRef: SyntaxNodeRef, input: string, maxDepth = 10) => {
|
||||
const lines: string[] = []
|
||||
|
||||
function addNode(currentNodeRef: SyntaxNodeRef, depth = 0) {
|
||||
if (depth > maxDepth) {
|
||||
lines.push(' '.repeat(depth) + '...')
|
||||
return
|
||||
}
|
||||
|
||||
const indent = ' '.repeat(depth)
|
||||
const text = input.slice(currentNodeRef.from, currentNodeRef.to)
|
||||
const singleTokens = ['+', '-', '*', '/', '->', 'fn', '=', 'equals']
|
||||
|
||||
let child = currentNodeRef.node.firstChild
|
||||
if (child) {
|
||||
lines.push(`${indent}${currentNodeRef.name}`)
|
||||
while (child) {
|
||||
addNode(child, depth + 1)
|
||||
child = child.nextSibling
|
||||
}
|
||||
} else {
|
||||
const cleanText = currentNodeRef.name === 'String' ? text.slice(1, -1) : text
|
||||
if (singleTokens.includes(currentNodeRef.name)) {
|
||||
lines.push(`${indent}${currentNodeRef.name}`)
|
||||
} else {
|
||||
lines.push(`${indent}${currentNodeRef.name} ${cleanText}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addNode(nodeRef)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
50
src/index.css
Normal file
50
src/index.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@apply text-[rgba(255,255,255,0.87)] bg-[#242424] font-sans;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply grid place-items-center min-w-[320px] min-h-screen relative m-0;
|
||||
}
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
opacity: 0.05;
|
||||
background: url("./logo.svg");
|
||||
background-size: 256px;
|
||||
transform: rotate(-12deg) scale(1.35);
|
||||
animation: slide 30s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 256px 224px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
12
src/index.html
Normal file
12
src/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Shrimp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
7
src/parser/highlight.js
Normal file
7
src/parser/highlight.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { styleTags, tags } from '@lezer/highlight'
|
||||
|
||||
export const highlighting = styleTags({
|
||||
Identifier: tags.name,
|
||||
Number: tags.number,
|
||||
String: tags.string,
|
||||
})
|
||||
52
src/parser/shrimp.grammar
Normal file
52
src/parser/shrimp.grammar
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
@external propSource highlighting from "./highlight.js"
|
||||
|
||||
@top Program { expr* }
|
||||
|
||||
@skip { space }
|
||||
|
||||
@tokens {
|
||||
@precedence { fn Boolean Identifier }
|
||||
|
||||
space { @whitespace+ }
|
||||
Number { $[0-9]+ ('.' $[0-9]+)? }
|
||||
Boolean { "true" | "false" }
|
||||
String { '"' !["]* '"' }
|
||||
Identifier { $[A-Za-z_]$[A-Za-z_0-9-]* }
|
||||
fn { "fn" }
|
||||
arrow { "->" }
|
||||
equals { "=" }
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
leftParen { "(" }
|
||||
rightParen { ")" }
|
||||
}
|
||||
|
||||
@precedence {
|
||||
multiplicative @left,
|
||||
additive @left,
|
||||
function @right
|
||||
assignment @right
|
||||
}
|
||||
|
||||
expr {
|
||||
Assignment |
|
||||
Function |
|
||||
BinOp |
|
||||
atom
|
||||
}
|
||||
|
||||
BinOp {
|
||||
expr !multiplicative "*" expr |
|
||||
expr !multiplicative "/" expr |
|
||||
expr !additive "+" expr |
|
||||
expr !additive "-" expr
|
||||
}
|
||||
|
||||
Params { Identifier* }
|
||||
Function { !function fn Params arrow expr }
|
||||
|
||||
atom { Identifier | Number | String | Boolean | leftParen expr rightParen }
|
||||
|
||||
Assignment { Identifier !assignment equals expr }
|
||||
11
src/parser/shrimp.terms.ts
Normal file
11
src/parser/shrimp.terms.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
Program = 1,
|
||||
Assignment = 2,
|
||||
Identifier = 3,
|
||||
Function = 4,
|
||||
Params = 5,
|
||||
BinOp = 6,
|
||||
Number = 11,
|
||||
String = 12,
|
||||
Boolean = 13
|
||||
208
src/parser/shrimp.test.ts
Normal file
208
src/parser/shrimp.test.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import { expectTree, regenerateParser } from '@/parser/test-helper'
|
||||
import { beforeAll, describe, test } from 'bun:test'
|
||||
|
||||
describe('BinOp', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('addition tests', () => {
|
||||
expectTree('2 + 3').toMatch(`
|
||||
BinOp
|
||||
Number 2
|
||||
+
|
||||
Number 3
|
||||
`)
|
||||
})
|
||||
|
||||
test('subtraction tests', () => {
|
||||
expectTree('5 - 2').toMatch(`
|
||||
BinOp
|
||||
Number 5
|
||||
-
|
||||
Number 2
|
||||
`)
|
||||
})
|
||||
|
||||
test('multiplication tests', () => {
|
||||
expectTree('4 * 3').toMatch(`
|
||||
BinOp
|
||||
Number 4
|
||||
*
|
||||
Number 3
|
||||
`)
|
||||
})
|
||||
|
||||
test('division tests', () => {
|
||||
expectTree('8 / 2').toMatch(`
|
||||
BinOp
|
||||
Number 8
|
||||
/
|
||||
Number 2
|
||||
`)
|
||||
})
|
||||
|
||||
test('mixed operations with precedence', () => {
|
||||
expectTree('2 + 3 * 4 - 5 / 1').toMatch(`
|
||||
BinOp
|
||||
BinOp
|
||||
Number 2
|
||||
+
|
||||
BinOp
|
||||
Number 3
|
||||
*
|
||||
Number 4
|
||||
-
|
||||
BinOp
|
||||
Number 5
|
||||
/
|
||||
Number 1
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fn', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('parses function with single parameter', () => {
|
||||
expectTree('fn x -> x + 1').toMatch(`
|
||||
Function
|
||||
Params
|
||||
Identifier x
|
||||
BinOp
|
||||
Identifier x
|
||||
+
|
||||
Number 1`)
|
||||
})
|
||||
|
||||
test('parses function with multiple parameters', () => {
|
||||
expectTree('fn x y -> x * y').toMatch(`
|
||||
Function
|
||||
Params
|
||||
Identifier x
|
||||
Identifier y
|
||||
BinOp
|
||||
Identifier x
|
||||
*
|
||||
Identifier y`)
|
||||
})
|
||||
|
||||
test('parses nested functions', () => {
|
||||
expectTree('fn x -> fn y -> x + y').toMatch(`
|
||||
Function
|
||||
Params
|
||||
Identifier x
|
||||
Function
|
||||
Params
|
||||
Identifier y
|
||||
BinOp
|
||||
Identifier x
|
||||
+
|
||||
Identifier y`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Identifier', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('parses hyphenated identifiers correctly', () => {
|
||||
expectTree('my-var - another-var').toMatch(`
|
||||
BinOp
|
||||
Identifier my-var
|
||||
-
|
||||
Identifier another-var`)
|
||||
|
||||
expectTree('double--trouble - another-var').toMatch(`
|
||||
BinOp
|
||||
Identifier double--trouble
|
||||
-
|
||||
Identifier another-var`)
|
||||
|
||||
expectTree('tail-- - another-var').toMatch(`
|
||||
BinOp
|
||||
Identifier tail--
|
||||
-
|
||||
Identifier another-var`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Assignment', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('parses assignment with addition', () => {
|
||||
expectTree('x = 5 + 3').toMatch(`
|
||||
Assignment
|
||||
Identifier x
|
||||
BinOp
|
||||
Number 5
|
||||
+
|
||||
Number 3`)
|
||||
})
|
||||
|
||||
test('parses assignment with functions', () => {
|
||||
expectTree('add = fn a b -> a + b').toMatch(`
|
||||
Assignment
|
||||
Identifier add
|
||||
Function
|
||||
Params
|
||||
Identifier a
|
||||
Identifier b
|
||||
BinOp
|
||||
Identifier a
|
||||
+
|
||||
Identifier b`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parentheses', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('parses expressions with parentheses correctly', () => {
|
||||
expectTree('(2 + 3) * 4').toMatch(`
|
||||
BinOp
|
||||
BinOp
|
||||
Number 2
|
||||
+
|
||||
Number 3
|
||||
*
|
||||
Number 4`)
|
||||
})
|
||||
|
||||
test('parses nested parentheses correctly', () => {
|
||||
expectTree('((1 + 2) * (3 - 4)) / 5').toMatch(`
|
||||
BinOp
|
||||
BinOp
|
||||
BinOp
|
||||
Number 1
|
||||
+
|
||||
Number 2
|
||||
*
|
||||
BinOp
|
||||
Number 3
|
||||
-
|
||||
Number 4
|
||||
/
|
||||
Number 5`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiline', () => {
|
||||
beforeAll(() => regenerateParser())
|
||||
|
||||
test('parses multiline expressions', () => {
|
||||
expectTree(`
|
||||
5 + 4
|
||||
fn x -> x - 1
|
||||
`).toMatch(`
|
||||
BinOp
|
||||
Number 5
|
||||
+
|
||||
Number 4
|
||||
Function
|
||||
Params
|
||||
Identifier x
|
||||
BinOp
|
||||
Identifier x
|
||||
-
|
||||
Number 1
|
||||
`)
|
||||
})
|
||||
})
|
||||
18
src/parser/shrimp.ts
Normal file
18
src/parser/shrimp.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {highlighting} from "./highlight.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "$OQVQPOOOkQPO'#CsO!fQQO'#C`O!nQPO'#CjOOQO'#Cs'#CsOVQPO'#CsOOQO'#Co'#CoQVQPOOOVQPO,58xOOQO'#Ck'#CkO#cQQO'#CaO#kQQO,58zOVQPO,58|OVQPO,58|O#pQPO,59_OOQO-E6h-E6hO$RQPO1G.dOOQO-E6i-E6iOVQPO1G.fOOQO1G.h1G.hO$yQPO1G.hOOQO1G.y1G.yO%qQPO7+$Q",
|
||||
stateData: "&n~ObOS~ORPOZSO[SO]SOeQOhTO~OdWORgXVgXWgXXgXYgXZgX[gX]gX`gXegXhgXigX~ORXOfTP~OV[OW[OX]OY]OR^XZ^X[^X]^X`^Xe^Xh^X~ORXOfTX~OfbO~OV[OW[OX]OY]OieO~OV[OW[OX]OY]ORQiZQi[Qi]Qi`QieQihQiiQi~OV[OW[ORUiXUiYUiZUi[Ui]Ui`UieUihUiiUi~OV[OW[OX]OY]ORSqZSq[Sq]Sq`SqeSqhSqiSq~Oe]R]~",
|
||||
goto: "!fhPPiPiriPPPPPPPu{PPP!RPPPi_UOTVW[]bRZQQVOR_VQYQRaYSROVQ^TQ`WQc[Qd]Rfb",
|
||||
nodeNames: "⚠ Program Assignment Identifier Function Params BinOp * / + - Number String Boolean",
|
||||
maxTerm: 25,
|
||||
propSources: [highlighting],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 2,
|
||||
tokenData: "*f~RjX^!spq!srs#hxy$Vyz$[z{$a{|$f}!O$k!P!Q$x!Q![$}!_!`%h!c!}%m#R#S%m#T#Y%m#Y#Z&R#Z#h%m#h#i)`#i#o%m#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~!xYb~X^!spq!s#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~#kTOr#hrs#zs;'S#h;'S;=`$P<%lO#h~$PO[~~$SP;=`<%l#h~$[Oh~~$aOi~~$fOV~~$kOX~R$pPYP!`!a$sQ$xOfQ~$}OW~~%SQZ~!O!P%Y!Q![$}~%]P!Q![%`~%ePZ~!Q![%`~%mOd~~%rTR~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~&WWR~}!O%m!Q![%m!c!}%m#R#S%m#T#U&p#U#b%m#b#c(x#c#o%m~&uVR~}!O%m!Q![%m!c!}%m#R#S%m#T#`%m#`#a'[#a#o%m~'aVR~}!O%m!Q![%m!c!}%m#R#S%m#T#g%m#g#h'v#h#o%m~'{VR~}!O%m!Q![%m!c!}%m#R#S%m#T#X%m#X#Y(b#Y#o%m~(iT]~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)PTe~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)eVR~}!O%m!Q![%m!c!}%m#R#S%m#T#f%m#f#g)z#g#o%m~*PVR~}!O%m!Q![%m!c!}%m#R#S%m#T#i%m#i#j'v#j#o%m",
|
||||
tokenizers: [0, 1],
|
||||
topRules: {"Program":[0,1]},
|
||||
tokenPrec: 255
|
||||
})
|
||||
83
src/parser/test-helper.ts
Normal file
83
src/parser/test-helper.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { beforeAll, expect } from 'bun:test'
|
||||
import { Tree, TreeCursor } from '@lezer/common'
|
||||
import grammarFile from './shrimp.grammar'
|
||||
import { parser } from './shrimp.ts'
|
||||
import { $ } from 'bun'
|
||||
|
||||
// Regenerate the parser if the grammar file is newer than the generated parser
|
||||
// This makes --watch work without needing to manually regenerate the parser
|
||||
export const regenerateParser = async () => {
|
||||
const grammarStat = await Bun.file('src/parser/shrimp.grammar').stat()
|
||||
const jsStat = await Bun.file('src/parser/shrimp.ts').stat()
|
||||
|
||||
if (grammarStat.mtime <= jsStat.mtime) return
|
||||
|
||||
console.log(`Regenerating parser from ${grammarFile}...`)
|
||||
await $`bun generate-parser `
|
||||
}
|
||||
|
||||
export const expectTree = (input: string) => {
|
||||
const tree = parser.parse(input)
|
||||
return {
|
||||
toMatch: (expected: string) => {
|
||||
expect(treeToString(tree, input)).toEqual(trimWhitespace(expected))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const treeToString = (tree: Tree, input: string): string => {
|
||||
const lines: string[] = []
|
||||
|
||||
const addNode = (cursor: TreeCursor, depth: number) => {
|
||||
if (!cursor.name) return
|
||||
|
||||
const indent = ' '.repeat(depth)
|
||||
const text = input.slice(cursor.from, cursor.to)
|
||||
const nodeName = cursor.name // Save the node name before moving cursor
|
||||
|
||||
if (cursor.firstChild()) {
|
||||
lines.push(`${indent}${nodeName}`)
|
||||
do {
|
||||
addNode(cursor, depth + 1)
|
||||
} while (cursor.nextSibling())
|
||||
cursor.parent()
|
||||
} else {
|
||||
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
|
||||
// Node names that should be displayed as single tokens (operators, keywords)
|
||||
const singleTokens = ['+', '-', '*', '/', '->']
|
||||
if (singleTokens.includes(nodeName)) {
|
||||
lines.push(`${indent}${nodeName}`)
|
||||
} else {
|
||||
lines.push(`${indent}${nodeName} ${cleanText}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cursor = tree.cursor()
|
||||
if (cursor.firstChild()) {
|
||||
do {
|
||||
addNode(cursor, 0)
|
||||
} while (cursor.nextSibling())
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const trimWhitespace = (str: string): string => {
|
||||
const lines = str.split('\n').filter((line) => line.trim().length > 0)
|
||||
const firstLine = lines[0]
|
||||
if (!firstLine) return ''
|
||||
|
||||
const leadingWhitespace = firstLine.match(/^(\s*)/)?.[1] || ''
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (!line.startsWith(leadingWhitespace)) {
|
||||
let foundWhitespace = line.match(/^(\s*)/)?.[1] || ''
|
||||
throw new Error(
|
||||
`Line has inconsistent leading whitespace: "${line}" (found "${foundWhitespace}", expected "${leadingWhitespace}")`
|
||||
)
|
||||
}
|
||||
return line.slice(leadingWhitespace.length)
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
30
src/server.tsx
Normal file
30
src/server.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { serve } from "bun";
|
||||
import index from "./index.html";
|
||||
|
||||
const server = serve({
|
||||
routes: {
|
||||
"/*": index,
|
||||
|
||||
"/api/hello": {
|
||||
async GET(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
async PUT(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "PUT",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
development: process.env.NODE_ENV !== "production" && {
|
||||
hmr: true,
|
||||
console: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`🚀 Server running at ${server.url}`);
|
||||
36
tsconfig.json
Normal file
36
tsconfig.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user