diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e50bb5e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,34 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + name: Deploy to Cloudflare Pages + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install + + - name: Build web package + run: cd packages/web && bun run build + + - name: Deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 184aa26..c6db93f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,19 +2,22 @@ name: Run Tests on: pull_request: - branches: [ main ] + branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Setup Bun uses: oven-sh/setup-bun@v1 - + - name: Install dependencies run: bun install - + + - name: Run type check + run: bun run tsc + - name: Run tests - run: bun test \ No newline at end of file + run: bun test diff --git a/bun.lock b/bun.lock index 76df28e..d307c99 100644 --- a/bun.lock +++ b/bun.lock @@ -7,12 +7,13 @@ "packages/lang": { "name": "@scorelang/lang", "dependencies": { + "@types/node": "^22.15.30", "cli-table3": "^0.6.5", "tiny-invariant": "^1.3.3", "ts-pattern": "^5.7.0", }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.2.15", }, "peerDependencies": { "typescript": "^5", @@ -23,6 +24,7 @@ "version": "0.0.0", "dependencies": { "@scorelang/lang": "*", + "@types/bun": "^1.2.15", "@types/node": "^22.15.30", "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", @@ -239,7 +241,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], - "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], @@ -305,7 +307,7 @@ "browserslist": ["browserslist@4.24.5", "", { "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" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="], - "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -795,8 +797,6 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "bun-types/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/packages/lang/package.json b/packages/lang/package.json index 681bbf3..04fd1e8 100644 --- a/packages/lang/package.json +++ b/packages/lang/package.json @@ -4,12 +4,13 @@ "type": "module", "private": true, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "^1.2.15" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { + "@types/node": "^22.15.30", "cli-table3": "^0.6.5", "tiny-invariant": "^1.3.3", "ts-pattern": "^5.7.0" @@ -18,7 +19,7 @@ "packages/*" ], "scripts": { - "build": "bun build ./src/index.ts --compile --outfile scorelang", + "build": "bun build ./src/cli.ts --compile --outfile scorelang", "tsc": "tsc --noEmit", "lint": "eslint ." } diff --git a/packages/lang/src/ast.ts b/packages/lang/src/ast.ts index 5fc9004..cb8afc6 100644 --- a/packages/lang/src/ast.ts +++ b/packages/lang/src/ast.ts @@ -10,7 +10,9 @@ export interface Statement extends Node { } export class Team { - constructor(private token: Token, private name: string) {} + constructor(private token: Token, private name: string) { + this.name; // disable-lint + } tokenLiteral(): string { return this.token.value; @@ -18,7 +20,9 @@ export class Team { } export class Score { - constructor(private token: Token, private value: number) {} + constructor(private token: Token, private value: number) { + this.token; // disable-lint + } tokenLiteral(): number { return this.value; diff --git a/packages/lang/src/cli.ts b/packages/lang/src/cli.ts new file mode 100644 index 0000000..ed2aaa1 --- /dev/null +++ b/packages/lang/src/cli.ts @@ -0,0 +1,30 @@ +import { match } from "ts-pattern"; +import { Evaluator } from "./evaluator"; +import Lexer from "./lexer"; +import { Parser } from "./parser"; +import invariant from "tiny-invariant"; +import { calculatePointsTable, printPointsTable } from "./utils"; + +async function main() { + // Try to read from file argument if provided + const args = process.argv.slice(2); + const input = await match(args.length > 0) + .with(true, async () => await Bun.file(args[0]!).text()) + .with(false, async () => await Bun.stdin.text()) + .exhaustive(); + + invariant(input, "invariant: input should not be empty"); + + const lexer = new Lexer(input); + const parser = new Parser(lexer); + const program = parser.parse(); + const evaluator = new Evaluator(); + const { results } = evaluator.evaluate(program); + + const pointsTable = calculatePointsTable(results); + printPointsTable(pointsTable); +} + +if (import.meta.main) { + await main(); +} diff --git a/packages/lang/src/index.ts b/packages/lang/src/index.ts index 9048477..3c53326 100644 --- a/packages/lang/src/index.ts +++ b/packages/lang/src/index.ts @@ -1,37 +1,6 @@ -import { match } from "ts-pattern"; -import { Evaluator, type GameResult } from "./evaluator"; -import Lexer from "./lexer"; -import { Parser } from "./parser"; -import invariant from "tiny-invariant"; -import { calculatePointsTable, printPointsTable } from "./utils"; - -// Re-export types and utilities +// Re-export types and utilities export type { GameResult } from "./evaluator"; export { calculatePointsTable } from "./utils"; export { Evaluator } from "./evaluator"; export { Lexer } from "./lexer"; export { Parser } from "./parser"; - -async function main() { - // Try to read from file argument if provided - const args = process.argv.slice(2); - const input = await match(args.length > 0) - .with(true, async () => await Bun.file(args[0]!).text()) - .with(false, async () => await Bun.stdin.text()) - .exhaustive(); - - invariant(input, "invariant: input should not be empty"); - - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parse(); - const evaluator = new Evaluator(); - const { results } = evaluator.evaluate(program); - - const pointsTable = calculatePointsTable(results); - printPointsTable(pointsTable); -} - -if (import.meta.main) { - await main(); -} diff --git a/packages/lang/src/lexer.test.ts b/packages/lang/src/lexer.test.ts index 34a92eb..c0836c1 100644 --- a/packages/lang/src/lexer.test.ts +++ b/packages/lang/src/lexer.test.ts @@ -55,7 +55,7 @@ describe("Lexer", () => { const token = lexer.nextToken(); expect(token).toEqual(expectedToken); }); - }) + }); it("should tokenize a string with team names with multiple spaces", () => { const input = `Manchester United 2-0 Arsenal;`; @@ -72,5 +72,5 @@ describe("Lexer", () => { const token = lexer.nextToken(); expect(token).toEqual(expectedToken); }); - }) + }); }); diff --git a/packages/lang/src/lexer.ts b/packages/lang/src/lexer.ts index f879188..067d460 100644 --- a/packages/lang/src/lexer.ts +++ b/packages/lang/src/lexer.ts @@ -57,7 +57,7 @@ export class Lexer { while (this.isLetter(this.ch) || this.ch === " ") { this.readChar(); } - return this.program.slice(startPosition, this.position).trim() + return this.program.slice(startPosition, this.position).trim(); } private readNumber(): string { diff --git a/packages/lang/src/utils.ts b/packages/lang/src/utils.ts index e7b4aa2..7503849 100644 --- a/packages/lang/src/utils.ts +++ b/packages/lang/src/utils.ts @@ -1,4 +1,5 @@ import type { GameResult } from "./evaluator"; +import Table from "cli-table3"; type TeamStats = { points: number; @@ -67,7 +68,6 @@ export const calculatePointsTable = (results: GameResult[]) => { }; export const printPointsTable = (pointsTable: Map) => { - const Table = require("cli-table3"); const sortedTeams = [...pointsTable.entries()].sort( (a, b) => b[1].points - a[1].points ); diff --git a/tsconfig.json b/packages/lang/tsconfig.json similarity index 88% rename from tsconfig.json rename to packages/lang/tsconfig.json index fbee189..085e077 100644 --- a/tsconfig.json +++ b/packages/lang/tsconfig.json @@ -23,6 +23,8 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + + "types": ["node", "bun"] } } diff --git a/packages/web/_headers b/packages/web/_headers new file mode 100644 index 0000000..3bcd28d --- /dev/null +++ b/packages/web/_headers @@ -0,0 +1,6 @@ +# Security headers for Cloudflare Pages +/* + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + X-XSS-Protection: 1; mode=block diff --git a/packages/web/package.json b/packages/web/package.json index d4b068a..ac7dd0a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@scorelang/lang": "*", + "@types/bun": "^1.2.15", "@types/node": "^22.15.30", "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", diff --git a/packages/web/src/worker.tsx b/packages/web/src/worker.tsx new file mode 100644 index 0000000..2509802 --- /dev/null +++ b/packages/web/src/worker.tsx @@ -0,0 +1,26 @@ +export default { + async fetch(request: Request, env: any): Promise { + const url = new URL(request.url); + + // Serve static files from the public directory + if (url.pathname.startsWith('/assets/') || url.pathname.endsWith('.css') || url.pathname.endsWith('.js')) { + // Let Cloudflare serve static assets + return env.ASSETS.fetch(request); + } + + // For all other routes, serve the index.html (SPA routing) + const indexResponse = await env.ASSETS.fetch(new Request(new URL('/index.html', request.url))); + + if (indexResponse.ok) { + return new Response(indexResponse.body, { + status: 200, + headers: { + 'Content-Type': 'text/html', + ...indexResponse.headers, + }, + }); + } + + return new Response('Not found', { status: 404 }); + }, +}; diff --git a/packages/web/tsconfig.app.json b/packages/web/tsconfig.app.json index d803d10..057a4c6 100644 --- a/packages/web/tsconfig.app.json +++ b/packages/web/tsconfig.app.json @@ -4,7 +4,7 @@ "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], - "types": ["vite/client"], + "types": ["vite/client", "node", "bun"], "module": "ESNext", "skipLibCheck": true, @@ -20,7 +20,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/packages/web/tsconfig.node.json b/packages/web/tsconfig.node.json index 9728af2..fa3809c 100644 --- a/packages/web/tsconfig.node.json +++ b/packages/web/tsconfig.node.json @@ -17,7 +17,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 8b0f57b..8a25b4a 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -1,7 +1,12 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], -}) + build: { + outDir: "dist", + sourcemap: true, + }, + publicDir: "public", +}); diff --git a/packages/web/wrangler.jsonc b/packages/web/wrangler.jsonc new file mode 100644 index 0000000..08dfb09 --- /dev/null +++ b/packages/web/wrangler.jsonc @@ -0,0 +1,21 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "scorelang", + "main": "src/worker.tsx", + "compatibility_date": "2024-09-23", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true + }, + "routes": [ + { + "pattern": "score.zoid.dev", + "custom_domain": true + } + ], + "assets": { + "directory": "./dist", + "binding": "ASSETS" + }, + "vars": {} +}