diff --git a/.prettierignore b/.prettierignore index 004c815..c6145fd 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/.woodpecker.yml b/.woodpecker.yml index 656903a..a55d39a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -32,3 +32,14 @@ pipeline: auto_tag: true when: event: tag + + nightly_build: + image: woodpeckerci/plugin-docker-buildx + secrets: [docker_username, docker_password] + settings: + repo: dessalines/lemmy-ui + dockerfile: Dockerfile + platforms: linux/amd64 + tag: dev + when: + event: cron diff --git a/Dockerfile b/Dockerfile index 2b36581..92b3f7e 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 3bfc10d..881d9bc 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 7eab4cb..9acaba4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.18.1-rc.1", + "version": "0.18.1-rc.5", "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}\"", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 63c1b47..e5c163b 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -198,9 +198,9 @@ blockquote { .thumbnail { object-fit: cover; - aspect-ratio: 4/3; - width: 100%; - max-height: 6rem; + aspect-ratio: 1/1; + width: 5rem; + height: 5rem; } .thumbnail svg { diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx index f22b8a1..d485429 100644 --- a/src/server/handlers/catch-all-handler.tsx +++ b/src/server/handlers/catch-all-handler.tsx @@ -75,7 +75,12 @@ export default async (req: Request, res: Response) => { routeData = await activeRoute.fetchInitialData(initialFetchReq); } + + if (!activeRoute) { + res.status(404); + } } else if (try_site.state === "failed") { + res.status(500); errorPageData = getErrorPageData(new Error(try_site.msg), site); } @@ -89,6 +94,7 @@ export default async (req: Request, res: Response) => { if (error.msg === "instance_is_private") { return res.redirect(`/signup`); } else { + res.status(500); errorPageData = getErrorPageData(new Error(error.msg), site); } } diff --git a/src/server/handlers/security-handler.ts b/src/server/handlers/security-handler.ts new file mode 100644 index 0000000..0aed0cd --- /dev/null +++ b/src/server/handlers/security-handler.ts @@ -0,0 +1,17 @@ +import type { Response } from "express"; + +export default async ({ res }: { res: Response }) => { + res.setHeader("content-type", "text/plain; charset=utf-8"); + + res.send( + `Contact: mailto:security@lemmy.ml + Contact: mailto:admin@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Contact: mailto:security@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Expires: 2024-01-01T04:59:00.000Z + ` + ); +}; diff --git a/src/server/index.tsx b/src/server/index.tsx index aed8bca..458d7f0 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -1,10 +1,12 @@ import { setupDateFns } from "@utils/app"; +import { getStaticDir } from "@utils/env"; import express from "express"; import path from "path"; import process from "process"; import CatchAllHandler from "./handlers/catch-all-handler"; import ManifestHandler from "./handlers/manifest-handler"; import RobotsHandler from "./handlers/robots-handler"; +import SecurityHandler from "./handlers/security-handler"; import ServiceWorkerHandler from "./handlers/service-worker-handler"; import ThemeHandler from "./handlers/theme-handler"; import ThemesListHandler from "./handlers/themes-list-handler"; @@ -18,13 +20,20 @@ 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"]) { server.use(setDefaultCsp); } +server.get("/.well-known/security.txt", SecurityHandler); server.get("/robots.txt", RobotsHandler); server.get("/service-worker.js", ServiceWorkerHandler); server.get("/manifest.webmanifest", ManifestHandler); diff --git a/src/server/middleware.ts b/src/server/middleware.ts index a7cc6f2..235f072 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; import { UserService } from "../shared/services"; export function setDefaultCsp({ @@ -10,7 +10,7 @@ export function setDefaultCsp({ }) { res.setHeader( "Content-Security-Policy", - `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *` + `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:` ); next(); @@ -18,24 +18,33 @@ 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; -}) { +export function setCacheControl( + req: Request, + res: Response, + next: NextFunction +) { const user = UserService.Instance; - let caching; - if (user.auth()) { - caching = "private"; + let caching: string; + + if ( + process.env.NODE_ENV === "production" && + (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 { - caching = "public, max-age=60"; + if (user.auth()) { + 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 1377598..ba85228 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/shared/components/common/icon.tsx b/src/shared/components/common/icon.tsx index 5b6ddf8..92a41a3 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/image-upload-form.tsx b/src/shared/components/common/image-upload-form.tsx index e8005cc..854e710 100644 --- a/src/shared/components/common/image-upload-form.tsx +++ b/src/shared/components/common/image-upload-form.tsx @@ -84,6 +84,8 @@ export class ImageUploadForm extends Component< if (res.state === "success") { if (res.data.msg === "ok") { i.props.onUpload(res.data.url as string); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx index f7c4760..5623ace 100644 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@ -443,6 +443,10 @@ export class MarkdownTextArea extends Component< const textarea: any = document.getElementById(i.id); autosize.update(textarea); pictrsDeleteToast(image.name, res.data.delete_url as string); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); + i.setState({ imageUploadStatus: undefined }); + throw JSON.stringify(res.data); } else { throw JSON.stringify(res.data); } diff --git a/src/shared/components/common/moment-time.tsx b/src/shared/components/common/moment-time.tsx index ec97eb4..e658695 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 format from "date-fns/format"; +import { format } from "date-fns"; import parseISO from "date-fns/parseISO"; import { Component } from "inferno"; import { I18NextService } from "../../services"; @@ -13,7 +13,8 @@ interface MomentTimeProps { } function formatDate(input: string) { - return format(parseISO(input), "PPPPpppp"); + const parsed = parseISO(input + "Z"); + return format(parsed, "PPPPpppp"); } export class MomentTime extends Component { diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx index a84ec05..8d86d9d 100644 --- a/src/shared/components/community/communities.tsx +++ b/src/shared/components/community/communities.tsx @@ -284,7 +284,9 @@ export class Communities extends Component { handleSearchSubmit(i: Communities, event: any) { event.preventDefault(); const searchParamEncoded = encodeURIComponent(i.state.searchText); - i.context.router.history.push(`/search?q=${searchParamEncoded}`); + i.context.router.history.push( + `/search?q=${searchParamEncoded}&type=Communities` + ); } static async fetchInitialData({ diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx index 149ff03..4108e7a 100644 --- a/src/shared/components/home/emojis-form.tsx +++ b/src/shared/components/home/emojis-form.tsx @@ -508,6 +508,8 @@ export class EmojiForm extends Component { { form: form, index: index, overrideValue: res.data.url as string }, event ); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 5ef1a87..5e73367 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/setup.tsx b/src/shared/components/home/setup.tsx index bed1262..966e6d4 100644 --- a/src/shared/components/home/setup.tsx +++ b/src/shared/components/home/setup.tsx @@ -205,9 +205,7 @@ export class Setup extends Component { const data = i.state.registerRes.data; UserService.Instance.login(data); - if (UserService.Instance.jwtInfo) { - i.setState({ doneRegisteringUser: true }); - } + i.setState({ doneRegisteringUser: true }); } } } diff --git a/src/shared/components/home/tagline-form.tsx b/src/shared/components/home/tagline-form.tsx index bdbe1e6..f7cf99a 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 6631a8e..dfc5d66 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-form.tsx b/src/shared/components/post/post-form.tsx index 25a0fcc..c29d3b1 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -187,6 +187,8 @@ function handleImageUpload(i: PostForm, event: any) { imageLoading: false, imageDeleteUrl: res.data.delete_url as string, }); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index eb3d0f6..5c562a4 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 ( @@ -981,9 +1025,8 @@ export class PostListing extends Component { get modUnbanFromCommunityButton() { return ( @@ -993,20 +1036,15 @@ export class PostListing extends Component { get addModToCommunityButton() { return ( ); @@ -1015,11 +1053,10 @@ export class PostListing extends Component { get modBanButton() { return ( ); } @@ -1027,14 +1064,13 @@ export class PostListing extends Component { get modUnbanButton() { return ( ); @@ -1043,11 +1079,10 @@ export class PostListing extends Component { get purgePersonButton() { return ( ); } @@ -1055,11 +1090,10 @@ export class PostListing extends Component { get purgePostButton() { return ( ); } @@ -1067,20 +1101,31 @@ export class PostListing extends Component { get toggleAdminButton() { return ( ); } + get transferCommunityButton() { + return ( + + ); + } + get modRemoveButton() { const removed = this.postView.post.removed; return ( @@ -1095,102 +1140,17 @@ export class PostListing extends Component { {this.state.removeLoading ? ( ) : !removed ? ( - I18NextService.i18n.t("remove") + capitalizeFirstLetter(I18NextService.i18n.t("remove_post")) ) : ( - I18NextService.i18n.t("restore") + <> + {capitalizeFirstLetter(I18NextService.i18n.t("restore"))}{" "} + {I18NextService.i18n.t("post")} + )} ); } - /** - * Mod/Admin actions to be taken against the author. - */ - userActionsLine() { - // TODO: make nicer - const post_view = this.postView; - return ( - this.state.showAdvanced && ( -
- {this.canMod_ && ( - <> - {!this.creatorIsMod_ && - (!post_view.creator_banned_from_community - ? this.modBanFromCommunityButton - : this.modUnbanFromCommunityButton)} - {!post_view.creator_banned_from_community && - this.addModToCommunityButton} - - )} - - {/* Community creators and admins can transfer community to another mod */} - {(amCommunityCreator(post_view.creator.id, this.props.moderators) || - this.canAdmin_) && - this.creatorIsMod_ && - (!this.state.showConfirmTransferCommunity ? ( - - ) : ( - <> - - - - - ))} - {/* Admins can ban from all, and appoint other admins */} - {this.canAdmin_ && ( - <> - {!this.creatorIsAdmin_ && ( - <> - {!isBanned(post_view.creator) - ? this.modBanButton - : this.modUnbanButton} - {this.purgePersonButton} - {this.purgePostButton} - - )} - {!isBanned(post_view.creator) && - post_view.creator.local && - this.toggleAdminButton} - - )} -
- ) - ); - } - removeAndBanDialogs() { const post = this.postView; const purgeTypeText = @@ -1218,11 +1178,7 @@ export class PostListing extends Component { value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> - )} + {this.state.showConfirmTransferCommunity && ( + <> + + + + + )} {this.state.showBanDialog && (
@@ -1284,11 +1267,7 @@ export class PostListing extends Component { {/* */} {/*
*/}
- )} @@ -1409,7 +1380,6 @@ export class PostListing extends Component { {this.mobileThumbnail()} {this.commentsLine(true)} - {this.userActionsLine()} {this.duplicatesLine()} {this.removeAndBanDialogs()}
@@ -1433,15 +1403,14 @@ export class PostListing extends Component { )}
-
+
{this.thumbnail()}
-
+
{this.postTitleLine()} {this.createdLine()} {this.commentsLine()} {this.duplicatesLine()} - {this.userActionsLine()} {this.removeAndBanDialogs()}
diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index b58580e..5360066 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -332,9 +332,7 @@ export class Search extends Component { } async componentDidMount() { - if ( - !(this.state.isIsomorphic || this.props.history.location.state?.searched) - ) { + if (!this.state.isIsomorphic) { const promises = [this.fetchCommunities()]; if (this.state.searchText) { promises.push(this.search()); @@ -432,7 +430,15 @@ export class Search extends Component { q: query, auth, }; - resolveObjectResponse = await client.resolveObject(resolveObjectForm); + resolveObjectResponse = await HttpService.silent_client.resolveObject( + resolveObjectForm + ); + + // If we return this object with a state of failed, the catch-all-handler will redirect + // to an error page, so we ignore it by covering up the error with the empty state. + if (resolveObjectResponse.state === "failed") { + resolveObjectResponse = { state: "empty" }; + } } } } @@ -950,7 +956,7 @@ export class Search extends Component { if (auth) { this.setState({ resolveObjectRes: { state: "loading" } }); this.setState({ - resolveObjectRes: await HttpService.client.resolveObject({ + resolveObjectRes: await HttpService.silent_client.resolveObject({ q, auth, }), @@ -1097,10 +1103,6 @@ export class Search extends Component { sort: sort ?? urlSort, }; - this.props.history.push(`/search${getQueryString(queryParams)}`, { - searched: true, - }); - - await this.search(); + this.props.history.push(`/search${getQueryString(queryParams)}`); } } diff --git a/src/shared/config.ts b/src/shared/config.ts index 97b28d2..58ecc08 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,5 +1,7 @@ -export const favIconUrl = "/static/assets/icons/favicon.svg"; -export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png"; +import { getStaticDir } from "@utils/env"; + +export const favIconUrl = `${getStaticDir()}/assets/icons/favicon.svg`; +export const favIconPngUrl = `${getStaticDir()}/assets/icons/apple-touch-icon.png`; export const repoUrl = "https://github.com/LemmyNet"; export const joinLemmyUrl = "https://join-lemmy.org"; diff --git a/src/shared/services/HttpService.ts b/src/shared/services/HttpService.ts index 361ffbd..11ec292 100644 --- a/src/shared/services/HttpService.ts +++ b/src/shared/services/HttpService.ts @@ -1,9 +1,9 @@ import { getHttpBase } from "@utils/env"; import { LemmyHttp } from "lemmy-js-client"; -import { toast } from "../../shared/toast"; +import { toast } from "../toast"; import { I18NextService } from "./I18NextService"; -type EmptyRequestState = { +export type EmptyRequestState = { state: "empty"; }; @@ -45,7 +45,7 @@ export type WrappedLemmyHttp = { class WrappedLemmyHttpClient { #client: LemmyHttp; - constructor(client: LemmyHttp) { + constructor(client: LemmyHttp, silent = false) { this.#client = client; for (const key of Object.getOwnPropertyNames( @@ -61,8 +61,10 @@ class WrappedLemmyHttpClient { state: !(res === undefined || res === null) ? "success" : "empty", }; } catch (error) { - console.error(`API error: ${error}`); - toast(I18NextService.i18n.t(error), "danger"); + if (!silent) { + console.error(`API error: ${error}`); + toast(I18NextService.i18n.t(error), "danger"); + } return { state: "failed", msg: error, @@ -74,16 +76,23 @@ class WrappedLemmyHttpClient { } } -export function wrapClient(client: LemmyHttp) { - return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary +export function wrapClient(client: LemmyHttp, silent = false) { + // unfortunately, this verbose cast is necessary + return new WrappedLemmyHttpClient( + client, + silent + ) as unknown as WrappedLemmyHttp; } export class HttpService { static #_instance: HttpService; + #silent_client: WrappedLemmyHttp; #client: WrappedLemmyHttp; private constructor() { - this.#client = wrapClient(new LemmyHttp(getHttpBase())); + const lemmyHttp = new LemmyHttp(getHttpBase()); + this.#client = wrapClient(lemmyHttp); + this.#silent_client = wrapClient(lemmyHttp, true); } static get #Instance() { @@ -93,4 +102,8 @@ export class HttpService { public static get client() { return this.#Instance.#client; } + + public static get silent_client() { + return this.#Instance.#silent_client; + } } diff --git a/src/shared/utils/app/is-auth-path.ts b/src/shared/utils/app/is-auth-path.ts index 0ec963a..5a201ac 100644 --- a/src/shared/utils/app/is-auth-path.ts +++ b/src/shared/utils/app/is-auth-path.ts @@ -1,5 +1,5 @@ export default function isAuthPath(pathname: string) { - return /create_.*|inbox|settings|admin|reports|registration_applications/g.test( + return /^\/create_.*|inbox|settings|admin|reports|registration_applications/g.test( pathname ); } diff --git a/src/shared/utils/env/get-static-dir.ts b/src/shared/utils/env/get-static-dir.ts new file mode 100644 index 0000000..1d19596 --- /dev/null +++ b/src/shared/utils/env/get-static-dir.ts @@ -0,0 +1,5 @@ +// Returns path to static directory, intended +// for cache-busting based on latest commit hash. +export default function getStaticDir() { + return `/static/${process.env.COMMIT_HASH}`; +} diff --git a/src/shared/utils/env/index.ts b/src/shared/utils/env/index.ts index e14c673..3a9a3fe 100644 --- a/src/shared/utils/env/index.ts +++ b/src/shared/utils/env/index.ts @@ -6,6 +6,7 @@ import getHttpBaseExternal from "./get-http-base-external"; import getHttpBaseInternal from "./get-http-base-internal"; import getInternalHost from "./get-internal-host"; import getSecure from "./get-secure"; +import getStaticDir from "./get-static-dir"; import httpExternalPath from "./http-external-path"; import isHttps from "./is-https"; @@ -18,6 +19,7 @@ export { getHttpBaseInternal, getInternalHost, getSecure, + getStaticDir, httpExternalPath, isHttps, }; diff --git a/src/shared/utils/helpers/format-past-date.ts b/src/shared/utils/helpers/format-past-date.ts index 78bc2a2..5bef4e8 100644 --- a/src/shared/utils/helpers/format-past-date.ts +++ b/src/shared/utils/helpers/format-past-date.ts @@ -2,11 +2,8 @@ import formatDistanceStrict from "date-fns/formatDistanceStrict"; import parseISO from "date-fns/parseISO"; export default function (dateString?: string) { - return formatDistanceStrict( - parseISO(dateString ?? Date.now().toString()), - new Date(), - { - addSuffix: true, - } - ); + const parsed = parseISO((dateString ?? Date.now().toString()) + "Z"); + return formatDistanceStrict(parsed, new Date(), { + addSuffix: true, + }); } diff --git a/webpack.config.js b/webpack.config.js index 4d95a80..0c9806d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,56 +14,63 @@ const banner = ` @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0 `; -const base = { - output: { - filename: "js/server.js", - publicPath: "/", - hashFunction: "xxhash64", - }, - resolve: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - alias: { - "@": path.resolve(__dirname, "src/"), - "@utils": path.resolve(__dirname, "src/shared/utils/"), +function getBase(env, mode) { + return { + output: { + filename: "js/server.js", + publicPath: "/", + hashFunction: "xxhash64", }, - }, - performance: { - hints: false, - }, - module: { - rules: [ - { - test: /\.(scss|css)$/i, - use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], + resolve: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + alias: { + "@": path.resolve(__dirname, "src/"), + "@utils": path.resolve(__dirname, "src/shared/utils/"), }, - { - test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by - exclude: /node_modules/, // ignore node_modules - loader: "babel-loader", - }, - // Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467 - { - test: /\.m?js/, - resolve: { - fullySpecified: false, + }, + performance: { + hints: false, + }, + module: { + rules: [ + { + test: /\.(scss|css)$/i, + use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], }, - }, + { + test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by + exclude: /node_modules/, // ignore node_modules + loader: "babel-loader", + }, + // Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467 + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.COMMIT_HASH": `"${env.COMMIT_HASH}"`, + "process.env.NODE_ENV": `"${mode}"`, + }), + new MiniCssExtractPlugin({ + filename: "styles/styles.css", + }), + new CopyPlugin({ + patterns: [{ from: "./src/assets", to: "./assets" }], + }), + new webpack.BannerPlugin({ + banner, + }), ], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: "styles/styles.css", - }), - new CopyPlugin({ - patterns: [{ from: "./src/assets", to: "./assets" }], - }), - new webpack.BannerPlugin({ - banner, - }), - ], -}; + }; +} -const createServerConfig = (_env, mode) => { +const createServerConfig = (env, mode) => { + const base = getBase(env, mode); const config = merge({}, base, { mode, entry: "./src/server/index.tsx", @@ -90,12 +97,14 @@ const createServerConfig = (_env, mode) => { return config; }; -const createClientConfig = (_env, mode) => { +const createClientConfig = (env, mode) => { + const base = getBase(env, mode); const config = merge({}, base, { mode, entry: "./src/client/index.tsx", output: { filename: "js/client.js", + publicPath: `/static/${env.COMMIT_HASH}/`, }, plugins: [ ...base.plugins, @@ -103,10 +112,10 @@ const createClientConfig = (_env, mode) => { enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct workbox: { modifyURLPrefix: { - "/": "/static/", + "/": `/static/${env.COMMIT_HASH}/`, }, cacheId: "lemmy", - include: [/(assets|styles)\/.+\..+|client\.js$/g], + include: [/(assets|styles|js)\/.+\..+$/g], inlineWorkboxRuntime: true, runtimeCaching: [ {