mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 11:07:00 -04:00
567 lines
20 KiB
TypeScript
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',
|
|
},
|
|
});
|