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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/app/components/provider/serwist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client'

export { SerwistProvider } from '@serwist/turbopack/react'
60 changes: 33 additions & 27 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ToastProvider } from './components/base/toast'
import BrowserInitializer from './components/browser-initializer'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
import { I18nServerProvider } from './components/provider/i18n-server'
import { SerwistProvider } from './components/provider/serwist'
import SentryInitializer from './components/sentry-initializer'
import RoutePrefixHandle from './routePrefixHandle'
import './styles/globals.css'
Expand Down Expand Up @@ -39,6 +40,9 @@ const LocaleLayout = async ({
}) => {
const locale = await getLocaleOnServer()

const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const swUrl = `${basePath}/serwist/sw.js`

const datasetMap: Record<DatasetAttr, string | undefined> = {
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
[DatasetAttr.DATA_PUBLIC_API_PREFIX]: process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX,
Expand Down Expand Up @@ -92,33 +96,35 @@ const LocaleLayout = async ({
className="color-scheme h-full select-auto"
{...datasetMap}
>
<ReactScanLoader />
<JotaiProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
<SerwistProvider swUrl={swUrl}>
<ReactScanLoader />
<JotaiProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
</SerwistProvider>
</body>
</html>
)
Expand Down
14 changes: 14 additions & 0 deletions web/app/serwist/[path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { createSerwistRoute } from '@serwist/turbopack'

const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const revision = spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' }).stdout?.trim() || randomUUID()

export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute({
additionalPrecacheEntries: [{ url: `${basePath}/_offline.html`, revision }],
swSrc: 'app/sw.ts',
nextConfig: {
basePath,
},
})
104 changes: 104 additions & 0 deletions web/app/sw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />

import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'
import { CacheableResponsePlugin, CacheFirst, ExpirationPlugin, NetworkFirst, Serwist, StaleWhileRevalidate } from 'serwist'

declare global {
// eslint-disable-next-line ts/consistent-type-definitions
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined
}
}

declare const self: ServiceWorkerGlobalScope

const scopePathname = new URL(self.registration.scope).pathname
const basePath = scopePathname.replace(/\/serwist\/$/, '').replace(/\/$/, '')
const offlineUrl = `${basePath}/_offline.html`

const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: [
{
matcher: ({ url }) => url.origin === 'https://fonts.googleapis.com',
handler: new CacheFirst({
cacheName: 'google-fonts',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ url }) => url.origin === 'https://fonts.gstatic.com',
handler: new CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ request }) => request.destination === 'image',
handler: new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 64,
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ request }) => request.destination === 'script' || request.destination === 'style',
handler: new StaleWhileRevalidate({
cacheName: 'static-resources',
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60,
}),
],
}),
},
{
matcher: ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/api/'),
handler: new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
plugins: [
new ExpirationPlugin({
maxEntries: 16,
maxAgeSeconds: 60 * 60,
}),
],
}),
},
],
fallbacks: {
entries: [
{
url: offlineUrl,
matcher({ request }) {
return request.destination === 'document'
},
},
],
},
})

serwist.addEventListeners()
5 changes: 1 addition & 4 deletions web/knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ const config: KnipConfig = {
ignoreBinaries: [
'only-allow',
],
ignoreDependencies: [
// required by next-pwa
'babel-loader',
],
ignoreDependencies: [],
rules: {
files: 'warn',
dependencies: 'warn',
Expand Down
72 changes: 2 additions & 70 deletions web/next.config.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,8 @@
import withBundleAnalyzerInit from '@next/bundle-analyzer'
import createMDX from '@next/mdx'
import { codeInspectorPlugin } from 'code-inspector-plugin'
import withPWAInit from 'next-pwa'

const isDev = process.env.NODE_ENV === 'development'

const withPWA = withPWAInit({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
fallbacks: {
document: '/_offline.html',
},
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-webfonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 64,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
{
urlPattern: /\.(?:js|css)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-resources',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 1 day
},
},
},
{
urlPattern: /^\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 16,
maxAgeSeconds: 60 * 60, // 1 hour
},
},
},
],
})
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
Expand All @@ -97,6 +28,7 @@ const remoteImageURLs = [hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WE
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
serverExternalPackages: ['esbuild-wasm'],
transpilePackages: ['echarts', 'zrender'],
turbopack: {
rules: codeInspectorPlugin({
Expand Down Expand Up @@ -148,4 +80,4 @@ const nextConfig = {
},
}

export default withPWA(withBundleAnalyzer(withMDX(nextConfig)))
export default withBundleAnalyzer(withMDX(nextConfig))
6 changes: 3 additions & 3 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
"mitt": "^3.0.1",
"negotiator": "^1.0.0",
"next": "~15.5.9",
"next-pwa": "^5.6.0",
"next-themes": "^0.4.6",
"nuqs": "^2.8.6",
"pinyin-pro": "^3.27.0",
Expand Down Expand Up @@ -153,7 +152,6 @@
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.3",
"@babel/core": "^7.28.4",
"@chromatic-com/storybook": "^4.1.1",
"@eslint-react/eslint-plugin": "^2.3.13",
"@mdx-js/loader": "^3.1.1",
Expand All @@ -162,6 +160,7 @@
"@next/eslint-plugin-next": "15.5.9",
"@next/mdx": "15.5.9",
"@rgrove/parse-xml": "^4.2.0",
"@serwist/turbopack": "^9.5.0",
"@storybook/addon-docs": "9.1.13",
"@storybook/addon-links": "9.1.13",
"@storybook/addon-onboarding": "9.1.13",
Expand Down Expand Up @@ -194,9 +193,9 @@
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "4.0.16",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"code-inspector-plugin": "1.2.9",
"cross-env": "^10.1.0",
"esbuild-wasm": "^0.27.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
Expand All @@ -212,6 +211,7 @@
"postcss": "^8.5.6",
"react-scan": "^0.4.3",
"sass": "^1.93.2",
"serwist": "^9.5.0",
"storybook": "9.1.17",
"tailwindcss": "^3.4.18",
"tsx": "^4.21.0",
Expand Down
Loading
Loading