diff --git a/package.json b/package.json index 4e39ed057..b88e232dc 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "husky": "^9.0.0", "jsdom": "^24.0.0", "lint-staged": ">=10", - "rollup-plugin-visualizer": "^5.9.2", + "rollup-plugin-visualizer": "^5.12.0", "stylelint": "^16.10.0", "stylelint-config-standard-scss": "^13.1.0", "tailwindcss": "^3.4.13", diff --git a/vite.config.ts b/vite.config.ts index 797cf5115..bc5a9feeb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,119 +1,150 @@ -/* eslint-disable quotes */ /// import fs from 'node:fs'; import { fileURLToPath, URL } from 'node:url'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer'; -import { defineConfig } from 'vite'; +import { Connect, defineConfig, Plugin, UserConfig } from 'vite'; import checker from 'vite-plugin-checker'; import compileTime from 'vite-plugin-compile-time'; import { createHtmlPlugin } from 'vite-plugin-html'; import { VitePWA } from 'vite-plugin-pwa'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -const { NODE_ENV } = process.env; +const { NODE_ENV, PORT } = process.env; -// @ts-ignore -export default defineConfig(({ command }) => ({ - build: { - assetsDir: 'packs', - assetsInlineLimit: 0, - rollupOptions: { - output: { - assetFileNames: 'packs/assets/[name]-[hash].[ext]', - chunkFileNames: 'packs/js/[name]-[hash].js', - entryFileNames: 'packs/[name]-[hash].js', - }, - }, - sourcemap: true, - }, - assetsInclude: ['**/*.oga'], - server: { - port: Number(process.env.PORT ?? 3036), - }, - optimizeDeps: { - exclude: command === 'serve' ? ['@soapbox.pub/wasmboy'] : [], - }, - plugins: [ - checker({ typescript: true }), - compileTime(), - createHtmlPlugin({ - template: 'index.html', - minify: { - collapseWhitespace: true, - removeComments: false, - }, - inject: { - data: { - snippets: readFileContents('custom/snippets.html'), - csp: NODE_ENV === 'production' - ? "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self';" - : "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' blob: https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; img-src 'self' data: blob: https: http://localhost:* http://127.0.0.1:*; media-src 'self' https: http://localhost:* http://127.0.0.1:*; style-src 'self' 'unsafe-inline'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self';", +export default defineConfig(() => { + const config: UserConfig = { + build: { + assetsDir: 'packs', + assetsInlineLimit: 0, + rollupOptions: { + output: { + assetFileNames: 'packs/assets/[name]-[hash].[ext]', + chunkFileNames: 'packs/js/[name]-[hash].js', + entryFileNames: 'packs/[name]-[hash].js', }, }, - }), - react(), - VitePWA({ - injectRegister: null, - strategies: 'injectManifest', - injectManifest: { - injectionPoint: undefined, - plugins: [ - // @ts-ignore - compileTime(), - ], - }, - manifest: { - name: 'Soapbox', - short_name: 'Soapbox', - description: 'A social media frontend with a focus on custom branding and ease of use.', - }, - srcDir: 'src/service-worker', - filename: 'sw.ts', - }), - viteStaticCopy({ - targets: [{ - src: './node_modules/@twemoji/svg/*', - dest: 'packs/emoji/', - }, { - src: './src/instance', - dest: '.', - }, { - src: './custom/instance', - dest: '.', - }], - }), - visualizer({ - emitFile: true, - filename: 'report.html', - title: 'Soapbox Bundle', - }), - { - name: 'mock-api', - configureServer(server) { - server.middlewares.use((req, res, next) => { - if (/^\/api\//.test(req.url!)) { - res.statusCode = 404; - res.end('Not Found'); - } else { - next(); - } - }); - }, + sourcemap: true, }, - ], - resolve: { - alias: [ - { find: 'soapbox', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, + assetsInclude: ['**/*.oga'], + server: { + port: Number(PORT ?? 3036), + }, + plugins: [ + checker({ typescript: true }), + compileTime(), + createHtmlPlugin({ + template: 'index.html', + minify: { + collapseWhitespace: true, + removeComments: false, + }, + inject: { + data: { + snippets: readFileContents('custom/snippets.html'), + csp: buildCSP(NODE_ENV), + }, + }, + }), + react(), + VitePWA({ + injectRegister: null, + strategies: 'injectManifest', + injectManifest: { + injectionPoint: undefined, + plugins: [ + // @ts-ignore + compileTime(), + ], + }, + manifest: { + name: 'Soapbox', + short_name: 'Soapbox', + description: 'A social media frontend with a focus on custom branding and ease of use.', + }, + srcDir: 'src/service-worker', + filename: 'sw.ts', + }), + viteStaticCopy({ + targets: [{ + src: './node_modules/@twemoji/svg/*', + dest: 'packs/emoji/', + }, { + src: './src/instance', + dest: '.', + }, { + src: './custom/instance', + dest: '.', + }], + }), + visualizer({ + emitFile: true, + filename: 'report.html', + title: 'Soapbox Bundle', + }) as Plugin, + { + // Vite's default behavior is to serve index.html (HTTP 200) for unmatched routes, like a PWA. + // Instead, 404 on known backend routes to more closely match a real server. + name: 'vite-mastodon-dev', + configureServer(server) { + const notFoundHandler: Connect.SimpleHandleFunction = (_req, res) => { + res.statusCode = 404; + res.end(); + }; + + server.middlewares.use('/api/', notFoundHandler); + server.middlewares.use('/oauth/', notFoundHandler); + server.middlewares.use('/nodeinfo/', notFoundHandler); + server.middlewares.use('/.well-known/', notFoundHandler); + }, + }, ], - }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: 'src/jest/test-setup.ts', - }, -})); + resolve: { + alias: [ + { find: 'soapbox', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, + ], + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: 'src/jest/test-setup.ts', + }, + }; + + return config; +}); + +/** Build a sane default CSP string to embed in index.html in case the server doesn't return one. */ +/* eslint-disable quotes */ +function buildCSP(env: string | undefined): string { + const csp = [ + "default-src 'none'", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "frame-src 'self' https:", + "font-src 'self'", + "base-uri 'self'", + "manifest-src 'self'", + ]; + + if (env === 'development') { + csp.push( + "connect-src 'self' blob: https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*", + "img-src 'self' data: blob: https: http://localhost:* http://127.0.0.1:*", + "media-src 'self' https: http://localhost:* http://127.0.0.1:*", + ); + } else { + csp.push( + "connect-src 'self' blob: https: wss:", + "img-src 'self' data: blob: https:", + "media-src 'self' https:", + ); + } + + return csp.join('; '); +} +/* eslint-enable quotes */ /** Return file as string, or return empty string if the file isn't found. */ function readFileContents(path: string) { diff --git a/yarn.lock b/yarn.lock index 7ed31ca3e..19398bd64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7416,10 +7416,10 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup-plugin-visualizer@^5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.2.tgz#f1aa2d9b1be8ebd6869223c742324897464d8891" - integrity sha512-waHktD5mlWrYFrhOLbti4YgQCn1uR24nYsNuXxg7LkPH8KdTXVWR9DNY1WU0QqokyMixVXJS4J04HNrVTMP01A== +rollup-plugin-visualizer@^5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz#661542191ce78ee4f378995297260d0c1efb1302" + integrity sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ== dependencies: open "^8.4.0" picomatch "^2.3.1"