diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7b807566..b62d4b4a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,6 +39,7 @@ jobs: clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - name: 🐦 Set up Flutter id: flutter uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0 @@ -109,6 +110,7 @@ jobs: clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - name: 🐦 Set up Flutter id: flutter uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0 @@ -153,7 +155,7 @@ jobs: if: ${{ matrix.target == 'web' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: - name: app-${{ matrix.target }}-build + name: build-${{ matrix.target }} path: "./packages/app/build/" if-no-files-found: error @@ -171,6 +173,7 @@ jobs: clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - name: 🐦 Set up Flutter id: flutter uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0 @@ -217,6 +220,7 @@ jobs: clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - name: 🐦 Set up Flutter id: flutter uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0 @@ -273,6 +277,7 @@ jobs: clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - name: 🐦 Set up Flutter id: flutter uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0 @@ -285,21 +290,61 @@ jobs: - name: ✨ Verify formatting run: dart format . --output=none --set-exit-if-changed + deploy: + name: Deploy + needs: ["build"] + runs-on: ubuntu-latest + timeout-minutes: 2 + + permissions: + id-token: write # Needed to authenticate with Deno Deploy. + concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: 📚 Git checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: recursive + clean: true + persist-credentials: false + set-safe-directory: true + fetch-depth: 1 + sparse-checkout: packages/server + - name: ⚙️ Download build + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-web + path: "./packages/server/static" + - name: 🔧 Deploy + uses: denoland/deployctl@612f83df2b874c6908d68de5cf3f36a6538fa8f7 # 1.12.0 + with: + project: "harvest-hub" + entrypoint: ./src/server.ts + root: packages/server + include: | + src/ + static/ + deno.json + deno.lock + spell-check: name: Check Spelling needs: [] runs-on: ubuntu-latest steps: - - name: 📚 Git Checkout + - name: 📚 Git checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive clean: true persist-credentials: false set-safe-directory: true + fetch-depth: 1 - - name: 🪄 Spell Check + - name: 🪄 Spell check uses: streetsidesoftware/cspell-action@934c74da3775ac844ec89503f666f67efb427fed # v6.8.1 with: files: | @@ -331,14 +376,15 @@ jobs: runs-on: ubuntu-latest steps: - - name: 📚 Git Checkout + - name: 📚 Git checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive clean: true persist-credentials: false set-safe-directory: true - - name: 🕵️ Markdown linting + fetch-depth: 1 + - name: 🕵️ Lint Markdown uses: DavidAnson/markdownlint-cli2-action@db43aef879112c3119a410d69f66701e0d530809 # v17.0.0 id: markdownlint with: diff --git a/packages/server/deno.json b/packages/server/deno.json new file mode 100644 index 00000000..dd3b92d2 --- /dev/null +++ b/packages/server/deno.json @@ -0,0 +1,22 @@ +{ + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.11", + "@std/http": "jsr:@std/http@^1.0.13", + "@std/path": "jsr:@std/path@^1.0.8" + }, + "compilerOptions": { + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedLocals": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noUnusedParameters": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true + } +} diff --git a/packages/server/deno.lock b/packages/server/deno.lock new file mode 100644 index 00000000..507c63a3 --- /dev/null +++ b/packages/server/deno.lock @@ -0,0 +1,71 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@^1.0.11": "1.0.11", + "jsr:@std/cli@^1.0.12": "1.0.13", + "jsr:@std/encoding@^1.0.7": "1.0.7", + "jsr:@std/fmt@^1.0.5": "1.0.5", + "jsr:@std/html@^1.0.3": "1.0.3", + "jsr:@std/http@^1.0.13": "1.0.13", + "jsr:@std/internal@^1.0.5": "1.0.5", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/net@^1.0.4": "1.0.4", + "jsr:@std/path@^1.0.8": "1.0.8", + "jsr:@std/streams@^1.0.9": "1.0.9" + }, + "jsr": { + "@std/assert@1.0.11": { + "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/cli@1.0.13": { + "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" + }, + "@std/encoding@1.0.7": { + "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" + }, + "@std/fmt@1.0.5": { + "integrity": "0cfab43364bc36650d83c425cd6d99910fc20c4576631149f0f987eddede1a4d" + }, + "@std/html@1.0.3": { + "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" + }, + "@std/http@1.0.13": { + "integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e", + "dependencies": [ + "jsr:@std/cli", + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/html", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path", + "jsr:@std/streams" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@1.0.4": { + "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/streams@1.0.9": { + "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.11", + "jsr:@std/http@^1.0.13", + "jsr:@std/path@^1.0.8" + ] + } +} diff --git a/packages/server/src/server.test.ts b/packages/server/src/server.test.ts new file mode 100644 index 00000000..3d116e9a --- /dev/null +++ b/packages/server/src/server.test.ts @@ -0,0 +1,55 @@ +import { assertEquals, assertStringIncludes } from "@std/assert"; +import { join } from "@std/path"; +import { respond } from "./server.ts"; + +async function populateTempDir( + files: readonly { name: string; content: string }[] +): Promise { + const encoder = new TextEncoder(); + const tempDirPath = await Deno.makeTempDir(); + + const promises = files.map(async ({ name, content }) => { + await Deno.writeFile(join(tempDirPath, name), encoder.encode(content)); + }); + + await Promise.all(promises); + + return tempDirPath; +} + +Deno.test( + "serveDir should return the correct response for an html file", + async () => { + const tempDirPath = await populateTempDir([ + { name: "index.html", content: "" }, + { name: "index.pck", content: "" }, + { name: "index.wasm", content: "" }, + ]); + + const req = new Request("http://localhost"); + const res = await respond(req, tempDirPath); + await res.body?.cancel(); + + assertEquals(res.status, 200); + assertStringIncludes(res.headers.get("Content-Type") ?? "", "text/html"); + } +); + +Deno.test( + "serveDir should return the correct response for a wasm file", + async () => { + const tempDirPath = await populateTempDir([ + { name: "index.wasm", content: "" }, + ]); + + const req = new Request("http://localhost/index.wasm"); + const res = await respond(req, tempDirPath); + await res.body?.cancel(); + + assertEquals(res.status, 200); + assertStringIncludes( + res.headers.get("Content-Type") ?? "", + "application/wasm" + ); + } +); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts new file mode 100644 index 00000000..13eee5e3 --- /dev/null +++ b/packages/server/src/server.ts @@ -0,0 +1,19 @@ +import { serveDir } from "@std/http"; + +export async function respond(req: Request, dir = "static"): Promise { + const res = await serveDir(req, { + fsRoot: dir, + enableCors: true, + }); + + res.headers.set("Cross-Origin-Opener-Policy", "same-origin"); + res.headers.set("Cross-Origin-Embedder-Policy", " require-corp"); + + return res; +} + +export default { + fetch: async (req: Request): Promise => { + return await respond(req); + }, +} satisfies Deno.ServeDefaultExport;