diff --git a/.prettierignore b/.prettierignore index 004c815f..c6145fd8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ src/shared/translations lemmy-translations src/assets/css/themes/*.css stats.json +dist diff --git a/Dockerfile b/Dockerfile index 2b36581d..92b3f7e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ COPY .git .git RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts" RUN yarn --production --prefer-offline -RUN yarn build:prod +RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod # Prune the image RUN node-prune /usr/src/app/node_modules diff --git a/dev.dockerfile b/dev.dockerfile index 3bfc10da..881d9bc3 100644 --- a/dev.dockerfile +++ b/dev.dockerfile @@ -20,6 +20,7 @@ COPY generate_translations.js \ COPY lemmy-translations lemmy-translations COPY src src +COPY .git .git # Set UI version RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts" diff --git a/package.json b/package.json index ef07b29d..e9289746 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.18.1-rc.3", + "version": "0.18.1-rc.7", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", @@ -8,9 +8,9 @@ "scripts": { "analyze": "webpack --mode=none", "prebuild:dev": "yarn clean && node generate_translations.js", - "build:dev": "webpack --mode=development", + "build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development", "prebuild:prod": "yarn clean && node generate_translations.js", - "build:prod": "webpack --mode=production", + "build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production", "clean": "yarn run rimraf dist", "dev": "yarn build:dev --watch", "lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"", @@ -48,11 +48,11 @@ "check-password-strength": "^2.0.7", "classnames": "^2.3.1", "clean-webpack-plugin": "^4.0.0", + "cookie": "^0.5.0", "copy-webpack-plugin": "^11.0.0", "cross-fetch": "^3.1.5", "css-loader": "^6.7.3", "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0", "emoji-mart": "^5.4.0", "emoji-short-name": "^2.0.0", "express": "~4.18.2", @@ -66,7 +66,6 @@ "inferno-i18next-dess": "0.0.2", "inferno-router": "^8.1.1", "inferno-server": "^8.1.1", - "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", "lemmy-js-client": "0.18.0-rc.2", "lodash.isequal": "^4.5.0", @@ -98,6 +97,7 @@ "@babel/core": "^7.21.8", "@types/autosize": "^4.0.0", "@types/bootstrap": "^5.2.6", + "@types/cookie": "^0.5.1", "@types/express": "^4.17.17", "@types/html-to-text": "^9.0.0", "@types/lodash.isequal": "^4.5.6", @@ -126,6 +126,7 @@ "style-loader": "^3.3.2", "terser": "^5.17.3", "typescript": "^5.0.4", + "typescript-language-server": "^3.3.2", "webpack-bundle-analyzer": "^4.9.0", "webpack-dev-server": "4.15.0" }, diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx index d485429c..c599e465 100644 --- a/src/server/handlers/catch-all-handler.tsx +++ b/src/server/handlers/catch-all-handler.tsx @@ -1,11 +1,11 @@ import { initializeSite, isAuthPath } from "@utils/app"; import { getHttpBaseInternal } from "@utils/env"; import { ErrorPageData } from "@utils/types"; +import * as cookie from "cookie"; import fetch from "cross-fetch"; import type { Request, Response } from "express"; import { StaticRouter, matchPath } from "inferno-router"; import { renderToString } from "inferno-server"; -import IsomorphicCookie from "isomorphic-cookie"; import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { App } from "../../shared/components/app/app"; import { @@ -25,11 +25,15 @@ import { setForwardedHeaders } from "../utils/set-forwarded-headers"; export default async (req: Request, res: Response) => { try { const activeRoute = routes.find(route => matchPath(req.path, route)); - let auth: string | undefined = IsomorphicCookie.load("jwt", req); + + let auth = req.headers.cookie + ? cookie.parse(req.headers.cookie).jwt + : undefined; const getSiteForm: GetSite = { auth }; const headers = setForwardedHeaders(req.headers); + const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers }) ); @@ -43,6 +47,7 @@ export default async (req: Request, res: Response) => { let routeData: RouteData = {}; let errorPageData: ErrorPageData | undefined = undefined; let try_site = await client.getSite(getSiteForm); + if (try_site.state === "failed" && try_site.msg == "not_logged_in") { console.error( "Incorrect JWT token, skipping auth so frontend can remove jwt cookie" @@ -91,6 +96,7 @@ export default async (req: Request, res: Response) => { // Redirect to the 404 if there's an API error if (error) { console.error(error.msg); + if (error.msg === "instance_is_private") { return res.redirect(`/signup`); } else { @@ -119,6 +125,7 @@ export default async (req: Request, res: Response) => { // If an error is caught here, the error page couldn't even be rendered console.error(err); res.statusCode = 500; + return res.send( process.env.NODE_ENV === "development" ? err.message : "Server error" ); diff --git a/src/server/index.tsx b/src/server/index.tsx index 270f33c6..458d7f03 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -1,4 +1,5 @@ import { setupDateFns } from "@utils/app"; +import { getStaticDir } from "@utils/env"; import express from "express"; import path from "path"; import process from "process"; @@ -19,7 +20,13 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"] server.use(express.json()); server.use(express.urlencoded({ extended: false })); -server.use("/static", express.static(path.resolve("./dist"))); +server.use( + getStaticDir(), + express.static(path.resolve("./dist"), { + maxAge: 24 * 60 * 60 * 1000, // 1 day + immutable: true, + }) +); server.use(setCacheControl); if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) { diff --git a/src/server/middleware.ts b/src/server/middleware.ts index 9815e71e..0420e47e 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,5 +1,5 @@ -import type { NextFunction, Response } from "express"; -import { UserService } from "../shared/services"; +import type { NextFunction, Request, Response } from "express"; +import { hasJwtCookie } from "./utils/has-jwt-cookie"; export function setDefaultCsp({ res, @@ -18,24 +18,35 @@ export function setDefaultCsp({ // Set cache-control headers. If user is logged in, set `private` to prevent storing data in // shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching -// all responses for 60 seconds to reduce load on backend and database. The specific cache +// all responses for 5 seconds to reduce load on backend and database. The specific cache // interval is rather arbitrary and could be set higher (less server load) or lower (fresher data). // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control -export function setCacheControl({ - res, - next, -}: { - res: Response; - next: NextFunction; -}) { - const user = UserService.Instance; - let caching: string; - if (user.auth()) { - caching = "private"; - } else { - caching = "public, max-age=60"; +export function setCacheControl( + req: Request, + res: Response, + next: NextFunction +) { + if (process.env.NODE_ENV !== "production") { + return next(); } + + let caching: string; + + if ( + req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) || + req.path.includes("/css/themelist") + ) { + // Static content gets cached publicly for a day + caching = "public, max-age=86400"; + } else { + if (hasJwtCookie(req)) { + caching = "private"; + } else { + caching = "public, max-age=5"; + } + } + res.setHeader("Cache-Control", caching); next(); diff --git a/src/server/utils/create-ssr-html.tsx b/src/server/utils/create-ssr-html.tsx index 13775981..ba85228f 100644 --- a/src/server/utils/create-ssr-html.tsx +++ b/src/server/utils/create-ssr-html.tsx @@ -1,3 +1,4 @@ +import { getStaticDir } from "@utils/env"; import { Helmet } from "inferno-helmet"; import { renderToString } from "inferno-server"; import serialize from "serialize-javascript"; @@ -23,7 +24,7 @@ export async function createSsrHtml( if (!appleTouchIcon) { appleTouchIcon = site?.site_view.site.icon - ? `data:image/png;base64,${sharp( + ? `data:image/png;base64,${await sharp( await fetchIconPng(site.site_view.site.icon) ) .resize(180, 180) @@ -87,7 +88,7 @@ export async function createSsrHtml( - + ${helmet.link.toString() || fallbackTheme} @@ -102,7 +103,7 @@ export async function createSsrHtml(
${root}
- + `; diff --git a/src/server/utils/has-jwt-cookie.ts b/src/server/utils/has-jwt-cookie.ts new file mode 100644 index 00000000..ea558ffa --- /dev/null +++ b/src/server/utils/has-jwt-cookie.ts @@ -0,0 +1,6 @@ +import * as cookie from "cookie"; +import type { Request } from "express"; + +export function hasJwtCookie(req: Request): boolean { + return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length); +} diff --git a/src/shared/components/common/icon.tsx b/src/shared/components/common/icon.tsx index 5b6ddf81..92a41a3d 100644 --- a/src/shared/components/common/icon.tsx +++ b/src/shared/components/common/icon.tsx @@ -1,3 +1,4 @@ +import { getStaticDir } from "@utils/env"; import classNames from "classnames"; import { Component } from "inferno"; import { I18NextService } from "../../services"; @@ -23,7 +24,9 @@ export class Icon extends Component { })} >
{this.props.icon} diff --git a/src/shared/components/common/moment-time.tsx b/src/shared/components/common/moment-time.tsx index 24bd3c79..e6586953 100644 --- a/src/shared/components/common/moment-time.tsx +++ b/src/shared/components/common/moment-time.tsx @@ -1,5 +1,5 @@ import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers"; -import { formatInTimeZone } from "date-fns-tz"; +import { format } from "date-fns"; import parseISO from "date-fns/parseISO"; import { Component } from "inferno"; import { I18NextService } from "../../services"; @@ -13,9 +13,8 @@ interface MomentTimeProps { } function formatDate(input: string) { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; const parsed = parseISO(input + "Z"); - return formatInTimeZone(parsed, tz, "PPPPpppp"); + return format(parsed, "PPPPpppp"); } export class MomentTime extends Component { diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 5ef1a87d..5e733674 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -279,13 +279,15 @@ export class Home extends Component { trendingCommunitiesRes, commentsRes, postsRes, - tagline: getRandomFromList(this.state?.siteRes?.taglines ?? []) - ?.content, isIsomorphic: true, }; HomeCacheService.postsRes = postsRes; } + + this.state.tagline = getRandomFromList( + this.state?.siteRes?.taglines ?? [] + )?.content; } componentWillUnmount() { diff --git a/src/shared/components/home/tagline-form.tsx b/src/shared/components/home/tagline-form.tsx index bdbe1e63..f7cf99a6 100644 --- a/src/shared/components/home/tagline-form.tsx +++ b/src/shared/components/home/tagline-form.tsx @@ -141,7 +141,7 @@ export class TaglineForm extends Component { handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) { event.preventDefault(); - if (this.state.editingRow == d.index) { + if (d.i.state.editingRow == d.index) { d.i.setState({ editingRow: undefined }); } else { d.i.setState({ editingRow: d.index }); diff --git a/src/shared/components/person/person-listing.tsx b/src/shared/components/person/person-listing.tsx index 6631a8ea..dfc5d663 100644 --- a/src/shared/components/person/person-listing.tsx +++ b/src/shared/components/person/person-listing.tsx @@ -1,4 +1,5 @@ import { showAvatars } from "@utils/app"; +import { getStaticDir } from "@utils/env"; import { hostname, isCakeDay } from "@utils/helpers"; import classNames from "classnames"; import { Component } from "inferno"; @@ -88,7 +89,7 @@ export class PersonListing extends Component { !this.props.person.banned && showAvatars() && ( )} diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index ae6e2f3b..5c562a4a 100644 --- a/src/shared/components/post/post-listing.tsx +++ b/src/shared/components/post/post-listing.tsx @@ -333,7 +333,7 @@ export class PostListing extends Component { return (