Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/frontend-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Deploy frontend

on:
push:
branches:
- main
- prod
paths:
- 'apps/frontend/**/*'
- 'packages/ui/**/*'
- 'packages/utils/**/*'
- 'packages/assets/**/*'
- '**/wrangler.jsonc'
- '**/pnpm-*.yaml'
pull_request:
workflow_dispatch:

jobs:
deploy:
if: github.repository_owner == 'modrinth'
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Configure environment
id: meta
run: |
echo "cmd=deploy" >> $GITHUB_OUTPUT
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "env=staging" >> $GITHUB_OUTPUT
echo "url=https://staging.modrinth.com" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" != "refs/heads/prod" ] && [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "env=staging" >> $GITHUB_OUTPUT
echo "url=https://git-${GITHUB_SHA::8}-frontend-staging.modrinth.workers.dev" >> $GITHUB_OUTPUT
echo "cmd=versions upload --preview-alias git-${GITHUB_SHA::8} --var PREVIEW:true" >> $GITHUB_OUTPUT
else
# Production env should be empty
echo "url=https://modrinth.com" >> $GITHUB_OUTPUT
fi

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
working-directory: ./apps/frontend
run: pnpm install

- name: Inject build variables
working-directory: ./apps/frontend
run: |
if [ "${{ steps.meta.outputs.env }}" == "staging" ]; then
echo "Injecting staging variables from wrangler.jsonc..."
jq -r '.env.staging.vars | to_entries[] | "export \(.key)=\(.value|@sh)"' wrangler.jsonc | source /dev/stdin
else
echo "Injecting production variables from wrangler.jsonc..."
jq -r '.vars | to_entries[] | "export \(.key)=\(.value|@sh)"' wrangler.jsonc | source /dev/stdin
fi

- name: Build frontend
working-directory: ./apps/frontend
run: pnpm build
env:
CF_PAGES_BRANCH: ${{ github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
CF_PAGES_URL: ${{ steps.meta.outputs.url }}

- name: Deploy Cloudflare Worker
id: wrangler
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
environment: ${{ steps.meta.outputs.env }}
workingDirectory: ./apps/frontend
packageManager: pnpm
wranglerVersion: '4.54.0'
command: ${{ steps.meta.outputs.cmd }}

- name: Purge cache
if: github.ref == 'refs/heads/prod'
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"hosts": ["modrinth.com", "www.modrinth.com", "staging.modrinth.com"]}' \
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
30 changes: 0 additions & 30 deletions .github/workflows/frontend-pages.yml

This file was deleted.

20 changes: 13 additions & 7 deletions apps/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
// Import directly from utils to avoid loading .vue files at config time
import { LOCALES } from '@modrinth/ui/src/composables/i18n.ts'
import serverSidedVue from '@vitejs/plugin-vue'
import { promises as fs } from 'fs'
import fs from 'fs/promises'
import { defineNuxtConfig } from 'nuxt/config'
import svgLoader from 'vite-svg-loader'

Expand Down Expand Up @@ -96,6 +95,11 @@ export default defineNuxtConfig({
},
}),
],
build: {
rollupOptions: {
external: ['cloudflare:workers'],
},
},
},
hooks: {
async 'nitro:config'(nitroConfig) {
Expand Down Expand Up @@ -183,7 +187,7 @@ export default defineNuxtConfig({
process.env.CF_PAGES_BRANCH ||
// @ts-ignore
globalThis.CF_PAGES_BRANCH ||
'master',
'main',
hash:
process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.CF_PAGES_COMMIT_SHA ||
Expand Down Expand Up @@ -242,6 +246,11 @@ export default defineNuxtConfig({
rollupConfig: {
// @ts-expect-error because of rolldown-vite - completely fine though
plugins: [serverSidedVue()],
external: ['cloudflare:workers'],
},
preset: 'cloudflare_module',
cloudflare: {
nodeCompat: true,
},
},
devtools: {
Expand Down Expand Up @@ -304,11 +313,8 @@ function getFeatureFlagOverrides() {

function getDomain() {
if (process.env.NODE_ENV === 'production') {
if (process.env.SITE_URL) {
return process.env.SITE_URL
}
// @ts-ignore
else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
// @ts-ignore
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL
} else if (process.env.HEROKU_APP_NAME) {
Expand Down
8 changes: 6 additions & 2 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"test": "nuxi build"
"test": "nuxi build",
"cf-deploy": "pnpm run build && wrangler deploy --env preview",
"cf-dev": "pnpm run build && wrangler dev --env preview",
"cf-typegen": "wrangler types"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",
Expand All @@ -30,7 +33,8 @@
"typescript": "^5.4.5",
"vite-svg-loader": "^5.1.0",
"vue-component-type-helpers": "^3.1.8",
"vue-tsc": "^2.0.24"
"vue-tsc": "^2.0.24",
"wrangler": "^4.54.0"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
Expand Down
24 changes: 23 additions & 1 deletion apps/frontend/src/composables/fetch.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
let cachedRateLimitKey = undefined
let rateLimitKeyPromise = undefined

async function getRateLimitKey(config) {
if (config.rateLimitKey) return config.rateLimitKey
if (cachedRateLimitKey !== undefined) return cachedRateLimitKey

if (!rateLimitKeyPromise) {
rateLimitKeyPromise = (async () => {
try {
const { env } = await import('cloudflare:workers')
return await env.RATE_LIMIT_IGNORE_KEY?.get()
} catch {
return undefined
}
})()
}

cachedRateLimitKey = await rateLimitKeyPromise
return cachedRateLimitKey
}

export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig()
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
Expand All @@ -7,7 +29,7 @@ export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
}

if (import.meta.server) {
options.headers['x-ratelimit-key'] = config.rateLimitKey
options.headers['x-ratelimit-key'] = await getRateLimitKey(config)
}

if (!skipAuth) {
Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/src/helpers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ import {
} from '@modrinth/api-client'
import type { Ref } from 'vue'

async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
try {
// @ts-expect-error only avail in workers env
const { env } = await import('cloudflare:workers')
return await env.RATE_LIMIT_IGNORE_KEY?.get()
} catch {
// Not running in Cloudflare Workers environment
return undefined
}
}

export function createModrinthClient(
auth: Ref<{ token: string | undefined }>,
config: { apiBaseUrl: string; archonBaseUrl: string; rateLimitKey?: string },
Expand All @@ -22,7 +33,7 @@ export function createModrinthClient(
const clientConfig: NuxtClientConfig = {
labrinthBaseUrl: config.apiBaseUrl,
archonBaseUrl: config.archonBaseUrl,
rateLimitKey: config.rateLimitKey,
rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore,
features: [
new AuthFeature({
token: async () => auth.value.token,
Expand Down
59 changes: 59 additions & 0 deletions apps/frontend/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "frontend",
"compatibility_date": "2025-12-10",
"main": "./.output/server/index.mjs",
"assets": {
"binding": "ASSETS",
"directory": "./.output/public/"
},
"compatibility_flags": ["nodejs_compat", "no_nodejs_compat_v2"],
"preview_urls": true,
"workers_dev": true,
"limits": {
"cpu_ms": 2000
},
"observability": {
"enabled": true,
"head_sampling_rate": 0.001
},
"keep_vars": false,
"secrets_store_secrets": [
{
"binding": "RATE_LIMIT_IGNORE_KEY",
"store_id": "c9024fef252d4a53adf513feca64417d",
"secret_name": "labrinth-production-ratelimit-key"
}
],
"vars": {
"ENVIRONMENT": "production",
"BASE_URL": "https://api.modrinth.com/v2/",
"BROWSER_BASE_URL": "https://api.modrinth.com/v2/",
"PYRO_BASE_URL": "https://archon.modrinth.com/",
"STRIPE_PUBLISHABLE_KEY": "pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5"
},
"env": {
"staging": {
"observability": {
"enabled": true,
"head_sampling_rate": 0.1
},
"routes": ["staging.modrinth.com/*"],
"vars": {
"ENVIRONMENT": "staging",
"BASE_URL": "https://staging-api.modrinth.com/v2/",
"BROWSER_BASE_URL": "https://staging-api.modrinth.com/v2/",
"PYRO_BASE_URL": "https://staging-archon.modrinth.com/",
"STRIPE_PUBLISHABLE_KEY": "pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b"
},
"secrets_store_secrets": [
{
"binding": "RATE_LIMIT_IGNORE_KEY",
"store_id": "c9024fef252d4a53adf513feca64417d",
"secret_name": "labrinth-staging-ratelimit-key"
}
],
"preview_urls": true
}
}
}
Loading
Loading