PronounsPage/nuxt.config.ts
2024-09-12 10:11:25 +02:00

567 lines
20 KiB
TypeScript

import './src/dotenv.ts';
import { loadSuml } from './server/loader.ts';
import fs from 'fs';
import path from 'path';
import { defineNuxtConfig } from 'nuxt/config';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import replacePlugin from '@rollup/plugin-replace';
import yamlPlugin from '@rollup/plugin-yaml';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import type { IncomingMessage } from 'connect';
import type { ServerResponse } from 'http';
import { buildList } from './src/helpers.ts';
import buildLocaleList from './src/buildLocaleList.ts';
import formatError from './src/error.ts';
import type { Config } from './locale/config.ts';
import type { Translations } from './locale/translations.ts';
import mdPlugin from './plugins/rollup/md.ts';
import sumlPlugin from './plugins/rollup/suml.ts';
import tsvPlugin from './plugins/rollup/tsv.ts';
const config = loadSuml('config') as Config;
const translations = loadSuml('translations') as Translations;
const locale = config.locale;
const locales = buildLocaleList(locale);
const title = translations.title;
const description = translations.description;
const colour = '#C71585';
let __dirname = new URL('.', import.meta.url).pathname;
if (process.platform === 'win32') {
// A small hack, for Windows can't have nice things.
__dirname = __dirname.slice(1);
}
const versionFile = `${__dirname}/cache/version`;
const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile).toString('utf-8') : undefined;
const publicKeyFile = `${__dirname}/keys/public.pem`;
const publicKey = fs.existsSync(publicKeyFile) ? fs.readFileSync(publicKeyFile).toString('utf-8') : undefined;
const allLocalesUrls = buildList(function*() {
if (process.env.NODE_ENV === 'development') {
if (process.env.BASE_URL) {
yield process.env.BASE_URL;
}
yield 'http://pronouns.test:3000';
yield 'http://localhost:3000';
} else if (process.env.ENV === 'test') {
if (process.env.BASE_URL) {
yield process.env.BASE_URL;
}
} else {
yield 'https://pronouns.page';
for (const localeDescription of Object.values(locales)) {
yield localeDescription.url;
}
}
});
process.env.ALL_LOCALES_URLS = allLocalesUrls.join(',');
const esBuildOptions = {
supported: {
'top-level-await': true,
},
};
const postCssPlugins: Record<string, object> = {
autoprefixer: {},
cssnano: {},
};
if (config.dir === 'rtl') {
postCssPlugins.rtlcss = {};
}
const hostname = process.env.HOST ?? '0.0.0.0';
const port = parseInt(process.env.PORT ?? '3000');
export default defineNuxtConfig({
compatibilityDate: '2024-07-06',
devServer: {
host: hostname, // listen on any host name
port,
},
devtools: {
enabled: true,
},
css: [
'~/assets/style.scss',
],
typescript: {
typeCheck: process.env.NODE_ENV !== 'production',
tsConfig: {
compilerOptions: {
baseUrl: './',
allowImportingTsExtensions: true,
},
},
},
modules: [
'@pinia/nuxt',
'nuxt-csurf',
'@nuxtjs/plausible',
'@vite-pwa/nuxt',
'@nuxt/test-utils/module',
],
pwa: {
registerType: 'autoUpdate',
manifest: {
name: title,
short_name: title,
description,
lang: locale,
background_color: '#ffffff',
theme_color: colour,
// icons generated via `pwa-assets-generator --preset minimal-2023 public/logo/logo-primary.svg`
// see https://vite-pwa-org.netlify.app/assets-generator/cli
icons: [
{
src: 'pwa-64x64.png',
sizes: '64x64',
type: 'image/png',
},
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'maskable-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
workbox: {
runtimeCaching: [
{
urlPattern: /^\/@/,
handler: 'NetworkFirst',
},
],
},
devOptions: {
enabled: true,
},
},
runtimeConfig: {
public: {
env: process.env.ENV,
baseUrl: process.env.BASE_URL,
homeUrl: process.env.HOME_URL || 'https://pronouns.page',
allLocalesUrls,
version,
publicKey,
turnstileSiteKey: process.env.TURNSTILE_SITEKEY,
cloudfront: process.env.CLOUDFRONT,
heartbeatLink: process.env.HEARTBEAT_LINK!,
shopifyStorefrontToken: process.env.SHOPIFY_STOREFRONT_TOKEN ?? '',
plausible: {
enabled: !!process.env.PLAUSIBLE_API_HOST,
domain: process.env.PLAUSIBLE_DOMAIN || translations.domain,
apiHost: process.env.PLAUSIBLE_API_HOST,
// Disables automatic tracking of page views, meaning we have to do it manually
// If it's not done manually, a privacy issue occurs, which we *do not want*
autoPageviews: false,
},
sentry: {
dsn: process.env.SENTRY_DSN,
},
},
},
sourcemap: true,
vue: {
compilerOptions: {
whitespace: 'preserve',
},
transformAssetUrls: {
// disable transforming on <img src> because components/Header.vue has optional
// /img-local/census/census-banner-horizontal.png and /img-local/census/census-banner-horizontal-mobile.png
img: [],
},
},
vite: {
plugins: [
mdPlugin(),
sumlPlugin(),
tsvPlugin(),
yamlPlugin(),
nodePolyfills({ include: ['crypto', 'stream', 'util'] }),
replacePlugin({
preventAssignment: false,
sourceMap: true,
delimiters: ['', ''],
values: Object.fromEntries([
// workaround for crypto polyfill not working in production mode
// https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/92#issuecomment-2228168969
[`if ((crypto && crypto.getRandomValues) || !process.browser) {
exports.randomFill = randomFill
exports.randomFillSync = randomFillSync
} else {
exports.randomFill = oldBrowser
exports.randomFillSync = oldBrowser
}`,
`exports.randomFill = randomFill
exports.randomFillSync = randomFillSync`],
]),
}),
sentryVitePlugin({
disable: !process.env.SENTRY_AUTH_TOKEN,
telemetry: false,
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
resolve: {
alias: {
'~@fortawesome': path.resolve(__dirname, 'node_modules/@fortawesome'),
'~@vuepic': path.resolve(__dirname, 'node_modules/@vuepic'),
'~avris-sorter': path.resolve(__dirname, 'node_modules/avris-sorter'),
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
},
},
esbuild: esBuildOptions,
},
nitro: {
rollupConfig: {
external: [
'canvas',
'sharp',
],
},
esbuild: {
options: esBuildOptions,
},
},
postcss: {
plugins: postCssPlugins,
},
serverHandlers: [
{
route: '/api/**',
handler: '~/server/index.ts',
},
],
routeRules: {
'/api/connect/apple/callback': {
csurf: false,
},
'/api/sentry/tunnel': {
csurf: false,
},
},
hooks: {
'pages:extend'(routes) {
if (config.pronouns.enabled) {
routes.push({
path: `/${encodeURIComponent(config.pronouns.route)}`,
file: '~/routes/pronouns.vue',
});
}
if (config.sources.enabled) {
routes.push({
path: `/${encodeURIComponent(config.sources.route)}`,
file: '~/routes/sources.vue',
});
}
if (config.nouns.enabled) {
routes.push({
path: `/${encodeURIComponent(config.nouns.route)}`,
file: '~/routes/nouns.vue',
});
for (const subroute of config.nouns.subroutes || []) {
routes.push({
path: `/${encodeURIComponent(subroute)}`,
file: `~/data/nouns/${subroute}.vue`,
});
}
if (config.nouns.templates.enabled && config.nouns.templates.route) {
routes.push({
path: `/${encodeURIComponent(config.nouns.templates.route)}`,
file: '~/routes/nounTemplates.vue',
});
}
}
if (config.inclusive.enabled) {
routes.push({
path: `/${encodeURIComponent(config.inclusive.route)}`,
file: '~/routes/inclusive.vue',
});
}
if (config.terminology.enabled) {
routes.push({
path: `/${encodeURIComponent(config.terminology.route)}`,
file: '~/routes/terminology.vue',
});
}
if (config.names.enabled) {
routes.push({
path: `/${encodeURIComponent(config.names.route)}`,
file: '~/routes/names.vue',
});
}
if (config.faq.enabled) {
routes.push({
path: `/${encodeURIComponent(config.faq.route)}`,
file: '~/routes/faq.vue',
});
}
if (config.links.enabled) {
routes.push({
path: `/${encodeURIComponent(config.links.route)}`,
file: '~/routes/links.vue',
});
if (config.links.academicRoute) {
routes.push({
path: `/${encodeURIComponent(config.links.academicRoute)}`,
file: '~/routes/academic.vue',
});
}
if (config.links.mediaRoute) {
routes.push({
path: `/${encodeURIComponent(config.links.mediaRoute)}`,
file: '~/routes/media.vue',
});
}
if (config.links.translinguisticsRoute) {
routes.push({
path: `/${encodeURIComponent(config.links.translinguisticsRoute)}`,
file: '~/routes/translinguistics.vue',
});
}
}
if (config.links.enabled && config.links.blog) {
routes.push({
path: `/${encodeURIComponent(config.links.blogRoute)}`,
file: '~/routes/blog.vue',
name: 'blog',
});
routes.push({
path: `/${encodeURIComponent(config.links.blogRoute)}/:slug`,
file: '~/routes/blogEntry.vue',
name: 'blogEntry',
});
if (config.blog && config.blog.shortcuts) {
for (const shortcut in config.blog.shortcuts) {
if (!config.blog.shortcuts.hasOwnProperty(shortcut)) {
continue;
}
const slug = config.blog.shortcuts[shortcut];
if ((config.blog.keepFullPath || []).includes(slug)) {
continue;
}
routes.push({
path: `/${encodeURIComponent(shortcut)}`,
file: '~/routes/blogEntry.vue',
meta: { slug },
name: `blogEntryShortcut:${shortcut}`,
});
}
}
}
if (config.links.zine && config.links.zine.enabled) {
routes.push({
path: `/${encodeURIComponent(config.links.zine.route)}`,
file: '~/routes/zine.vue',
});
}
if (config.people.enabled) {
routes.push({
path: `/${encodeURIComponent(config.people.route)}`,
file: '~/routes/people.vue',
});
}
if (config.english.enabled) {
routes.push({
path: `/${encodeURIComponent(config.english.route)}`,
file: '~/routes/english.vue',
});
}
if (config.contact.enabled) {
routes.push({
path: `/${encodeURIComponent(config.contact.route)}`,
file: '~/routes/contact.vue',
});
if (config.contact.team.enabled) {
routes.push({
path: `/${encodeURIComponent(config.contact.team.route)}`,
file: '~/routes/team.vue',
});
}
}
if (config.census.enabled) {
routes.push({
path: `/${encodeURIComponent(config.census.route)}`,
file: '~/routes/census.vue',
});
routes.push({
path: `/${encodeURIComponent(config.census.route)}/admin`,
file: '~/routes/censusModeration.vue',
});
}
if (config.user.enabled) {
routes.push({
path: `/${encodeURIComponent(config.user.route)}`,
file: '~/routes/user.vue',
});
routes.push({
path: `/${encodeURIComponent(config.user.termsRoute)}`,
file: '~/routes/terms.vue',
});
routes.push({
path: `/${encodeURIComponent(config.user.privacyRoute)}`,
file: '~/routes/privacy.vue',
});
}
routes.push({ path: '/license', file: '~/routes/license.vue' });
routes.push({ path: '/design', file: '~/routes/design.vue' });
routes.push({ path: '/admin', file: '~/routes/admin.vue' });
routes.push({ path: '/admin/users', file: '~/routes/adminUsers.vue' });
routes.push({ path: '/admin/profiles', file: '~/routes/adminProfiles.vue' });
routes.push({
path: '/admin/audit-log/:username/:id',
file: '~/routes/adminAuditLog.vue',
});
routes.push({
path: '/admin/timesheets',
file: '~/routes/adminTimesheets.vue',
});
routes.push({
path: '/admin/timesheets/overview',
file: '~/routes/adminTimesheetsOverview.vue',
});
routes.push({
path: '/admin/moderation',
file: '~/routes/adminModeration.vue',
});
routes.push({
path: '/admin/abuse-reports',
file: '~/routes/adminAbuseReports.vue',
});
routes.push({
path: '/admin/pending-bans',
file: '~/routes/adminPendingBans.vue',
});
routes.push({
path: '/admin/translations/missing',
file: '~/routes/adminTranslationsMissing.vue',
});
routes.push({
path: '/admin/translations/awaiting',
file: '~/routes/adminTranslationsAwaiting.vue',
});
if (config.profile.enabled) {
routes.push({ path: '/u/:username', file: '~/routes/profile.vue' });
routes.push({ path: '/@:username', file: '~/routes/profile.vue' });
routes.push({ path: '/card/@:username', file: '~/routes/profileCard.vue' });
if (config.profile.editorEnabled) {
routes.push({ path: '/editor', file: '~/routes/profileEditor.vue' });
}
}
if (config.pronouns.enabled) {
for (const prefix of [...config.pronouns?.sentence?.prefixes || [], config.pronouns?.prefix || '']) {
routes.push({
path: `${prefix}/${encodeURIComponent(config.pronouns.any)}`,
file: '~/routes/any.vue',
});
routes.push({
path: `${prefix}/${encodeURIComponent(config.pronouns.any)}::group`,
file: '~/routes/any.vue',
});
if (config.pronouns.null && config.pronouns.null.routes) {
for (const route of config.pronouns.null.routes) {
routes.push({
path: `${prefix}/${encodeURIComponent(route)}`,
file: '~/routes/avoiding.vue',
});
}
}
if (config.pronouns.mirror) {
routes.push({
path: `${prefix}/${encodeURIComponent(config.pronouns.mirror.route)}`,
file: '~/routes/mirror.vue',
});
}
}
}
if (config.calendar && config.calendar.enabled) {
routes.push({
path: `/${encodeURIComponent(config.calendar.route)}`,
file: '~/routes/calendar.vue',
});
routes.push({
path: `/${encodeURIComponent(config.calendar.route)}/:year(\\d\\d\\d\\d)`,
file: '~/routes/calendar.vue',
});
routes.push({
path: `/${encodeURIComponent(config.calendar.route)}/:year(\\d\\d\\d\\d)-:month(\\d\\d)-:day(\\d\\d)`,
file: '~/routes/calendarDay.vue',
});
}
if (config.workshops && config.workshops.enabled) {
routes.push({
path: `/${encodeURIComponent(config.workshops.route)}`,
file: '~/routes/workshops.vue',
});
}
if (config.api !== null) {
routes.push({ path: '/api', file: '~/routes/api.vue' });
}
routes.push({ name: 'all', path: '/:path(.*)', file: '~/routes/pronoun.vue' });
},
render: {
errorMiddleware(app) {
app.use((err: Error, req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => {
if (err) {
console.error(formatError(err, req));
}
next(err);
});
},
},
listen(_server, { port }) {
if (version) {
process.stderr.write(`[${new Date().toISOString()}] ` +
`Listening on port ${port} with version ${version}\n`);
}
},
},
loadingIndicator: {
name: 'views/loading.html',
},
});