Complete Story 1-1 and fix TypeScript issues

Add TypeScript strict mode and typecheck tasks to monorepo infrastructure.
Fix E2E test @payload-config alias and frontend TypeScript errors.

- Add tsconfig.json to backend with strict mode and path aliases
- Add typecheck task to Turborepo and all packages
- Fix @payload-config alias for E2E tests and dev server
- Add setToken method to AuthService for middleware use
- Fix implicit any types in Footer.astro and Header.astro
- Remove invalid typescript config from astro.config.mjs
This commit is contained in:
2026-01-31 17:12:47 +08:00
parent d0e8c3bcff
commit 0846318d6e
15 changed files with 137 additions and 61 deletions

View File

@@ -20,17 +20,20 @@
"start": "cross-env NODE_OPTIONS=--no-deprecation next start", "start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"test": "pnpm run test:int && pnpm run test:e2e", "test": "pnpm run test:int && pnpm run test:e2e",
"test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" pnpm exec playwright test --config=playwright.config.ts", "test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" pnpm exec playwright test --config=playwright.config.ts",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts" "test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts",
"test:load": "k6 run tests/k6/public-browsing.js",
"test:load:all": "k6 run tests/k6/public-browsing.js && k6 run tests/k6/api-performance.js",
"test:load:admin": "k6 run tests/k6/admin-operations.js",
"test:load:api": "k6 run tests/k6/api-performance.js",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@opennextjs/cloudflare": "^1.10.1", "@enchun/shared": "workspace:*",
"@payloadcms/admin-bar": "3.59.1", "@payloadcms/admin-bar": "3.59.1",
"@payloadcms/db-mongodb": "3.59.1", "@payloadcms/db-mongodb": "3.59.1",
"@payloadcms/email-resend": "3.59.1", "@payloadcms/email-resend": "3.59.1",
"@payloadcms/live-preview-react": "3.59.1", "@payloadcms/live-preview-react": "3.59.1",
"@payloadcms/next": "3.59.1", "@payloadcms/next": "3.59.1",
"@payloadcms/payload-cloud": "3.59.1",
"@payloadcms/plugin-form-builder": "3.59.1",
"@payloadcms/plugin-nested-docs": "3.59.1", "@payloadcms/plugin-nested-docs": "3.59.1",
"@payloadcms/plugin-redirects": "3.59.1", "@payloadcms/plugin-redirects": "3.59.1",
"@payloadcms/plugin-search": "3.59.1", "@payloadcms/plugin-search": "3.59.1",
@@ -58,8 +61,7 @@
"react-hook-form": "7.45.4", "react-hook-form": "7.45.4",
"sharp": "0.34.2", "sharp": "0.34.2",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7"
"@enchun/shared": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",

View File

@@ -1,13 +1,27 @@
import type { GlobalAfterChangeHook } from 'payload' import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache' import { revalidateTag } from 'next/cache'
import { auditLogger } from '@/utilities/auditLogger'
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { export const revalidateFooter: GlobalAfterChangeHook = async ({ doc, req }) => {
const { payload, context } = req
if (!context.disableRevalidate) { if (!context.disableRevalidate) {
payload.logger.info(`Revalidating footer`) payload.logger.info(`Revalidating footer`)
revalidateTag('global_footer') revalidateTag('global_footer')
} }
// 記錄 Footer 變更
if (req.user) {
await auditLogger(req, {
action: 'update',
collection: 'global_footer',
userId: req.user.id,
userName: req.user.name as string,
userEmail: req.user.email as string,
userRole: req.user.role as string,
})
}
return doc return doc
} }

View File

@@ -1,13 +1,27 @@
import type { GlobalAfterChangeHook } from 'payload' import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache' import { revalidateTag } from 'next/cache'
import { auditLogger } from '@/utilities/auditLogger'
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { export const revalidateHeader: GlobalAfterChangeHook = async ({ doc, req }) => {
const { payload, context } = req
if (!context.disableRevalidate) { if (!context.disableRevalidate) {
payload.logger.info(`Revalidating header`) payload.logger.info(`Revalidating header`)
revalidateTag('global_header') revalidateTag('global_header')
} }
// 記錄 Header 變更
if (req.user) {
await auditLogger(req, {
action: 'update',
collection: 'global_header',
userId: req.user.id,
userName: req.user.name as string,
userEmail: req.user.email as string,
userRole: req.user.role as string,
})
}
return doc return doc
} }

View File

@@ -1,51 +1,55 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"target": "ES2022", "target": "ES2022",
"module": "ESNext",
"lib": [ "lib": [
"DOM",
"DOM.Iterable",
"ES2022" "ES2022"
], ],
"allowJs": true, "moduleResolution": "Bundler",
"skipLibCheck": true,
"noEmit": true,
"incremental": true,
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "allowJs": false,
"isolatedModules": true, "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
],
"@payload-config": [
"./src/payload.config"
]
},
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
], ],
"paths": { "noEmit": true,
"@payload-config": [ "incremental": true,
"./src/payload.config.ts" "isolatedModules": true
],
"react": [
"./node_modules/@types/react"
],
"@/*": [
"./src/*"
],
}
}, },
"include": [ "include": [
"**/*.ts", "src/**/*",
"**/*.tsx",
".next/types/**/*.ts",
"redirects.js",
"next-env.d.ts",
"next.config.js", "next.config.js",
"next-sitemap.config.cjs" ".next/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules",
], ".next",
"dist",
"tests/**/*",
"**/__tests__/**/*",
"**/*.spec.ts",
"**/*.test.ts",
"vitest.config.mts",
"playwright.config.ts"
]
} }

View File

@@ -1,9 +1,15 @@
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths' import tsconfigPaths from 'vite-tsconfig-paths'
import path from 'node:path'
export default defineConfig({ export default defineConfig({
plugins: [tsconfigPaths(), react()], plugins: [tsconfigPaths(), react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: { test: {
environment: 'jsdom', environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'], setupFiles: ['./vitest.setup.ts'],

View File

@@ -6,7 +6,13 @@ import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
output: "server", output: "server",
adapter: cloudflare(), adapter: cloudflare({
imageService: 'passthrough',
platformProxy: {
enabled: true,
configPath: './wrangler.jsonc',
},
}),
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],
server: { server: {
@@ -19,7 +25,4 @@ export default defineConfig({
}, },
}, },
}, },
typescript: {
strict: true,
},
}); });

View File

@@ -9,18 +9,21 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"check": "astro check", "check": "astro check",
"deploy": "wrangler pages deploy dist" "typecheck": "astro check",
"deploy": "wrangler deploy"
}, },
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "^12.6.9", "@astrojs/cloudflare": "^12.6.12",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"astro": "^5.14.1", "astro": "6.0.0-beta.1",
"better-auth": "^1.3.13" "better-auth": "^1.3.13"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.6",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"typescript": "^5.4.0" "typescript": "^5.7.3",
"wrangler": "^4.59.2"
} }
} }

View File

@@ -51,6 +51,13 @@ import { Image } from 'astro:assets';
<script> <script>
// Client-side data fetching for footer // Client-side data fetching for footer
interface LinkItem {
link?: {
url?: string;
label?: string;
};
}
async function loadFooterData() { async function loadFooterData() {
try { try {
console.log('Fetching footer data...'); console.log('Fetching footer data...');
@@ -61,7 +68,7 @@ import { Image } from 'astro:assets';
// Update marketing solutions // Update marketing solutions
const marketingUl = document.getElementById('marketing-solutions'); const marketingUl = document.getElementById('marketing-solutions');
if (marketingUl && data.navItems?.[0]?.childNavItems) { if (marketingUl && data.navItems?.[0]?.childNavItems) {
const links = data.navItems[0].childNavItems.map(item => const links = data.navItems[0].childNavItems.map((item: LinkItem) =>
`<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>` `<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>`
).join(''); ).join('');
marketingUl.innerHTML = links; marketingUl.innerHTML = links;
@@ -70,7 +77,7 @@ import { Image } from 'astro:assets';
// Update marketing articles (行銷放大鏡) // Update marketing articles (行銷放大鏡)
const articlesUl = document.getElementById('marketing-articles'); const articlesUl = document.getElementById('marketing-articles');
if (articlesUl && data.navItems?.[1]?.childNavItems) { if (articlesUl && data.navItems?.[1]?.childNavItems) {
const links = data.navItems[1].childNavItems.map(item => const links = data.navItems[1].childNavItems.map((item: LinkItem) =>
`<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>` `<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>`
).join(''); ).join('');
articlesUl.innerHTML = links; articlesUl.innerHTML = links;

View File

@@ -135,7 +135,7 @@ import { Image } from "astro:assets";
mobileNav.innerHTML = ""; mobileNav.innerHTML = "";
// Populate desktop navigation // Populate desktop navigation
navItems.forEach((item) => { navItems.forEach((item: NavItem) => {
const linkHtml = createNavLink(item); const linkHtml = createNavLink(item);
const li = document.createElement("li"); const li = document.createElement("li");
li.innerHTML = linkHtml; li.innerHTML = linkHtml;
@@ -143,7 +143,7 @@ import { Image } from "astro:assets";
}); });
// Populate mobile navigation // Populate mobile navigation
navItems.forEach((item) => { navItems.forEach((item: NavItem) => {
const linkHtml = createNavLink(item) const linkHtml = createNavLink(item)
.replace("px-3 py-2", "block px-3 py-2") .replace("px-3 py-2", "block px-3 py-2")
.replace( .replace(

View File

@@ -15,7 +15,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
} }
// Validate token // Validate token
authService.token = token; authService.setToken(token);
const user = await authService.getCurrentUser(); const user = await authService.getCurrentUser();
if (!user) { if (!user) {

View File

@@ -61,6 +61,7 @@ export class AuthService {
} }
async logout(): Promise<void> { async logout(): Promise<void> {
const tokenForRequest = this.token;
this.token = null; this.token = null;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.removeItem('payload-token'); localStorage.removeItem('payload-token');
@@ -68,17 +69,25 @@ export class AuthService {
// Optional: Call logout endpoint // Optional: Call logout endpoint
try { try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (tokenForRequest) {
headers['Authorization'] = `Bearer ${tokenForRequest}`;
}
await fetch(`${PAYLOAD_URL}/api/users/logout`, { await fetch(`${PAYLOAD_URL}/api/users/logout`, {
method: 'POST', method: 'POST',
headers: { headers,
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
},
}); });
} catch (error) { } catch (error) {
// Ignore logout errors // Ignore logout errors
} }
} }
setToken(token: string): void {
this.token = token;
}
async getCurrentUser(): Promise<User | null> { async getCurrentUser(): Promise<User | null> {
if (!this.token) return null; if (!this.token) return null;

View File

@@ -5,5 +5,6 @@
"paths": { "paths": {
"@shared/*": ["../packages/shared/src/*"] "@shared/*": ["../packages/shared/src/*"]
} }
} },
"exclude": ["node_modules", "dist", "tests"]
} }

View File

@@ -8,11 +8,13 @@
"build": "turbo run build", "build": "turbo run build",
"lint": "turbo run lint", "lint": "turbo run lint",
"test": "turbo run test", "test": "turbo run test",
"typecheck": "turbo run typecheck",
"bmad:refresh": "bmad-method install -f -i codex", "bmad:refresh": "bmad-method install -f -i codex",
"bmad:list": "bmad-method list:agents", "bmad:list": "bmad-method list:agents",
"bmad:validate": "bmad-method validate" "bmad:validate": "bmad-method validate"
}, },
"devDependencies": { "devDependencies": {
"eslint-plugin-react-hooks": "^7.0.1",
"turbo": "^2.0.5" "turbo": "^2.0.5"
}, },
"pnpm": { "pnpm": {

View File

@@ -5,10 +5,17 @@
"declarationDir": "dist", "declarationDir": "dist",
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"module": "ES2020", "module": "ES2020",
"moduleResolution": "Node", "moduleResolution": "Bundler",
"target": "ES2020", "target": "ES2020",
"rootDir": "src", "rootDir": "src",
"outDir": "dist" "outDir": "dist",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }

View File

@@ -18,6 +18,10 @@
}, },
"check": { "check": {
"outputs": [] "outputs": []
},
"typecheck": {
"dependsOn": ["^typecheck"],
"outputs": []
} }
} }
} }