import { InMemoryCacheAdapter } from "@ethicdevs/fastify-stream-react-views";
import {
AppServer,
makeAppServer,
startAppServer,
stopAppServerAndExit,
} from "@ethicdevs/react-monolith";
import cuid from "cuid";
import fastifyCookie, { CookieSerializeOptions } from "@fastify/cookie";
import fastifyCustomSession, {
PrismaSessionAdapter,
} from "@ethicdevs/fastify-custom-session";
import fastifyFormBody from "@fastify/formbody";
import fastifyGitServer from "@ethicdevs/fastify-git-server";
import fastifyServeStatic from "fastify-static";
import loadPrismJsLanguages from "prismjs/components/";
import { GlobalRole, PrismaClient } from "@prisma/client";
import * as Paths from "../paths";
import { version as appVersion } from "../package.json";
import InternalErrorView, {
InternalErrorViewProps,
} from "./views/InternalErrorView";
import { Const } from "./const";
import { Env } from "./env";
import { codeAnalysisPlugin, cryptoPlugin, prismaPlugin } from "./plugins";
import { makeGitServerService } from "./services/gitServer";
import {
getEnv,
localAppDomainPreHandler,
makeRequestHandler,
} from "./utils/server";
import { FastifyError } from "fastify";
let server: null | AppServer = null;
async function main(): Promise<AppServer> {
const env = getEnv();
const prisma = new PrismaClient();
const depsBaseUrl = `/${Paths.PUBLIC_FOLDER_NAME}/${Paths.ASSET_DEPS_FOLDER_NAME}`;
const publicBaseUrl = `/${Paths.PUBLIC_FOLDER_NAME}`;
const cookiesOpts: CookieSerializeOptions = {
domain: `.${Env.DEPLOYMENT_DOMAIN}`,
httpOnly: true,
path: "/",
secure: false,
sameSite: "lax",
signed: true,
};
server = await makeAppServer(Env.HOST, Env.PORT, {
appName: Const.APP_NAME,
appVersion,
env,
a11y: {
localeDirection: "ltr",
},
assets: {
depsFolder: Paths.ASSET_DEPS_FOLDER_NAME,
importPrefix: publicBaseUrl,
},
cacheAdapter: new InMemoryCacheAdapter({
maxSize: Const.SSR_CACHE_MAX_SIZE_BYTES,
}),
featureFlags: {
withIncrementalBuild: true,
withImportsMap: true,
withInstantRouter: true,
withStyledSSR: true,
withTreeShaking: true,
withDefaultErrorHandlers: false,
},
paths: {
assetsOutFolder: Paths.PUBLIC_FOLDER,
distFolder: Paths.DIST_FOLDER,
islandsFolder: Paths.ISLANDS_FOLDER,
rootFolder: Paths.ROOT_FOLDER,
routesFile: Paths.ROUTES_FILE,
viewsFolder: Paths.VIEWS_FOLDER,
},
externalDependencies: {
"cross-fetch": "CrossFetch",
"markdown-to-jsx": "MarkdownToJSX",
prismjs: "Prism",
},
baseHeadTags: [
{
kind: "link",
rel: "manifest",
href: "/manifest.json",
},
{
kind: "meta",
name: "shortcut icon",
content: "/public/favicon.ico",
},
{
kind: "meta",
name: "viewport",
content:
"minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover",
},
{
kind: "meta",
name: "mobile-web-app-capable",
content: "yes",
},
{
kind: "meta",
name: "apple-mobile-web-app-capable",
content: "yes",
},
{
kind: "meta",
name: "apple-mobile-web-app-status-bar-style",
content: "default",
},
{
kind: "meta",
name: "format-detection",
content: "telephone=no",
},
{
kind: "meta",
name: "apple-mobile-web-app-title",
content: Const.APP_NAME,
},
{
kind: "meta",
name: "og:type",
content: "website",
},
{
kind: "meta",
name: "og:site_name",
content: Const.APP_NAME,
},
{
kind: "meta",
name: "og:title",
content: Const.APP_NAME,
},
{
kind: "meta",
name: "og:image",
content: `${Env.DEPLOYMENT_SCHEME}://${Env.DEPLOYMENT_DOMAIN}/public/assets/social-icon.png`,
},
{
kind: "meta",
name: "twitter:card",
content: "summary",
},
{
kind: "meta",
name: "twitter:title",
content: Const.APP_NAME,
},
{
kind: "meta",
name: "twitter:image",
content: `${Env.DEPLOYMENT_SCHEME}://${Env.DEPLOYMENT_DOMAIN}/public/assets/social-icon-192.png`,
},
],
baseScriptTags: [
{
async: true,
id: "es-importmap-shim",
src: `${depsBaseUrl}/es-module-shims.production.min.js`,
type: "application/javascript",
},
{
id: "prism.js-config",
type: "application/javascript",
textContent: `if (typeof window !== "undefined") {
window.Prism = window.Prism || { plugins: {} };
window.Prism.manual = true;
window.Prism.plugins.autoloader = {
use_minifed: ${env === "production" ? "true" : "false"},
languages_path: "/public/assets/prism/components/",
};
}`,
},
],
setupServerBeforeRoutes(s) {
s.addHook("preHandler", localAppDomainPreHandler);
const cleanupFastifyError = (error: Error | FastifyError) => {
error.message = error.message.replaceAll(Paths.ROOT_FOLDER, "");
error.stack = error.stack?.replaceAll(Paths.ROOT_FOLDER, "");
};
try {
loadPrismJsLanguages();
} catch (err) {
const error = err as Error;
cleanupFastifyError(error);
console.error(
"Couldn't load Prism.JS languages, syntax highlighting will not work properly.",
`Error: ${error.message}`
);
}
s.decorateReply("makeRequestHandler", makeRequestHandler);
s.setErrorHandler((error, request, reply) => {
cleanupFastifyError(error);
const reqHandler = reply.makeRequestHandler(request, reply);
return reqHandler<InternalErrorViewProps>(InternalErrorView.name, {
error,
});
});
s.setNotFoundHandler((request, reply) => {
const error = new Error("404 - Not Found");
error.name = "NotFoundError";
cleanupFastifyError(error);
const reqHandler = reply.makeRequestHandler(request, reply);
return reqHandler<InternalErrorViewProps>(InternalErrorView.name, {
error,
});
});
s.register(codeAnalysisPlugin);
s.register(cryptoPlugin).after(() => {
const gitService = makeGitServerService({
cryptoService: s.cryptoService,
request: {
prisma,
} as any,
});
s.register(fastifyGitServer, {
withSideBandMessages: true,
authorizationResolver: gitService.authorizationResolver,
repositoryResolver: gitService.repositoryResolver,
onPush: gitService.onPushEvent,
});
});
s.register(prismaPlugin, { prisma });
s.register(fastifyFormBody);
s.register(fastifyServeStatic, {
root: Paths.PUBLIC_FOLDER,
prefix: publicBaseUrl,
});
s.register(fastifyCookie, {
secret: Env.COOKIE_SECRET,
parseOptions: cookiesOpts,
});
s.register(fastifyCustomSession, {
cookieName: Env.COOKIE_NAME,
cookieOptions: cookiesOpts,
getUniqId: cuid as () => string,
password: Env.COOKIE_SECRET,
storeAdapter: new PrismaSessionAdapter(prisma) as any,
ttl: Const.SESSION_TTL_SECONDS,
initialSession: {
sessionId: null,
authenticated: false,
curr_user_avatar_uri: null,
curr_user_uid: null,
curr_user_username: null,
curr_user_role: GlobalRole.GUEST,
flash_message: null,
flash_message_shown_once: false,
two_factor_lock: false,
two_factor_complete: false,
},
});
s.get("/interceptor-imsw.js", {}, async (_, reply) => {
return reply.sendFile("interceptor-imsw.js");
});
s.get("/register-imsw.js", {}, async (_, reply) => {
return reply.sendFile("register-imsw.js");
});
s.get("/manifest.json", {}, async (_, reply) => {
return reply.sendFile("manifest.json");
});
},
});
await startAppServer(server);
return server;
}
["unhandledRejection", "uncaughtException"].forEach((exception) => {
process.on(exception, async (reason: Error) => {
await stopAppServerAndExit(server, reason);
});
});
["SIGQUIT", "SIGTERM", "SIGINT"].forEach((killSignal) => {
process.on(killSignal, async () => {
await stopAppServerAndExit(server);
});
});
main()
.then((server) => {
const { $config, $host, $port } = server.reactMonolith!;
console.log(
`[🚀][${$config.env}] App Server ready at http://${$host}:${$port} !`
);
})
.catch(async (err) => {
const error = err as Error;
console.error(`[❌] Cannot start App Server. Error: ${error.message}`);
await stopAppServerAndExit(null, error);
});