diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx
index cc6eb72ca..a8ee10562 100644
--- a/client/admin/admin.jsx
+++ b/client/admin/admin.jsx
@@ -49,4 +49,4 @@ const Admin = ()=>{
);
};
-module.exports = Admin;
+export default Admin;
diff --git a/client/admin/admin.less b/client/admin/admin.less
index 0fc353194..432f92e8b 100644
--- a/client/admin/admin.less
+++ b/client/admin/admin.less
@@ -1,11 +1,9 @@
-@import 'naturalcrit/styles/reset.less';
-@import 'naturalcrit/styles/elements.less';
-@import 'naturalcrit/styles/animations.less';
-@import 'naturalcrit/styles/colors.less';
-@import 'naturalcrit/styles/tooltip.less';
-@import './themes/fonts/iconFonts/fontAwesome.less';
-
-@import 'font-awesome/css/font-awesome.css';
+@import '@sharedStyles/reset.less';
+@import '@sharedStyles/elements.less';
+@import '@sharedStyles/animations.less';
+@import '@sharedStyles/colors.less';
+@import '@sharedStyles/tooltip.less';
+@import '@themes/fonts/iconFonts/fontAwesome.less';
html,body, #reactContainer, .naturalCrit { min-height : 100%; }
diff --git a/client/admin/brewUtils/brewUtils.less b/client/admin/brewUtils/brewUtils.less
index 5bbbc3f69..cfa7dbbdf 100644
--- a/client/admin/brewUtils/brewUtils.less
+++ b/client/admin/brewUtils/brewUtils.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/colors.less';
+
.brewUtil {
.result {
margin-top : 20px;
diff --git a/client/admin/main.jsx b/client/admin/main.jsx
new file mode 100644
index 000000000..bd380789a
--- /dev/null
+++ b/client/admin/main.jsx
@@ -0,0 +1,6 @@
+import { createRoot } from "react-dom/client";
+import Admin from "./admin.jsx";
+
+const props = window.__INITIAL_PROPS__ || {};
+
+createRoot(document.getElementById("reactRoot")).render();
diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/autocompleteEmoji.js
index d5a3a71aa..fc64e7bbd 100644
--- a/client/components/codeEditor/autocompleteEmoji.js
+++ b/client/components/codeEditor/autocompleteEmoji.js
@@ -1,7 +1,7 @@
-import diceFont from 'themes/fonts/iconFonts/diceFont.js';
-import elderberryInn from 'themes/fonts/iconFonts/elderberryInn.js';
-import fontAwesome from 'themes/fonts/iconFonts/fontAwesome.js';
-import gameIcons from 'themes/fonts/iconFonts/gameIcons.js';
+import diceFont from '@themes/fonts/iconFonts/diceFont.js';
+import elderberryInn from '@themes/fonts/iconFonts/elderberryInn.js';
+import fontAwesome from '@themes/fonts/iconFonts/fontAwesome.js';
+import gameIcons from '@themes/fonts/iconFonts/gameIcons.js';
const emojis = {
...diceFont,
diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less
index c8e60974b..89d0c9497 100644
--- a/client/components/codeEditor/codeEditor.less
+++ b/client/components/codeEditor/codeEditor.less
@@ -5,10 +5,10 @@
@import (less) 'codemirror/addon/hint/show-hint.css';
//Icon fonts included so they can appear in emoji autosuggest dropdown
-@import (less) './themes/fonts/iconFonts/diceFont.less';
-@import (less) './themes/fonts/iconFonts/elderberryInn.less';
-@import (less) './themes/fonts/iconFonts/gameIcons.less';
-@import (less) './themes/fonts/iconFonts/fontAwesome.less';
+@import (less) '@themes/fonts/iconFonts/diceFont.less';
+@import (less) '@themes/fonts/iconFonts/elderberryInn.less';
+@import (less) '@themes/fonts/iconFonts/gameIcons.less';
+@import (less) '@themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation {
50% { color : white;background-color : red;}
diff --git a/client/components/renderWarnings/renderWarnings.less b/client/components/renderWarnings/renderWarnings.less
index 70799092a..a1413709b 100644
--- a/client/components/renderWarnings/renderWarnings.less
+++ b/client/components/renderWarnings/renderWarnings.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/colors.less';
+
.renderWarnings {
position : relative;
float : right;
diff --git a/client/components/splitPane/splitPane.less b/client/components/splitPane/splitPane.less
index 80a8695af..779d0c222 100644
--- a/client/components/splitPane/splitPane.less
+++ b/client/components/splitPane/splitPane.less
@@ -1,3 +1,4 @@
+@import '@sharedStyles/core.less';
.splitPane {
position : relative;
diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx
index 771a6aa31..8e74473b3 100644
--- a/client/homebrew/brewRenderer/brewRenderer.jsx
+++ b/client/homebrew/brewRenderer/brewRenderer.jsx
@@ -1,10 +1,12 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
+import brewRendererStylesUrl from './brewRenderer.less?url';
+import headerNavStylesUrl from './headerNav/headerNav.less?url';
import './brewRenderer.less';
import React, { useState, useRef, useMemo, useEffect } from 'react';
import _ from 'lodash';
-import MarkdownLegacy from '../../../shared/markdownLegacy.js';
-import Markdown from '../../../shared/markdown.js';
+import MarkdownLegacy from '@shared/markdownLegacy.js';
+import Markdown from '@shared/markdown.js';
import ErrorBar from './errorBar/errorBar.jsx';
import ToolBar from './toolBar/toolBar.jsx';
@@ -13,10 +15,10 @@ import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx';
import NotificationPopup from './notificationPopup/notificationPopup.jsx';
import Frame from 'react-frame-component';
import dedent from 'dedent';
-import { printCurrentBrew } from '../../../shared/helpers.js';
+import { printCurrentBrew } from '@shared/helpers.js';
import HeaderNav from './headerNav/headerNav.jsx';
-import { safeHTML } from './safeHTML.js';
+import safeHTML from './safeHTML.js';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
@@ -29,6 +31,8 @@ const INITIAL_CONTENT = dedent`
+
+
`;
diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less
index 5769a13df..ea9b43d75 100644
--- a/client/homebrew/brewRenderer/brewRenderer.less
+++ b/client/homebrew/brewRenderer/brewRenderer.less
@@ -1,4 +1,4 @@
-@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
+@import '@sharedStyles/core.less';
.brewRenderer {
height : 100vh;
diff --git a/client/homebrew/brewRenderer/errorBar/errorBar.less b/client/homebrew/brewRenderer/errorBar/errorBar.less
index 163648533..528521a07 100644
--- a/client/homebrew/brewRenderer/errorBar/errorBar.less
+++ b/client/homebrew/brewRenderer/errorBar/errorBar.less
@@ -1,3 +1,4 @@
+@import '@sharedStyles/colors.less';
.errorBar {
position : absolute;
diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx
index dd05391f0..5f4fc5608 100644
--- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx
+++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx
@@ -1,7 +1,7 @@
import './notificationPopup.less';
import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js';
-import Markdown from 'markdown.js';
+import Markdown from '@shared/markdown.js';
import Dialog from '../../../components/dialog.jsx';
diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
index 85d4c8365..4f34bad8d 100644
--- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
+++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
@@ -1,3 +1,5 @@
+@import './client/homebrew/navbar/navbar.less';
+
.popups {
position : fixed;
top : calc(@navbarHeight + @viewerToolsHeight);
diff --git a/client/homebrew/brewRenderer/safeHTML.js b/client/homebrew/brewRenderer/safeHTML.js
index 2574f4cfe..d9438b663 100644
--- a/client/homebrew/brewRenderer/safeHTML.js
+++ b/client/homebrew/brewRenderer/safeHTML.js
@@ -43,4 +43,4 @@ function safeHTML(htmlString) {
return div.innerHTML;
};
-module.exports.safeHTML = safeHTML;
\ No newline at end of file
+export default safeHTML;
\ No newline at end of file
diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx
index 9707fe84f..7f55ebf08 100644
--- a/client/homebrew/editor/editor.jsx
+++ b/client/homebrew/editor/editor.jsx
@@ -4,7 +4,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import dedent from 'dedent';
-import Markdown from '../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
import SnippetBar from './snippetbar/snippetbar.jsx';
diff --git a/client/homebrew/editor/editor.less b/client/homebrew/editor/editor.less
index d7b51a428..3851b50c5 100644
--- a/client/homebrew/editor/editor.less
+++ b/client/homebrew/editor/editor.less
@@ -1,4 +1,6 @@
-@import 'themes/codeMirror/customEditorStyles.less';
+@import '@sharedStyles/core.less';
+@import '@themes/codeMirror/customEditorStyles.less';
+
.editor {
position : relative;
width : 100%;
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index 281d62c93..acd457d98 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -7,7 +7,8 @@ import request from '../../utils/request-middleware.js';
import Combobox from '../../../components/combobox.jsx';
import TagInput from '../tagInput/tagInput.jsx';
-import Themes from 'themes/themes.json';
+
+import Themes from '@themes/themes.json';
import validations from './validations.js';
import homebreweryThumbnail from '../../thumbnail.png';
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less
index e684037e2..6a4df1c46 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.less
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.less
@@ -1,4 +1,4 @@
-@import 'naturalcrit/styles/colors.less';
+@import '@sharedStyles/core.less';
.userThemeName {
padding-right : 10px;
diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx
index b6e977ea2..304664ff5 100644
--- a/client/homebrew/editor/snippetbar/snippetbar.jsx
+++ b/client/homebrew/editor/snippetbar/snippetbar.jsx
@@ -7,13 +7,13 @@ import _ from 'lodash';
import cx from 'classnames';
import { loadHistory } from '../../utils/versionHistory.js';
-import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
+import { brewSnippetsToJSON } from '@shared/helpers.js';
-import Legacy5ePHB from 'themes/Legacy/5ePHB/snippets.js';
-import V3_5ePHB from 'themes/V3/5ePHB/snippets.js';
-import V3_5eDMG from 'themes/V3/5eDMG/snippets.js';
-import V3_Journal from 'themes/V3/Journal/snippets.js';
-import V3_Blank from 'themes/V3/Blank/snippets.js';
+import Legacy5ePHB from '@themes/Legacy/5ePHB/snippets.js';
+import V3_5ePHB from '@themes/V3/5ePHB/snippets.js';
+import V3_5eDMG from '@themes/V3/5eDMG/snippets.js';
+import V3_Journal from '@themes/V3/Journal/snippets.js';
+import V3_Blank from '@themes/V3/Blank/snippets.js';
const ThemeSnippets = {
Legacy_5ePHB : Legacy5ePHB,
@@ -23,7 +23,7 @@ const ThemeSnippets = {
V3_Blank : V3_Blank,
};
-import EditorThemes from 'build/homebrew/codeMirror/editorThemes.json';
+import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json';
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less
index 8e50aa764..37853ca75 100644
--- a/client/homebrew/editor/snippetbar/snippetbar.less
+++ b/client/homebrew/editor/snippetbar/snippetbar.less
@@ -1,5 +1,6 @@
+@import '@sharedStyles/core.less';
@import (less) './client/icons/customIcons.less';
-@import (less) '././././themes/fonts/5e/fonts.less';
+@import (less) '@themes/fonts/5e/fonts.less';
.snippetBar {
@menuHeight : 25px;
diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx
index 326287ee6..9ab69074b 100644
--- a/client/homebrew/homebrew.jsx
+++ b/client/homebrew/homebrew.jsx
@@ -1,8 +1,7 @@
-
-import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
+import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers
import './homebrew.less';
import React from 'react';
-import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
+import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
@@ -41,24 +40,21 @@ const Homebrew = (props)=>{
brews
} = props;
- global.account = account;
- global.version = version;
- global.config = config;
-
const backgroundObject = ()=>{
- if(global.config.deployment || (config.local && config.development)){
- const bgText = global.config.deployment || 'Local';
- return {
- backgroundImage : `url("data:image/svg+xml;utf8,")`
- };
+ if(config?.deployment || (config?.local && config?.development)) {
+ const bgText = config?.deployment || 'Local';
+ return {
+ backgroundImage : `url("data:image/svg+xml;utf8,")`
+ };
}
return null;
};
+
updateLocalStorage();
return (
-
-
+
+
} />
} />
@@ -80,4 +76,4 @@ const Homebrew = (props)=>{
);
};
-module.exports = Homebrew;
\ No newline at end of file
+export default Homebrew;
diff --git a/client/homebrew/homebrew.less b/client/homebrew/homebrew.less
index 2cbc35857..9bafadffe 100644
--- a/client/homebrew/homebrew.less
+++ b/client/homebrew/homebrew.less
@@ -1,4 +1,4 @@
-@import 'naturalcrit/styles/core.less';
+@import '@sharedStyles/core.less';
.homebrew {
height : 100%;
background-color:@steel;
diff --git a/client/homebrew/main.jsx b/client/homebrew/main.jsx
new file mode 100644
index 000000000..77a88d30f
--- /dev/null
+++ b/client/homebrew/main.jsx
@@ -0,0 +1,6 @@
+import { createRoot } from "react-dom/client";
+import Homebrew from "./homebrew.jsx";
+
+const props = window.__INITIAL_PROPS__ || {};
+
+createRoot(document.getElementById("reactRoot")).render();
diff --git a/client/homebrew/navbar/account.navitem.jsx b/client/homebrew/navbar/account.navitem.jsx
index 8c0e92023..0be06c471 100644
--- a/client/homebrew/navbar/account.navitem.jsx
+++ b/client/homebrew/navbar/account.navitem.jsx
@@ -97,7 +97,7 @@ const Account = createReactClass({
// Logged out
// LOCAL ONLY
- if(global.config.local) {
+ if(global.config?.local) {
return
login
;
diff --git a/client/homebrew/navbar/error-navitem.less b/client/homebrew/navbar/error-navitem.less
index 637ddac95..4da665915 100644
--- a/client/homebrew/navbar/error-navitem.less
+++ b/client/homebrew/navbar/error-navitem.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/core.less';
+
.navItem.error {
position : relative;
background-color : @red;
diff --git a/client/homebrew/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx
index e75be8d59..aa77dd2a0 100644
--- a/client/homebrew/navbar/navbar.jsx
+++ b/client/homebrew/navbar/navbar.jsx
@@ -7,17 +7,11 @@ import PatreonNavItem from './patreon.navitem.jsx';
const Navbar = createReactClass({
displayName : 'Navbar',
- getInitialState : function() {
- return {
- //showNonChromeWarning : false,
- ver : '0.0.0'
- };
- },
-
- getInitialState : function() {
- return {
- ver : global.version
- };
+ getInitialState: function() {
+ return {
+ // showNonChromeWarning: false, // uncomment if needed
+ ver: global.version || '0.0.0'
+ };
},
/*
diff --git a/client/homebrew/navbar/navbar.less b/client/homebrew/navbar/navbar.less
index 7b0217bf8..eb518e808 100644
--- a/client/homebrew/navbar/navbar.less
+++ b/client/homebrew/navbar/navbar.less
@@ -1,4 +1,4 @@
-@import 'naturalcrit/styles/colors.less';
+@import '@sharedStyles/core.less';
@navbarHeight : 28px;
@viewerToolsHeight : 32px;
diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx
index 4664d509b..ac72121f1 100644
--- a/client/homebrew/navbar/newbrew.navitem.jsx
+++ b/client/homebrew/navbar/newbrew.navitem.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import Nav from './nav.jsx';
-import { splitTextStyleAndMetadata } from '../../../shared/helpers.js';
+import { splitTextStyleAndMetadata } from '@shared/helpers.js';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
diff --git a/client/homebrew/navbar/print.navitem.jsx b/client/homebrew/navbar/print.navitem.jsx
index e7a6ff053..ea262cf03 100644
--- a/client/homebrew/navbar/print.navitem.jsx
+++ b/client/homebrew/navbar/print.navitem.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import Nav from './nav.jsx';
-import { printCurrentBrew } from '../../../shared/helpers.js';
+import { printCurrentBrew } from '@shared/helpers.js';
export default function(){
return
diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
index d335f3ca9..93685947c 100644
--- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
+++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
@@ -1,3 +1,4 @@
+@import '@sharedStyles/core.less';
.brewItem {
position : relative;
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx
index fa33e2863..d40058557 100644
--- a/client/homebrew/pages/editPage/editPage.jsx
+++ b/client/homebrew/pages/editPage/editPage.jsx
@@ -4,34 +4,35 @@ import './editPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
-import Markdown from '../../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
-import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
+import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
-import AccountNavItem from '../../navbar/account.navitem.jsx';
-import ErrorNavItem from '../../navbar/error-navitem.jsx';
-import HelpNavItem from '../../navbar/help.navitem.jsx';
-import VaultNavItem from '../../navbar/vault.navitem.jsx';
-import PrintNavItem from '../../navbar/print.navitem.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import NewBrewItem from '@navbar/newbrew.navitem.jsx';
+import AccountNavItem from '@navbar/account.navitem.jsx';
+import ErrorNavItem from '@navbar/error-navitem.jsx';
+import HelpNavItem from '@navbar/help.navitem.jsx';
+import VaultNavItem from '@navbar/vault.navitem.jsx';
+import PrintNavItem from '@navbar/print.navitem.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
-import { Meta } from 'vitreum/headtags';
+import Headtags from '../../../../vitreum/headtags.js';
+const Meta = Headtags.Meta;
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
-import ShareNavItem from '../../navbar/share.navitem.jsx';
+import ShareNavItem from '@navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
@@ -77,7 +78,7 @@ const EditPage = (props)=>{
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null);
- const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
+ const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
diff --git a/client/homebrew/pages/errorPage/errorPage.jsx b/client/homebrew/pages/errorPage/errorPage.jsx
index ffbfc43bb..837775b97 100644
--- a/client/homebrew/pages/errorPage/errorPage.jsx
+++ b/client/homebrew/pages/errorPage/errorPage.jsx
@@ -1,7 +1,7 @@
import './errorPage.less';
import React from 'react';
import UIPage from '../basePages/uiPage/uiPage.jsx';
-import Markdown from '../../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
import ErrorIndex from './errors/errorIndex.js';
const ErrorPage = ({ brew })=>{
diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx
index 36f6f8162..030e05a04 100644
--- a/client/homebrew/pages/homePage/homePage.jsx
+++ b/client/homebrew/pages/homePage/homePage.jsx
@@ -4,30 +4,31 @@ import './homePage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
-import Markdown from '../../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
-import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
+import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
-import AccountNavItem from '../../navbar/account.navitem.jsx';
-import ErrorNavItem from '../../navbar/error-navitem.jsx';
-import HelpNavItem from '../../navbar/help.navitem.jsx';
-import VaultNavItem from '../../navbar/vault.navitem.jsx';
-import PrintNavItem from '../../navbar/print.navitem.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import NewBrewItem from '@navbar/newbrew.navitem.jsx';
+import AccountNavItem from '@navbar/account.navitem.jsx';
+import ErrorNavItem from '@navbar/error-navitem.jsx';
+import HelpNavItem from '@navbar/help.navitem.jsx';
+import VaultNavItem from '@navbar/vault.navitem.jsx';
+import PrintNavItem from '@navbar/print.navitem.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
-import { Meta } from 'vitreum/headtags';
+import Headtags from '@vitreum/headtags.js';
+const Meta = Headtags.Meta;
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
diff --git a/client/homebrew/pages/homePage/homePage.less b/client/homebrew/pages/homePage/homePage.less
index b89c2c7dd..fd072c1e1 100644
--- a/client/homebrew/pages/homePage/homePage.less
+++ b/client/homebrew/pages/homePage/homePage.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/core.less';
+
.homePage {
position : relative;
a.floatingNewButton {
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index 1559d223c..7f3247d04 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -4,29 +4,28 @@ import './newPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
-import Markdown from '../../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
-import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
+import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
-import AccountNavItem from '../../navbar/account.navitem.jsx';
-import ErrorNavItem from '../../navbar/error-navitem.jsx';
-import HelpNavItem from '../../navbar/help.navitem.jsx';
-import VaultNavItem from '../../navbar/vault.navitem.jsx';
-import PrintNavItem from '../../navbar/print.navitem.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import NewBrewItem from '@navbar/newbrew.navitem.jsx';
+import AccountNavItem from '@navbar/account.navitem.jsx';
+import ErrorNavItem from '@navbar/error-navitem.jsx';
+import HelpNavItem from '@navbar/help.navitem.jsx';
+import VaultNavItem from '@navbar/vault.navitem.jsx';
+import PrintNavItem from '@navbar/print.navitem.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
-import { Meta } from 'vitreum/headtags';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
@@ -59,7 +58,7 @@ const NewPage = (props)=>{
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
// const saveTimeout = useRef(null);
// const warnUnsavedTimeout = useRef(null);
- const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
+ const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
diff --git a/client/homebrew/pages/newPage/newPage.less b/client/homebrew/pages/newPage/newPage.less
index d2f07ac40..39befb44e 100644
--- a/client/homebrew/pages/newPage/newPage.less
+++ b/client/homebrew/pages/newPage/newPage.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/colors.less';
+
.newPage {
.navItem.save {
background-color : @orange;
diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx
index 4a0b0f99c..093fc8965 100644
--- a/client/homebrew/pages/sharePage/sharePage.jsx
+++ b/client/homebrew/pages/sharePage/sharePage.jsx
@@ -1,18 +1,19 @@
import './sharePage.less';
import React, { useState, useEffect, useCallback } from 'react';
-import { Meta } from 'vitreum/headtags';
+import Headtags from '../../../../vitreum/headtags.js';
+const Meta = Headtags.Meta;
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import MetadataNav from '../../navbar/metadata.navitem.jsx';
-import PrintNavItem from '../../navbar/print.navitem.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import MetadataNav from '@navbar/metadata.navitem.jsx';
+import PrintNavItem from '@navbar/print.navitem.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
-import Account from '../../navbar/account.navitem.jsx';
+import Account from '@navbar/account.navitem.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
-import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
+import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx
index a6a43858e..0b2ebb56b 100644
--- a/client/homebrew/pages/userPage/userPage.jsx
+++ b/client/homebrew/pages/userPage/userPage.jsx
@@ -3,15 +3,15 @@ import _ from 'lodash';
import ListPage from '../basePages/listPage/listPage.jsx';
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
-import Account from '../../navbar/account.navitem.jsx';
-import NewBrew from '../../navbar/newbrew.navitem.jsx';
-import HelpNavItem from '../../navbar/help.navitem.jsx';
-import ErrorNavItem from '../../navbar/error-navitem.jsx';
-import VaultNavitem from '../../navbar/vault.navitem.jsx';
+import Account from '@navbar/account.navitem.jsx';
+import NewBrew from '@navbar/newbrew.navitem.jsx';
+import HelpNavItem from '@navbar/help.navitem.jsx';
+import ErrorNavItem from '@navbar/error-navitem.jsx';
+import VaultNavitem from '@navbar/vault.navitem.jsx';
const UserPage = (props)=>{
props = {
diff --git a/client/homebrew/pages/vaultPage/vaultPage.jsx b/client/homebrew/pages/vaultPage/vaultPage.jsx
index 3a8c6bc25..e6a9768bb 100644
--- a/client/homebrew/pages/vaultPage/vaultPage.jsx
+++ b/client/homebrew/pages/vaultPage/vaultPage.jsx
@@ -3,13 +3,13 @@
import './vaultPage.less';
import React, { useState, useEffect, useRef } from 'react';
-import Nav from '../../navbar/nav.jsx';
-import Navbar from '../../navbar/navbar.jsx';
-import RecentNavItems from '../../navbar/recent.navitem.jsx';
+import Nav from '@navbar/nav.jsx';
+import Navbar from '@navbar/navbar.jsx';
+import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
-import Account from '../../navbar/account.navitem.jsx';
-import NewBrew from '../../navbar/newbrew.navitem.jsx';
-import HelpNavItem from '../../navbar/help.navitem.jsx';
+import Account from '@navbar/account.navitem.jsx';
+import NewBrew from '@navbar/newbrew.navitem.jsx';
+import HelpNavItem from '@navbar/help.navitem.jsx';
import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import ErrorIndex from '../errorPage/errors/errorIndex.js';
diff --git a/client/homebrew/pages/vaultPage/vaultPage.less b/client/homebrew/pages/vaultPage/vaultPage.less
index 304fefc72..b2010b59b 100644
--- a/client/homebrew/pages/vaultPage/vaultPage.less
+++ b/client/homebrew/pages/vaultPage/vaultPage.less
@@ -1,3 +1,5 @@
+@import '@sharedStyles/core.less';
+
.vaultPage {
height : 100%;
overflow-y : hidden;
diff --git a/client/template.js b/client/template.js
deleted file mode 100644
index 537be6748..000000000
--- a/client/template.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const template = async function(name, title='', props = {}){
- const ogTags = [];
- const ogMeta = props.ogMeta ?? {};
- Object.entries(ogMeta).forEach(([key, value])=>{
- if(!value) return;
- const tag = ``;
- ogTags.push(tag);
- });
- const ogMetaTags = ogTags.join('\n');
-
- const ssrModule = await import(`../build/${name}/ssr.cjs`);
-
- return `
-
-
-
-
-
-
- ${ogMetaTags}
-
- ${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}
-
-
- ${ssrModule.default(props)}
-
-
-
-
- `;
-};
-
-export default template;
\ No newline at end of file
diff --git a/themes/codeMirror/editorThemes.json b/editorThemes.json
similarity index 100%
rename from themes/codeMirror/editorThemes.json
rename to editorThemes.json
diff --git a/index.html b/index.html
new file mode 100644
index 000000000..5ee864b28
--- /dev/null
+++ b/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ The Homebrewery - NaturalCrit
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 610d37a53..521e60c54 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,8 @@
"url": "git://github.com/naturalcrit/homebrewery.git"
},
"scripts": {
- "dev": "node --experimental-require-module scripts/dev.js",
- "quick": "node --experimental-require-module scripts/quick.js",
- "build": "node --experimental-require-module scripts/buildHomebrew.js && node --experimental-require-module scripts/buildAdmin.js",
- "builddev": "node --experimental-require-module scripts/buildHomebrew.js --dev",
+ "start": "node server.js",
+ "build": "vite build",
"lint": "eslint --fix",
"lint:dry": "eslint",
"stylelint": "stylelint --fix **/*.{less}",
@@ -44,7 +42,6 @@
"phb": "node --experimental-require-module scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build",
- "start": "node --experimental-require-module server.js",
"docker:build": "docker build -t ${DOCKERID}/homebrewery:$npm_package_version .",
"docker:publish": "docker login && docker push ${DOCKERID}/homebrewery:$npm_package_version"
},
@@ -93,10 +90,11 @@
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
- "@babel/runtime": "^7.28.4",
+ "@babel/runtime": "^7.28.6",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^20.1.0",
"@sanity/diff-match-patch": "^3.2.0",
+ "@vitejs/plugin-react": "^5.1.2",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
@@ -105,7 +103,6 @@
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent": "^1.7.1",
- "expr-eval": "^2.0.2",
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0",
@@ -136,14 +133,11 @@
"react-dom": "^18.3.1",
"react-frame-component": "^5.2.7",
"react-router": "^7.9.6",
- "romans": "^3.1.0",
"sanitize-filename": "1.6.3",
- "superagent": "^10.2.1",
- "vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
- "written-number": "^0.11.1"
+ "superagent": "^10.2.1"
},
"devDependencies": {
- "@stylistic/stylelint-plugin": "^4.0.0",
+ "@stylistic/stylelint-plugin": "^5.0.1",
"babel-jest": "^30.2.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.39.1",
@@ -155,9 +149,10 @@
"jsdom": "^28.1.0",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
- "stylelint": "^16.25.0",
- "stylelint-config-recess-order": "^7.3.0",
- "stylelint-config-recommended": "^17.0.0",
- "supertest": "^7.1.4"
+ "stylelint": "^17.4.0",
+ "stylelint-config-recess-order": "^7.6.1",
+ "stylelint-config-recommended": "^18.0.0",
+ "supertest": "^7.1.4",
+ "vite": "^7.3.1"
}
}
diff --git a/scripts/buildAdmin.js b/scripts/buildAdmin.js
deleted file mode 100644
index 9c77315ef..000000000
--- a/scripts/buildAdmin.js
+++ /dev/null
@@ -1,32 +0,0 @@
-
-import fs from 'fs-extra';
-import Proj from './project.json' with { type: 'json' };
-import vitreum from 'vitreum';
-const { pack } = vitreum;
-
-import lessTransform from 'vitreum/transforms/less.js';
-import assetTransform from 'vitreum/transforms/asset.js';
-
-const isDev = !!process.argv.find((arg)=>arg=='--dev');
-
-const transforms = {
- '.less' : lessTransform,
- '*' : assetTransform('./build')
-};
-
-const build = async ({ bundle, render, ssr })=>{
- const css = await lessTransform.generate({ paths: './shared' });
- await fs.outputFile('./build/admin/bundle.css', css);
- await fs.outputFile('./build/admin/bundle.js', bundle);
- await fs.outputFile('./build/admin/ssr.cjs', ssr);
-};
-
-fs.emptyDirSync('./build/admin');
-pack('./client/admin/admin.jsx', {
- paths : ['./shared'],
- libs : Proj.libs,
- dev : isDev && build,
- transforms
-})
- .then(build)
- .catch(console.error);
diff --git a/scripts/buildHomebrew.js b/scripts/buildHomebrew.js
deleted file mode 100644
index 4d55a4176..000000000
--- a/scripts/buildHomebrew.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import fs from 'fs-extra';
-import zlib from 'zlib';
-import Proj from './project.json' with { type: 'json' };
-import vitreum from 'vitreum';
-const { pack, watchFile, livereload } = vitreum;
-
-import lessTransform from 'vitreum/transforms/less.js';
-import assetTransform from 'vitreum/transforms/asset.js';
-import babel from '@babel/core';
-import babelConfig from '../babel.config.json' with { type : 'json' };
-import less from 'less';
-
-const isDev = !!process.argv.find((arg)=>arg === '--dev');
-
-const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
-
-const transforms = {
- '.js' : (code, filename, opts)=>babelify(code),
- '.jsx' : (code, filename, opts)=>babelify(code),
- '.less' : lessTransform,
- '*' : assetTransform('./build')
-};
-
-const build = async ({ bundle, render, ssr })=>{
- const css = await lessTransform.generate({ paths: './shared' });
- //css = `@layer bundle {\n${css}\n}`;
- await fs.outputFile('./build/homebrew/bundle.css', css);
- await fs.outputFile('./build/homebrew/bundle.js', bundle);
- await fs.outputFile('./build/homebrew/ssr.cjs', ssr);
-
- await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
-
- //compress files in production
- if(!isDev){
- await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
- await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
- await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
- } else {
- await fs.remove('./build/homebrew/bundle.css.br');
- await fs.remove('./build/homebrew/bundle.js.br');
- await fs.remove('./build/homebrew/ssr.js.br');
- }
-};
-
-fs.emptyDirSync('./build');
-
-
-(async ()=>{
-
- //v==----------------------------- COMPILE THEMES --------------------------------==v//
-
- // Update list of all Theme files
- const themes = { Legacy: {}, V3: {} };
-
- let themeFiles = fs.readdirSync('./themes/Legacy');
- for (const dir of themeFiles) {
- const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
- themeData.path = dir;
- themes.Legacy[dir] = (themeData);
- //fs.copy(`./themes/Legacy/${dir}/dropdownTexture.png`, `./build/themes/Legacy/${dir}/dropdownTexture.png`);
- const src = `./themes/Legacy/${dir}/style.less`;
- ((outputDirectory)=>{
- less.render(fs.readFileSync(src).toString(), {
- compress : !isDev
- }, function(e, output) {
- fs.outputFile(outputDirectory, output.css);
- });
- })(`./build/themes/Legacy/${dir}/style.css`);
-
- }
-
- themeFiles = fs.readdirSync('./themes/V3');
- for (const dir of themeFiles) {
- const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
- themeData.path = dir;
- themes.V3[dir] = (themeData);
- fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
- fs.copy(`./themes/V3/${dir}/dropdownPreview.png`, `./build/themes/V3/${dir}/dropdownPreview.png`);
- const src = `./themes/V3/${dir}/style.less`;
- ((outputDirectory)=>{
- less.render(fs.readFileSync(src).toString(), {
- compress : !isDev
- }, function(e, output) {
- fs.outputFile(outputDirectory, output.css);
- });
- })(`./build/themes/V3/${dir}/style.css`);
- }
-
- await fs.outputFile('./themes/themes.json', JSON.stringify(themes, null, 2));
-
- // await less.render(lessCode, {
- // compress : !dev,
- // sourceMap : (dev ? {
- // sourceMapFileInline: true,
- // outputSourceFiles: true
- // } : false),
- // })
-
- // Move assets
- await fs.copy('./themes/fonts', './build/fonts');
- await fs.copy('./themes/assets', './build/assets');
- await fs.copy('./client/icons', './build/icons');
-
- //v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
-
- const editorThemesBuildDir = './build/homebrew/cm-themes';
- await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
- await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
- const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
-
- const editorThemeFile = './themes/codeMirror/editorThemes.json';
- if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
- const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
- stream.write('[\n"default"');
-
- for (const themeFile of editorThemeFiles) {
- stream.write(`,\n"${themeFile.slice(0, -4)}"`);
- }
- stream.write('\n]\n');
- stream.end();
-
-
- await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
-
- //v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
-
- const bundles = await pack('./client/homebrew/homebrew.jsx', {
- paths : ['./shared', './'],
- libs : Proj.libs,
- dev : isDev && build,
- transforms
- });
- build(bundles);
-
- // Possible method for generating separate bundles for theme snippets: factor-bundle first sending all common files to bundle.js, then again using default settings, keeping only snippet bundles
- // await fs.outputFile('./build/junk.js', '');
- // await fs.outputFile('./build/themes/Legacy/5ePHB/snippets.js', '');
- //
- // const files = ['./client/homebrew/homebrew.jsx','./themes/Legacy/5ePHB/snippets.js'];
- //
- // bundles = await pack(files, {
- // dedupe: false,
- // plugin : [['factor-bundle', { outputs: [ './build/junk.js','./build/themes/Legacy/5ePHB/snippets.js'], threshold : function(row, groups) {
- // console.log(groups);
- // if (groups.some(group => /.*homebrew.jsx$/.test(group))) {
- // console.log("found homebrewery")
- // return true;
- // }
- // return this._defaultThreshold(row, groups);
- // }}]],
- // paths : ['./shared','./','./build'],
- // libs : Proj.libs,
- // dev : isDev && build,
- // transforms
- // });
- // build(bundles);
- //
-
- //In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
- if(isDev){
- livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
- watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
- ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
- ext : 'js json' // Extensions to watch (only .js/.json by default)
- //watch : ['./server', './themes'], // Watch additional folders if needed
- });
- }
-
-})().catch(console.error);
\ No newline at end of file
diff --git a/scripts/dev.js b/scripts/dev.js
deleted file mode 100644
index 45f6c3d99..000000000
--- a/scripts/dev.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const label = 'dev';
-console.time(label);
-
-const jsx = require('vitreum/steps/jsx.watch.js');
-const less = require('vitreum/steps/less.watch.js');
-const assets = require('vitreum/steps/assets.watch.js');
-const server = require('vitreum/steps/server.watch.js');
-const livereload = require('vitreum/steps/livereload.js');
-
-const Proj = require('./project.json');
-
-Promise.resolve()
- .then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
- .then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
- .then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
- .then((deps)=>less('admin', { shared: ['./shared'] }, deps))
-
- .then(()=>assets(Proj.assets, ['./shared', './client']))
- .then(()=>livereload())
- .then(()=>server('./server.js', ['server']))
- .then(console.timeEnd.bind(console, label))
- .catch(console.error);
\ No newline at end of file
diff --git a/scripts/quick.js b/scripts/quick.js
deleted file mode 100644
index e763d85f7..000000000
--- a/scripts/quick.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const label = 'quick';
-console.time(label);
-
-const jsx = require('vitreum/steps/jsx.js').partial;
-const less = require('vitreum/steps/less.js').partial;
-const server = require('vitreum/steps/server.watch.js').partial;
-
-const Proj = require('./project.json');
-
-Promise.resolve()
- .then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
- .then(less('homebrew', ['./shared']))
- .then(jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
- .then(less('admin', ['./shared']))
- .then(server('./server.js', ['server']))
- .then(console.timeEnd.bind(console, label))
- .catch(console.error);
\ No newline at end of file
diff --git a/server.js b/server.js
index fe5a9a363..02aeee356 100644
--- a/server.js
+++ b/server.js
@@ -1,20 +1,40 @@
-import DB from './server/db.js';
-import server from './server/app.js';
-import config from './server/config.js';
+import DB from "./server/db.js";
+import createApp from "./server/app.js";
+import config from "./server/config.js";
+import { createServer as createViteServer } from "vite";
-DB.connect(config).then(()=>{
- // Ensure that we have successfully connected to the database
- // before launching server
- const PORT = process.env.PORT || config.get('web_port') || 8000;
- server.listen(PORT, ()=>{
- const reset = '\x1b[0m'; // Reset to default style
- const bright = '\x1b[1m'; // Bright (bold) style
- const cyan = '\x1b[36m'; // Cyan color
- const underline = '\x1b[4m'; // Underlined style
+const isDev = process.env.NODE_ENV === "local";
+
+async function start() {
+ let vite;
+
+ if (isDev) {
+ vite = await createViteServer({
+ server: { middlewareMode: true },
+ appType: "custom",
+ });
+ }
+
+ await DB.connect(config).catch((err) => {
+ console.error("Database connection failed:", err);
+ process.exit(1);
+ });
+
+ const app = await createApp(vite);
+
+ const PORT = process.env.PORT || config.get("web_port") || 3000;
+ app.listen(PORT, () => {
+ const reset = "\x1b[0m"; // Reset to default style
+ const bright = "\x1b[1m"; // Bright (bold) style
+ const cyan = "\x1b[36m"; // Cyan color
+ const underline = "\x1b[4m"; // Underlined style
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`);
- console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`);
-
+ console.log(
+ `\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`,
+ );
});
-});
+}
+
+start();
diff --git a/server/admin.api.js b/server/admin.api.js
index a3d7622f1..93e0036d1 100644
--- a/server/admin.api.js
+++ b/server/admin.api.js
@@ -4,18 +4,23 @@ import { model as NotificationModel } from './notifications.model.js';
import express from 'express';
import Moment from 'moment';
import zlib from 'zlib';
-import templateFn from '../client/template.js';
+import config from './config.js';
+import path from 'path';
+import fs from 'fs-extra';
+
+const nodeEnv = config.get('node_env');
+const isProd = nodeEnv === 'production';
import HomebrewAPI from './homebrew.api.js';
import asyncHandler from 'express-async-handler';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
-const router = express.Router();
-
-
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
+export default function createAdminApi(vite) {
+ const router = express.Router();
+
const mw = {
adminOnly : (req, res, next)=>{
if(!req.get('authorization')){
@@ -371,15 +376,28 @@ router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, n
}
});
-router.get('/admin', mw.adminOnly, (req, res)=>{
- templateFn('admin', {
+router.get('/admin', mw.adminOnly, asyncHandler(async (req, res) => {
+ const props = {
url : req.originalUrl
- })
- .then((page)=>res.send(page))
- .catch((err)=>{
- console.log(err);
- res.sendStatus(500);
- });
-});
+ };
+
+ const htmlPath = isProd
+ ? path.resolve('build', 'index.html')
+ : path.resolve('index.html');
+
+ let html = fs.readFileSync(htmlPath, 'utf-8');
+
+ if (!isProd && vite?.transformIndexHtml) {
+ html = await vite.transformIndexHtml(req.originalUrl, html);
+ }
+
+ res.send(html.replace(
+ '',
+ `\n`
+ ));
+}));
+
+
+ return router;
+}
-export default router;
diff --git a/server/admin.api.spec.js b/server/admin.api.spec.js
index cb25dd67d..ce2a06d0d 100644
--- a/server/admin.api.spec.js
+++ b/server/admin.api.spec.js
@@ -1,42 +1,45 @@
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
import mongoose from 'mongoose';
import supertest from 'supertest';
-import HBApp from './app.js';
+import createApp from './app.js';
import { model as NotificationModel } from './notifications.model.js';
import { model as HomebrewModel } from './homebrew.model.js';
-
-// Mimic https responses to avoid being redirected all the time
-const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
-
+let app;
+let request;
let dbState;
+beforeAll(async ()=>{
+ app = await createApp();
+ request = supertest.agent(app).set('X-Forwarded-Proto', 'https');
+});
+
describe('Tests for admin api', ()=>{
beforeEach(()=>{
- // Mock DB ready (for dbCheck middleware)
dbState = mongoose.connection.readyState;
mongoose.connection.readyState = 1;
});
afterEach(()=>{
- // Restore DB ready state
mongoose.connection.readyState = dbState;
-
jest.resetAllMocks();
});
+ afterAll(async ()=>{
+ await mongoose.connection.close();
+ });
+
describe('Notifications', ()=>{
it('should return list of all notifications', async ()=>{
const testNotifications = ['a', 'b'];
- jest.spyOn(NotificationModel, 'find')
- .mockImplementationOnce(()=>{
+ jest.spyOn(NotificationModel, 'find').mockImplementationOnce(()=>{
return { exec: jest.fn().mockResolvedValue(testNotifications) };
});
- const response = await app
- .get('/admin/notification/all')
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
+ const response = await request
+ .get('/admin/notification/all')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200);
expect(response.body).toEqual(testNotifications);
@@ -56,18 +59,17 @@ describe('Tests for admin api', ()=>{
_id : expect.any(String),
createdAt : expect.any(String),
startAt : inputNotification.startAt,
- stopAt : inputNotification.stopAt,
+ stopAt : inputNotification.stopAt
};
- jest.spyOn(NotificationModel.prototype, 'save')
- .mockImplementationOnce(function() {
- return Promise.resolve(this);
- });
+ jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
+ return Promise.resolve(this);
+ });
- const response = await app
- .post('/admin/notification/add')
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .send(inputNotification);
+ const response = await request
+ .post('/admin/notification/add')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(inputNotification);
expect(response.status).toBe(201);
expect(response.body).toEqual(savedNotification);
@@ -81,16 +83,14 @@ describe('Tests for admin api', ()=>{
stopAt : new Date().toISOString()
};
- //Change 'save' function to just return itself instead of actually interacting with the database
- jest.spyOn(NotificationModel.prototype, 'save')
- .mockImplementationOnce(function() {
- return Promise.resolve(this);
- });
+ jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
+ return Promise.resolve(this);
+ });
- const response = await app
- .post('/admin/notification/add')
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .send(inputNotification);
+ const response = await request
+ .post('/admin/notification/add')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(inputNotification);
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Dismiss key is required!' });
@@ -99,15 +99,15 @@ describe('Tests for admin api', ()=>{
it('should delete a notification based on its dismiss key', async ()=>{
const dismissKey = 'testKey';
- jest.spyOn(NotificationModel, 'findOneAndDelete')
- .mockImplementationOnce((key)=>{
- return { exec: jest.fn().mockResolvedValue(key) };
- });
- const response = await app
- .delete(`/admin/notification/delete/${dismissKey}`)
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
+ jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce((key)=>{
+ return { exec: jest.fn().mockResolvedValue(key) };
+ });
- expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
+ const response = await request
+ .delete(`/admin/notification/delete/${dismissKey}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
+
+ expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
expect(response.status).toBe(200);
expect(response.body).toEqual({ dismissKey: 'testKey' });
});
@@ -115,15 +115,15 @@ describe('Tests for admin api', ()=>{
it('should handle error deleting a notification that doesnt exist', async ()=>{
const dismissKey = 'testKey';
- jest.spyOn(NotificationModel, 'findOneAndDelete')
- .mockImplementationOnce(()=>{
- return { exec: jest.fn().mockResolvedValue() };
- });
- const response = await app
- .delete(`/admin/notification/delete/${dismissKey}`)
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
+ jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce(()=>{
+ return { exec: jest.fn().mockResolvedValue() };
+ });
- expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
+ const response = await request
+ .delete(`/admin/notification/delete/${dismissKey}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
+
+ expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Notification not found' });
});
@@ -132,30 +132,24 @@ describe('Tests for admin api', ()=>{
describe('Locks', ()=>{
describe('Count', ()=>{
it('Count of all locked documents', async ()=>{
- const testNumber = 16777216; // 8^8, because why not
+ const testNumber = 16777216;
- jest.spyOn(HomebrewModel, 'countDocuments')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testNumber);
- });
+ jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.resolve(testNumber));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/lock/count');
+ const response = await request
+ .get('/api/lock/count')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({ count: testNumber });
});
it('Handle error while fetching count of locked documents', async ()=>{
- jest.spyOn(HomebrewModel, 'countDocuments')
- .mockImplementationOnce(()=>{
- return Promise.reject();
- });
+ jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.reject());
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/lock/count');
+ const response = await request
+ .get('/api/lock/count')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -163,7 +157,7 @@ describe('Tests for admin api', ()=>{
message : 'Unable to get lock count',
name : 'Lock Count Error',
originalUrl : '/api/lock/count',
- status : 500,
+ status : 500
});
});
});
@@ -172,28 +166,22 @@ describe('Tests for admin api', ()=>{
it('Get list of all locked documents', async ()=>{
const testLocks = ['a', 'b'];
- jest.spyOn(HomebrewModel, 'aggregate')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testLocks);
- });
+ jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/locks');
+ const response = await request
+ .get('/api/locks')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({ lockedDocuments: testLocks });
});
it('Handle error while fetching list of all locked documents', async ()=>{
- jest.spyOn(HomebrewModel, 'aggregate')
- .mockImplementationOnce(()=>{
- return Promise.reject();
- });
+ jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/locks');
+ const response = await request
+ .get('/api/locks')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -208,28 +196,22 @@ describe('Tests for admin api', ()=>{
it('Get list of all locked documents with pending review requests', async ()=>{
const testLocks = ['a', 'b'];
- jest.spyOn(HomebrewModel, 'aggregate')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testLocks);
- });
+ jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/lock/reviews');
+ const response = await request
+ .get('/api/lock/reviews')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({ reviewDocuments: testLocks });
});
it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{
- jest.spyOn(HomebrewModel, 'aggregate')
- .mockImplementationOnce(()=>{
- return Promise.reject();
- });
+ jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .get('/api/lock/reviews');
+ const response = await request
+ .get('/api/lock/reviews')
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -247,8 +229,8 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); }
+ markModified : ()=>true,
+ save : ()=>Promise.resolve()
};
const testLock = {
@@ -257,15 +239,12 @@ describe('Tests for admin api', ()=>{
shareMessage : 'share'
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .post(`/api/lock/${testBrew.shareId}`)
- .send(testLock);
+ const response = await request
+ .post(`/api/lock/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(testLock);
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
@@ -289,24 +268,21 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve(),
lock : {
code : 1,
editMessage : 'oldEdit',
- shareMessage : 'oldShare',
+ shareMessage : 'oldShare'
}
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .post(`/api/lock/${testBrew.shareId}`)
- .send(testLock);
+ const response = await request
+ .post(`/api/lock/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(testLock);
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
@@ -329,24 +305,21 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve(),
lock : {
code : 1,
editMessage : 'oldEdit',
- shareMessage : 'oldShare',
+ shareMessage : 'oldShare'
}
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .post(`/api/lock/${testBrew.shareId}`)
- .send(testLock);
+ const response = await request
+ .post(`/api/lock/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(testLock);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -364,8 +337,8 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.reject(); }
+ markModified : ()=>true,
+ save : ()=>Promise.reject()
};
const testLock = {
@@ -374,15 +347,12 @@ describe('Tests for admin api', ()=>{
shareMessage : 'share'
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .post(`/api/lock/${testBrew.shareId}`)
- .send(testLock);
+ const response = await request
+ .post(`/api/lock/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
+ .send(testLock);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -408,19 +378,17 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/unlock/${testBrew.shareId}`);
+ const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
+ 'Authorization',
+ `Basic ${Buffer.from('admin:password3').toString('base64')}`
+ );
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -433,18 +401,16 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve()
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/unlock/${testBrew.shareId}`);
+ const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
+ 'Authorization',
+ `Basic ${Buffer.from('admin:password3').toString('base64')}`
+ );
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -453,7 +419,7 @@ describe('Tests for admin api', ()=>{
name : 'Not Locked',
originalUrl : `/api/unlock/${testBrew.shareId}`,
shareId : testBrew.shareId,
- status : 500,
+ status : 500
});
});
@@ -468,19 +434,17 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.reject(); },
+ markModified : ()=>true,
+ save : ()=>Promise.reject(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/unlock/${testBrew.shareId}`);
+ const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
+ 'Authorization',
+ `Basic ${Buffer.from('admin:password3').toString('base64')}`
+ );
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -506,40 +470,28 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .put(`/api/lock/review/request/${testBrew.shareId}`);
+ const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({
message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`,
- name : 'Review Requested',
+ name : 'Review Requested'
});
});
it('Error when cannot find a locked brew', async ()=>{
- const testBrew = {
- shareId : 'shareId'
- };
+ const testBrew = { shareId: 'shareId' };
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(false);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
-
- const response = await app
- .put(`/api/lock/review/request/${testBrew.shareId}`)
- .catch((err)=>{return err;});
+ const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -569,25 +521,20 @@ describe('Tests for admin api', ()=>{
};
jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(false);
- });
+ .mockImplementationOnce(()=>Promise.resolve(testBrew));
-
- const response = await app
- .put(`/api/lock/review/request/${testBrew.shareId}`)
- .catch((err)=>{return err;});
+ const response = await request
+ .put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
- HBErrorCode : '70',
+ HBErrorCode : '71',
code : 500,
- message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
- name : 'Brew Not Found',
+ message : `Review already requested for brew ${testBrew.shareId} - ${testBrew.title}`,
+ name : 'Review Already Requested',
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
});
});
-
it('Handle error while adding review request to a locked brew', async ()=>{
const testLock = {
applied : 'YES',
@@ -599,18 +546,14 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.reject(); },
+ markModified : ()=>true,
+ save : ()=>Promise.reject(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .put(`/api/lock/review/request/${testBrew.shareId}`);
+ const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -634,19 +577,16 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.resolve(); },
+ markModified : ()=>true,
+ save : ()=>Promise.resolve(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/lock/review/remove/${testBrew.shareId}`);
+ const response = await request
+ .put(`/api/lock/review/remove/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -656,18 +596,13 @@ describe('Tests for admin api', ()=>{
});
it('Error when clearing review request from a brew with no review request', async ()=>{
- const testBrew = {
- shareId : 'shareId',
- };
+ const testBrew = { shareId: 'shareId' };
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(false);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/lock/review/remove/${testBrew.shareId}`);
+ const response = await request
+ .put(`/api/lock/review/remove/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -690,19 +625,16 @@ describe('Tests for admin api', ()=>{
const testBrew = {
shareId : 'shareId',
title : 'title',
- markModified : ()=>{ return true; },
- save : ()=>{ return Promise.reject(); },
+ markModified : ()=>true,
+ save : ()=>Promise.reject(),
lock : testLock
};
- jest.spyOn(HomebrewModel, 'findOne')
- .mockImplementationOnce(()=>{
- return Promise.resolve(testBrew);
- });
+ jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
- const response = await app
- .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
- .put(`/api/lock/review/remove/${testBrew.shareId}`);
+ const response = await request
+ .put(`/api/lock/review/remove/${testBrew.shareId}`)
+ .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({
diff --git a/server/app.js b/server/app.js
index 1bdb5aac3..07fc3ba9c 100644
--- a/server/app.js
+++ b/server/app.js
@@ -12,10 +12,9 @@ import _ from 'lodash';
import jwt from 'jwt-simple';
import express from 'express';
import config from './config.js';
+import path from 'path';
import fs from 'fs-extra';
-const app = express();
-
import api from './homebrew.api.js';
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
import adminApi from './admin.api.js';
@@ -24,7 +23,6 @@ import GoogleActions from './googleActions.js';
import serveCompressedStaticAssets from './static-assets.mv.js';
import sanitizeFilename from 'sanitize-filename';
import asyncHandler from 'express-async-handler';
-import templateFn from '../client/template.js';
import { model as HomebrewModel } from './homebrew.model.js';
import { DEFAULT_BREW } from './brewDefaults.js';
@@ -37,599 +35,626 @@ import cookieParser from 'cookie-parser';
import forceSSL from './forcessl.mw.js';
import dbCheck from './middleware/dbCheck.js';
-
-const sanitizeBrew = (brew, accessType)=>{
- brew._id = undefined;
- brew.__v = undefined;
- if(accessType !== 'edit' && accessType !== 'shareAuthor') {
- brew.editId = undefined;
- }
- return brew;
-};
-
-app.set('trust proxy', 1 /* number of proxies between user and server */);
-
-app.use('/', serveCompressedStaticAssets(`build`));
-app.use(contentNegotiation);
-app.use(bodyParser.json({ limit: '25mb' }));
-app.use(cookieParser());
-app.use(forceSSL);
-
import cors from 'cors';
-const nodeEnv = config.get('node_env');
-const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
+export default async function createApp(vite) {
+ const app = express();
-const corsOptions = {
- origin : (origin, callback)=>{
+ const nodeEnv = config.get('node_env');
+ const isProd = nodeEnv === 'production';
+ const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
- const allowedOrigins = [
- 'https://homebrewery.naturalcrit.com',
- 'https://www.naturalcrit.com',
- 'https://naturalcrit-stage.herokuapp.com',
- 'https://homebrewery-stage.herokuapp.com',
- ];
-
- const localNetworkRegex = /^http:\/\/(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+):\d+$/;
-
- const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
-
- if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin) || (isLocalEnvironment && localNetworkRegex.test(origin))) {
- callback(null, true);
- } else {
- console.log(origin, 'not allowed');
- callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
+ const sanitizeBrew = (brew, accessType)=>{
+ brew._id = undefined;
+ brew.__v = undefined;
+ if(accessType !== 'edit' && accessType !== 'shareAuthor') {
+ brew.editId = undefined;
}
- },
- methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
- credentials : true,
-};
+ return brew;
+ };
-app.use(cors(corsOptions));
+ app.set('trust proxy', 1 /* number of proxies between user and server */);
-//Account Middleware
-app.use((req, res, next)=>{
- if(req.cookies && req.cookies.nc_session){
- try {
- req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
+ if (vite) {
+ app.use(vite.middlewares);
+ }
+
+ app.use('/', serveCompressedStaticAssets('build'));
+ app.use(contentNegotiation);
+ app.use(bodyParser.json({ limit: '25mb' }));
+ app.use(cookieParser());
+ app.use(forceSSL);
+
+
+ const corsOptions = {
+ origin : (origin, callback)=>{
+
+ const allowedOrigins = [
+ 'https://homebrewery.naturalcrit.com',
+ 'https://www.naturalcrit.com',
+ 'https://naturalcrit-stage.herokuapp.com',
+ 'https://homebrewery-stage.herokuapp.com',
+ ];
+
+ const localNetworkRegex = /^http:\/\/(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+):\d+$/;
+
+ const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
+
+ if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin) || (isLocalEnvironment && localNetworkRegex.test(origin))) {
+ callback(null, true);
+ } else {
+ console.log(origin, 'not allowed');
+ callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
+ }
+ },
+ methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+ credentials : true,
+ };
+
+ app.use(cors(corsOptions));
+
+ //Account Middleware
+ app.use((req, res, next)=>{
+ if(req.cookies && req.cookies.nc_session){
+ try {
+ req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
//console.log("Just loaded up JWT from cookie:");
//console.log(req.account);
- } catch (e){
- console.log(e);
+ } catch (e){
+ console.log(e);
+ }
}
- }
- req.config = {
- google_client_id : config.get('google_client_id'),
- google_client_secret : config.get('google_client_secret')
- };
- return next();
-});
-
-app.use(homebrewApi);
-app.use(adminApi);
-app.use(vaultApi);
-
-const welcomeText = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
-const welcomeTextLegacy = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
-const migrateText = fs.readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
-const changelogText = fs.readFileSync('changelog.md', 'utf8');
-const faqText = fs.readFileSync('faq.md', 'utf8');
-
-String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
-
-const defaultMetaTags = {
- site_name : 'The Homebrewery - Make your Homebrew content look legit!',
- title : 'The Homebrewery',
- description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
- image : `${config.get('publicUrl')}/thumbnail.png`,
- type : 'website'
-};
-
-//Robots.txt
-app.get('/robots.txt', (req, res)=>{
- return res.sendFile(`robots.txt`, { root: process.cwd() });
-});
-
-//Home page
-app.get('/', (req, res, next)=>{
- req.brew = {
- text : welcomeText,
- renderer : 'V3',
- theme : '5ePHB'
- },
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'Homepage',
- description : 'Homepage'
- };
-
- splitTextStyleAndMetadata(req.brew);
- return next();
-});
-
-//Home page Legacy
-app.get('/legacy', (req, res, next)=>{
- req.brew = {
- text : welcomeTextLegacy,
- renderer : 'legacy',
- theme : '5ePHB'
- },
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'Homepage (Legacy)',
- description : 'Homepage'
- };
-
- splitTextStyleAndMetadata(req.brew);
- return next();
-});
-
-//Legacy/Other Document -> v3 Migration Guide
-app.get('/migrate', (req, res, next)=>{
- req.brew = {
- text : migrateText,
- renderer : 'V3',
- theme : '5ePHB'
- },
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'v3 Migration Guide',
- description : 'A brief guide to converting Legacy documents to the v3 renderer.'
- };
-
- splitTextStyleAndMetadata(req.brew);
- return next();
-});
-
-//Changelog page
-app.get('/changelog', async (req, res, next)=>{
- req.brew = {
- title : 'Changelog',
- text : changelogText,
- renderer : 'V3',
- theme : '5ePHB'
- },
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'Changelog',
- description : 'Development changelog.'
- };
-
- splitTextStyleAndMetadata(req.brew);
- return next();
-});
-
-//FAQ page
-app.get('/faq', async (req, res, next)=>{
- req.brew = {
- title : 'FAQ',
- text : faqText,
- renderer : 'V3',
- theme : '5ePHB'
- },
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'FAQ',
- description : 'Frequently Asked Questions'
- };
-
- splitTextStyleAndMetadata(req.brew);
- return next();
-});
-
-//Source page
-app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
-
- const replaceStrings = { '&': '&', '<': '<', '>': '>' };
- let text = brew.text;
- for (const replaceStr in replaceStrings) {
- text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
- }
- text = `${text}`;
- res.status(200).send(text);
-});
-
-//Download brew source page
-app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
- sanitizeBrew(brew, 'share');
- const prefix = 'HB - ';
-
- const encodeRFC3986ValueChars = (str)=>{
- return (
- encodeURIComponent(str)
- .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
- );
- };
-
- let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
- if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
- res.set({
- 'Cache-Control' : 'no-cache',
- 'Content-Type' : 'text/plain',
- 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
+ req.config = {
+ google_client_id : config.get('google_client_id'),
+ google_client_secret : config.get('google_client_secret')
+ };
+ return next();
});
- res.status(200).send(brew.text);
-});
-//Serve brew metadata
-app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
- sanitizeBrew(brew, 'share');
+ app.use(homebrewApi);
+ app.use(adminApi(vite));
+ app.use(vaultApi);
- const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
+ const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
+ const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
+ const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
+ const changelogText = fs.readFileSync('changelog.md', 'utf8');
+ const faqText = fs.readFileSync('faq.md', 'utf8');
+
+ String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
+
+ const defaultMetaTags = {
+ site_name : 'The Homebrewery - Make your Homebrew content look legit!',
+ title : 'The Homebrewery',
+ description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
+ image : `${config.get('publicUrl')}/thumbnail.png`,
+ type : 'website'
+ };
+
+ //Robots.txt
+ app.get('/robots.txt', (req, res)=>{
+ return res.sendFile(`robots.txt`, { root: process.cwd() });
+ });
+
+ //Home page
+ app.get('/', (req, res, next)=>{
+ req.brew = {
+ text : welcomeText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Home page Legacy
+ app.get('/legacy', (req, res, next)=>{
+ req.brew = {
+ text : welcomeTextLegacy,
+ renderer : 'legacy',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage (Legacy)',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Legacy/Other Document -> v3 Migration Guide
+ app.get('/migrate', (req, res, next)=>{
+ req.brew = {
+ text : migrateText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'v3 Migration Guide',
+ description : 'A brief guide to converting Legacy documents to the v3 renderer.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Changelog page
+ app.get('/changelog', async (req, res, next)=>{
+ req.brew = {
+ title : 'Changelog',
+ text : changelogText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Changelog',
+ description : 'Development changelog.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //FAQ page
+ app.get('/faq', async (req, res, next)=>{
+ req.brew = {
+ title : 'FAQ',
+ text : faqText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'FAQ',
+ description : 'Frequently Asked Questions'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Source page
+ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+
+ const replaceStrings = { '&': '&', '<': '<', '>': '>' };
+ let text = brew.text;
+ for (const replaceStr in replaceStrings) {
+ text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
+ }
+ text = `${text}`;
+ res.status(200).send(text);
+ });
+
+ //Download brew source page
+ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
+ const prefix = 'HB - ';
+
+ const encodeRFC3986ValueChars = (str)=>{
+ return (
+ encodeURIComponent(str)
+ .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
+ );
+ };
+
+ let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
+ if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
+ res.set({
+ 'Cache-Control' : 'no-cache',
+ 'Content-Type' : 'text/plain',
+ 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
+ });
+ res.status(200).send(brew.text);
+ });
+
+ //Serve brew metadata
+ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
+
+ const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
'published', 'views', 'shareId', 'createdAt', 'updatedAt',
'lastViewed', 'thumbnail', 'tags'
- ];
+ ];
- const metadata = fields.reduce((acc, field)=>{
+ const metadata = fields.reduce((acc, field)=>{
if(brew[field] !== undefined) acc[field] = brew[field];
return acc;
- }, {});
- res.status(200).json(metadata);
-});
+ }, {});
+ res.status(200).json(metadata);
+ });
-//Serve brew styling
-app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
+ //Serve brew styling
+ app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
-//User Page
-app.get('/user/:username', dbCheck, async (req, res, next)=>{
- const ownAccount = req.account && (req.account.username == req.params.username);
+ //User Page
+ app.get('/user/:username', dbCheck, async (req, res, next)=>{
+ const ownAccount = req.account && (req.account.username == req.params.username);
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.params.username}'s Collection`,
- description : 'View my collection of homebrew on the Homebrewery.'
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.params.username}'s Collection`,
+ description : 'View my collection of homebrew on the Homebrewery.'
// type : could be 'profile'?
- };
+ };
- const fields = [
- 'googleId',
- 'title',
- 'pageCount',
- 'description',
- 'authors',
- 'lang',
- 'published',
- 'views',
- 'shareId',
- 'editId',
- 'createdAt',
- 'updatedAt',
- 'lastViewed',
- 'thumbnail',
- 'tags'
- ];
+ const fields = [
+ 'googleId',
+ 'title',
+ 'pageCount',
+ 'description',
+ 'authors',
+ 'lang',
+ 'published',
+ 'views',
+ 'shareId',
+ 'editId',
+ 'createdAt',
+ 'updatedAt',
+ 'lastViewed',
+ 'thumbnail',
+ 'tags'
+ ];
- let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
+ let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
.catch((err)=>{
console.log(err);
});
- brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
+ brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
- if(ownAccount && req?.account?.googleId){
- const auth = await GoogleActions.authCheck(req.account, res);
- let googleBrews = await GoogleActions.listGoogleBrews(auth)
+ if(ownAccount && req?.account?.googleId){
+ const auth = await GoogleActions.authCheck(req.account, res);
+ let googleBrews = await GoogleActions.listGoogleBrews(auth)
.catch((err)=>{
console.error(err);
});
- // If stub matches file from Google, use Google metadata over stub metadata
- if(googleBrews && googleBrews.length > 0) {
- for (const brew of brews.filter((brew)=>brew.googleId)) {
- const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
- if(match !== -1) {
- brew.googleId = googleBrews[match].googleId;
- brew.pageCount = googleBrews[match].pageCount;
- brew.renderer = googleBrews[match].renderer;
- brew.version = googleBrews[match].version;
- brew.webViewLink = googleBrews[match].webViewLink;
- googleBrews.splice(match, 1);
+ // If stub matches file from Google, use Google metadata over stub metadata
+ if(googleBrews && googleBrews.length > 0) {
+ for (const brew of brews.filter((brew)=>brew.googleId)) {
+ const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
+ if(match !== -1) {
+ brew.googleId = googleBrews[match].googleId;
+ brew.pageCount = googleBrews[match].pageCount;
+ brew.renderer = googleBrews[match].renderer;
+ brew.version = googleBrews[match].version;
+ brew.webViewLink = googleBrews[match].webViewLink;
+ googleBrews.splice(match, 1);
+ }
}
+
+ //Remaining unstubbed google brews display current user as author
+ googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
+ brews = _.concat(brews, googleBrews);
}
-
- //Remaining unstubbed google brews display current user as author
- googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
- brews = _.concat(brews, googleBrews);
}
- }
- req.brews = _.map(brews, (brew)=>{
+ req.brews = _.map(brews, (brew)=>{
// Clean up brew data
- brew.title = brew.title?.trim();
- brew.description = brew.description?.trim();
- return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
+ brew.title = brew.title?.trim();
+ brew.description = brew.description?.trim();
+ return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
+ });
+
+ return next();
});
- return next();
-});
+ //Change author name on brews
+ app.put('/api/user/rename', dbCheck, async (req, res)=>{
+ const { username, newUsername } = req.body;
+ const ownAccount = req.account && (req.account.username == newUsername);
-//Change author name on brews
-app.put('/api/user/rename', dbCheck, async (req, res)=>{
- const { username, newUsername } = req.body;
- const ownAccount = req.account && (req.account.username == newUsername);
+ if(!username || !newUsername)
+ return res.status(400).json({ error: 'Username and newUsername are required.' });
+ if(!ownAccount)
+ return res.status(403).json({ error: 'Must be logged in to change your username' });
+ try {
+ const brews = await HomebrewModel.getByUser(username, true, ['authors']);
+ const renamePromises = brews.map(async (brew)=>{
+ const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
+ );
+ return HomebrewModel.updateOne(
+ { _id: brew._id },
+ { $set: { authors: updatedAuthors } }
+ );
+ });
+ await Promise.all(renamePromises);
- if(!username || !newUsername)
- return res.status(400).json({ error: 'Username and newUsername are required.' });
- if(!ownAccount)
- return res.status(403).json({ error: 'Must be logged in to change your username' });
- try {
- const brews = await HomebrewModel.getByUser(username, true, ['authors']);
- const renamePromises = brews.map(async (brew)=>{
- const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
- );
- return HomebrewModel.updateOne(
- { _id: brew._id },
- { $set: { authors: updatedAuthors } }
- );
- });
- await Promise.all(renamePromises);
-
- return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
- } catch (error) {
- console.error('Error renaming brews:', error);
- return res.status(500).json({ error: 'Failed to rename brews.' });
- }
-});
-
-//Edit Page
-app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
- req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
-
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
-
- req.ogMeta = { ...defaultMetaTags,
- title : req.brew.title || 'Untitled Brew',
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- locale : req.brew.lang,
- type : 'article'
- };
-
- sanitizeBrew(req.brew, 'edit');
- splitTextStyleAndMetadata(req.brew);
- res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
- return next();
-}));
-
-//New Page from ID
-app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
- sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- const brew = {
- shareId : req.brew.shareId,
- title : `CLONE - ${req.brew.title}`,
- text : req.brew.text,
- style : req.brew.style,
- renderer : req.brew.renderer,
- theme : req.brew.theme,
- tags : req.brew.tags,
- snippets : req.brew.snippets
- };
- req.brew = _.defaults(brew, DEFAULT_BREW);
-
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'New',
- description : 'Start crafting your homebrew on the Homebrewery!'
- };
-
- return next();
-}));
-
-//New Page
-app.get('/new', asyncHandler(async(req, res, next)=>{
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
-
- req.ogMeta = { ...defaultMetaTags,
- title : 'New',
- description : 'Start crafting your homebrew on the Homebrewery!'
- };
-
- return next();
-}));
-
-//Share Page
-app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
- const { brew } = req;
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- type : 'article'
- };
-
- // increase visitor view count, do not include visits by author(s)
- if(!brew.authors.includes(req.account?.username)){
- if(req.params.id.length > 12 && !brew._id) {
- const googleId = brew.googleId;
- const shareId = brew.shareId;
- await GoogleActions.increaseView(googleId, shareId, 'share', brew)
- .catch((err)=>{next(err);});
- } else {
- await HomebrewModel.increaseView({ shareId: brew.shareId });
+ return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
+ } catch (error) {
+ console.error('Error renaming brews:', error);
+ return res.status(500).json({ error: 'Failed to rename brews.' });
}
- };
+ });
- brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- return next();
-}));
+ //Edit Page
+ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
+ req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
-//Account Page
-app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
- const data = {};
- data.title = 'Account Information Page';
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
- if(!req.account) {
- res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
- const error = new Error('No valid account');
- error.status = 401;
- error.HBErrorCode = '50';
- error.page = data.title;
- return next(error);
- };
+ req.ogMeta = { ...defaultMetaTags,
+ title : req.brew.title || 'Untitled Brew',
+ description : req.brew.description || 'No description.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ locale : req.brew.lang,
+ type : 'article'
+ };
- let auth;
- let googleCount = [];
- if(req.account) {
- if(req.account.googleId) {
- auth = await GoogleActions.authCheck(req.account, res, false);
+ sanitizeBrew(req.brew, 'edit');
+ splitTextStyleAndMetadata(req.brew);
+ res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
+ return next();
+ }));
- googleCount = await GoogleActions.listGoogleBrews(auth)
+ //New Page from ID
+ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ const brew = {
+ shareId : req.brew.shareId,
+ title : `CLONE - ${req.brew.title}`,
+ text : req.brew.text,
+ style : req.brew.style,
+ renderer : req.brew.renderer,
+ theme : req.brew.theme,
+ tags : req.brew.tags,
+ snippets : req.brew.snippets
+ };
+ req.brew = _.defaults(brew, DEFAULT_BREW);
+
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'New',
+ description : 'Start crafting your homebrew on the Homebrewery!'
+ };
+
+ return next();
+ }));
+
+ //New Page
+ app.get('/new', asyncHandler(async(req, res, next)=>{
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'New',
+ description : 'Start crafting your homebrew on the Homebrewery!'
+ };
+
+ return next();
+ }));
+
+ //Share Page
+ app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
+ const { brew } = req;
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
+ description : req.brew.description || 'No description.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ type : 'article'
+ };
+
+ // increase visitor view count, do not include visits by author(s)
+ if(!brew.authors.includes(req.account?.username)){
+ if(req.params.id.length > 12 && !brew._id) {
+ const googleId = brew.googleId;
+ const shareId = brew.shareId;
+ await GoogleActions.increaseView(googleId, shareId, 'share', brew)
+ .catch((err)=>{next(err);});
+ } else {
+ await HomebrewModel.increaseView({ shareId: brew.shareId });
+ }
+ };
+
+ brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ }));
+
+ //Account Page
+ app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
+ const data = {};
+ data.title = 'Account Information Page';
+
+ if(!req.account) {
+ res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
+ const error = new Error('No valid account');
+ error.status = 401;
+ error.HBErrorCode = '50';
+ error.page = data.title;
+ return next(error);
+ };
+
+ let auth;
+ let googleCount = [];
+ if(req.account) {
+ if(req.account.googleId) {
+ auth = await GoogleActions.authCheck(req.account, res, false);
+
+ googleCount = await GoogleActions.listGoogleBrews(auth)
.catch((err)=>{
console.error(err);
});
- }
+ }
- const query = { authors: req.account.username, googleId: { $exists: false } };
- const mongoCount = await HomebrewModel.countDocuments(query)
+ const query = { authors: req.account.username, googleId: { $exists: false } };
+ const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{
console.log(err);
return 0;
});
- data.accountDetails = {
- username : req.account.username,
- issued : req.account.issued,
- googleId : Boolean(req.account.googleId),
- authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
- mongoCount : mongoCount,
- googleCount : googleCount?.length
+ data.accountDetails = {
+ username : req.account.username,
+ issued : req.account.issued,
+ googleId : Boolean(req.account.googleId),
+ authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
+ mongoCount : mongoCount,
+ googleCount : googleCount?.length
+ };
+ }
+
+ req.brew = data;
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : `Account Page`,
+ description : null
};
- }
- req.brew = data;
+ return next();
+ }));
- req.ogMeta = { ...defaultMetaTags,
- title : `Account Page`,
- description : null
- };
-
- return next();
-}));
-
-// Local only
-if(isLocalEnvironment){
+ // Local only
+ if(isLocalEnvironment){
// Login
- app.post('/local/login', (req, res)=>{
- const username = req.body.username;
- if(!username) return;
+ app.post('/local/login', (req, res)=>{
+ const username = req.body.username;
+ if(!username) return;
- const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
- return res.json(payload);
- });
-}
-
-// Add Static Local Paths
-app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
-app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
-
-//Vault Page
-app.get('/vault', asyncHandler(async(req, res, next)=>{
- req.ogMeta = { ...defaultMetaTags,
- title : 'The Vault',
- description : 'Search for Brews'
- };
- return next();
-}));
-
-//Send rendered page
-app.use(asyncHandler(async (req, res, next)=>{
- if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
-
- const page = await renderPage(req, res);
- if(!page) return;
- res.send(page);
-}));
-
-//Render the page
-const renderPage = async (req, res)=>{
- // Create configuration object
- const configuration = {
- local : isLocalEnvironment,
- publicUrl : config.get('publicUrl') ?? '',
- baseUrl : `${req.protocol}://${req.get('host')}`,
- environment : nodeEnv,
- deployment : config.get('heroku_app_name') ?? ''
- };
- const props = {
- version : version,
- url : req.customUrl || req.originalUrl,
- brew : req.brew,
- brews : req.brews,
- googleBrews : req.googleBrews,
- account : req.account,
- config : configuration,
- ogMeta : req.ogMeta,
- userThemes : req.userThemes
- };
- const title = req.brew ? req.brew.title : '';
- const page = await templateFn('homebrew', title, props)
- .catch((err)=>{
- console.log(err);
+ const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
+ return res.json(payload);
});
- return page;
-};
-
-//v=====----- Error-Handling Middleware -----=====v//
-//Format Errors as plain objects so all fields will appear in the string sent
-const formatErrors = (key, value)=>{
- if(value instanceof Error) {
- const error = {};
- Object.getOwnPropertyNames(value).forEach(function (key) {
- error[key] = value[key];
- });
- return error;
}
- return value;
-};
-const getPureError = (error)=>{
- return JSON.parse(JSON.stringify(error, formatErrors));
-};
+ // Add Static Local Paths
+ app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
+ app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
-app.use(async (err, req, res, next)=>{
- err.originalUrl = req.originalUrl;
- console.error(err);
+ //Vault Page
+ app.get('/vault', asyncHandler(async(req, res, next)=>{
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'The Vault',
+ description : 'Search for Brews'
+ };
+ return next();
+ }));
- if(err.originalUrl?.startsWith('/api')) {
+ //Send rendered page
+ app.use(asyncHandler(async (req, res, next)=>{
+ if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
+
+ const page = await renderPage(req, res);
+ if(!page) return;
+ res.send(page);
+ }));
+
+ //Render the page
+ const renderPage = async (req, res)=>{
+
+ // Create configuration object
+ const configuration = {
+ local : isLocalEnvironment,
+ publicUrl : config.get('publicUrl') ?? '',
+ baseUrl : `${req.protocol}://${req.get('host')}`,
+ environment : nodeEnv,
+ deployment : config.get('heroku_app_name') ?? ''
+ };
+ const props = {
+ version : version,
+ url : req.customUrl || req.originalUrl,
+ brew : req.brew,
+ brews : req.brews,
+ googleBrews : req.googleBrews,
+ account : req.account,
+ config : configuration,
+ ogMeta : req.ogMeta,
+ userThemes : req.userThemes
+ };
+
+ const ogTags = [];
+ const ogMeta = req.ogMeta ?? {};
+ Object.entries(ogMeta).forEach(([key, value])=>{
+ if(!value) return;
+ const tag = ``;
+ ogTags.push(tag);
+ });
+ const ogMetaTags = ogTags.join('\n');
+
+ const htmlPath = isProd ? path.resolve('build', 'index.html') : path.resolve('index.html');
+ let html = fs.readFileSync(htmlPath, 'utf-8');
+
+ if(!isProd && vite?.transformIndexHtml) {
+ html = await vite.transformIndexHtml(req.originalUrl, html);
+ }
+
+ html = html.replace(
+ '',
+ `\n\n${ogMetaTags}`
+ );
+
+ return html;
+ };
+
+ //v=====----- Error-Handling Middleware -----=====v//
+ //Format Errors as plain objects so all fields will appear in the string sent
+ const formatErrors = (key, value)=>{
+ if(value instanceof Error) {
+ const error = {};
+ Object.getOwnPropertyNames(value).forEach(function (key) {
+ error[key] = value[key];
+ });
+ return error;
+ }
+ return value;
+ };
+
+ const getPureError = (error)=>{
+ return JSON.parse(JSON.stringify(error, formatErrors));
+ };
+
+ app.use(async (err, req, res, next)=>{
+ err.originalUrl = req.originalUrl;
+ console.error(err);
+
+ if(err.originalUrl?.startsWith('/api')) {
// console.log('API error');
- res.status(err.status || err.response?.status || 500).send(err);
- return;
- }
+ res.status(err.status || err.response?.status || 500).send(err);
+ return;
+ }
- // console.log('non-API error');
- const status = err.status || err.code || 500;
+ // console.log('non-API error');
+ const status = err.status || err.code || 500;
- req.ogMeta = { ...defaultMetaTags,
- title : 'Error Page',
- description : 'Something went wrong!'
- };
- req.brew = {
- ...err,
- title : 'Error - Something went wrong!',
- text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
- status : status,
- HBErrorCode : err.HBErrorCode ?? '00',
- pureError : getPureError(err)
- };
- req.customUrl= '/error';
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Error Page',
+ description : 'Something went wrong!'
+ };
+ req.brew = {
+ ...err,
+ title : 'Error - Something went wrong!',
+ text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
+ status : status,
+ HBErrorCode : err.HBErrorCode ?? '00',
+ pureError : getPureError(err)
+ };
+ req.customUrl= '/error';
- const page = await renderPage(req, res);
- if(!page) return;
- res.send(page);
-});
+ const page = await renderPage(req, res);
+ if(!page) return;
+ res.send(page);
+ });
-app.use((req, res)=>{
- if(!res.headersSent) {
- console.error('Headers have not been sent, responding with a server error.', req.url);
- res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
- }
-});
-//^=====--------------------------------------=====^//
+ app.use((req, res)=>{
+ if(!res.headersSent) {
+ console.error('Headers have not been sent, responding with a server error.', req.url);
+ res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
+ }
+ });
+ //^=====--------------------------------------=====^//
-export default app;
+ return app;
+}
diff --git a/shared/helpers.js b/shared/helpers.js
index adf5b889a..8177aa7a9 100644
--- a/shared/helpers.js
+++ b/shared/helpers.js
@@ -19,7 +19,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
userSnippets.push({
name : snippetName,
icon : '',
- gen : snipSplit[snips + 1],
+ gen : snipSplit[snips + 1].replace(/\n$/, ''),
});
}
}
@@ -44,7 +44,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
if(snippetName.length != 0) {
const subSnip = {
name : snippetName,
- gen : snipSplit[snips + 1],
+ gen : snipSplit[snips + 1].replace(/\n$/, ''),
};
// if(full) subSnip.icon = '';
userSnippets.push(subSnip);
diff --git a/shared/naturalcrit/styles/core.less b/shared/naturalcrit/styles/core.less
index 02db5db18..3ef75144d 100644
--- a/shared/naturalcrit/styles/core.less
+++ b/shared/naturalcrit/styles/core.less
@@ -1,16 +1,16 @@
-@import 'naturalcrit/styles/reset.less';
-//@import 'naturalcrit/styles/elements.less';
-@import 'naturalcrit/styles/animations.less';
-@import 'naturalcrit/styles/colors.less';
-@import 'naturalcrit/styles/tooltip.less';
+@import './reset.less';
+//@import './elements.less';
+@import './animations.less';
+@import './colors.less';
+@import './tooltip.less';
@font-face {
font-family : 'CodeLight';
- src : data-uri('naturalcrit/styles/CODE Light.otf') format('opentype');
+ src : url('./CODE Light.otf') format('opentype');
}
@font-face {
font-family : 'CodeBold';
- src : data-uri('naturalcrit/styles/CODE Bold.otf') format('opentype');
+ src : url('./CODE Bold.otf') format('opentype');
}
html,body, #reactRoot {
height : 100vh;
diff --git a/tests/html/safeHTML.test.js b/tests/html/safeHTML.test.js
index 208101f9a..0b5b4c169 100644
--- a/tests/html/safeHTML.test.js
+++ b/tests/html/safeHTML.test.js
@@ -1,6 +1,6 @@
import globalJsdom from 'jsdom-global';
globalJsdom();
-import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
+import safeHTML from '../../client/homebrew/brewRenderer/safeHTML';
test('Exit if no document', function() {
const doc = document;
diff --git a/tests/markdown/basic.test.js b/tests/markdown/basic.test.js
index ddceb9197..f2405d0d8 100644
--- a/tests/markdown/basic.test.js
+++ b/tests/markdown/basic.test.js
@@ -1,6 +1,6 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '*Bold text*
';
diff --git a/tests/markdown/definition-lists.test.js b/tests/markdown/definition-lists.test.js
index 12499d559..35ad12ef7 100644
--- a/tests/markdown/definition-lists.test.js
+++ b/tests/markdown/definition-lists.test.js
@@ -1,6 +1,6 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
describe('Inline Definition Lists', ()=>{
test('No Term 1 Definition', function() {
diff --git a/tests/markdown/emojis.test.js b/tests/markdown/emojis.test.js
index 5f0e102c9..072de10f9 100644
--- a/tests/markdown/emojis.test.js
+++ b/tests/markdown/emojis.test.js
@@ -1,4 +1,4 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
import dedent from 'dedent';
// Marked.js adds line returns after closing tags on some default tokens.
diff --git a/tests/markdown/hard-breaks.test.js b/tests/markdown/hard-breaks.test.js
index 068b2053a..1f48f8f1e 100644
--- a/tests/markdown/hard-breaks.test.js
+++ b/tests/markdown/hard-breaks.test.js
@@ -1,6 +1,6 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
describe('Hard Breaks', ()=>{
test('Single Break', function() {
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 8562ae412..95ca2f58d 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import dedent from 'dedent';
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
diff --git a/tests/markdown/non-breaking-spaces.test.js b/tests/markdown/non-breaking-spaces.test.js
index 9da59b047..731d0546e 100644
--- a/tests/markdown/non-breaking-spaces.test.js
+++ b/tests/markdown/non-breaking-spaces.test.js
@@ -1,6 +1,6 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
describe('Non-Breaking Spaces Interactions', ()=>{
test('I am actually a single-line definition list!', function() {
diff --git a/tests/markdown/paragraph-justification.test.js b/tests/markdown/paragraph-justification.test.js
index f5a5b12ab..6ce454623 100644
--- a/tests/markdown/paragraph-justification.test.js
+++ b/tests/markdown/paragraph-justification.test.js
@@ -1,6 +1,6 @@
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
describe('Justification', ()=>{
test('Left Justify', function() {
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index 49cd333c8..884553703 100644
--- a/tests/markdown/variables.test.js
+++ b/tests/markdown/variables.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import dedent from 'dedent';
-import Markdown from 'markdown.js';
+import Markdown from '../../shared/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
diff --git a/tests/routes/static-pages.test.js b/tests/routes/static-pages.test.js
index ebfa48dd4..2741b8d70 100644
--- a/tests/routes/static-pages.test.js
+++ b/tests/routes/static-pages.test.js
@@ -1,27 +1,32 @@
import supertest from 'supertest';
-import HBApp from 'app.js';
+import createApp from '../../server/app.js';
-// Mimic https responses to avoid being redirected all the time
-const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
+let app;
+let request;
+
+beforeAll(async ()=>{
+ app = await createApp();
+ request = supertest.agent(app).set('X-Forwarded-Proto', 'https');
+});
describe('Tests for static pages', ()=>{
- it('Home page works', ()=>{
- return app.get('/').expect(200);
+ it('Home page works', async ()=>{
+ await request.get('/').expect(200);
});
- it('Home page legacy works', ()=>{
- return app.get('/legacy').expect(200);
+ it('Home page legacy works', async ()=>{
+ await request.get('/legacy').expect(200);
});
- it('Changelog page works', ()=>{
- return app.get('/changelog').expect(200);
+ it('Changelog page works', async ()=>{
+ await request.get('/changelog').expect(200);
});
- it('FAQ page works', ()=>{
- return app.get('/faq').expect(200);
+ it('FAQ page works', async ()=>{
+ await request.get('/faq').expect(200);
});
- it('robots.txt works', ()=>{
- return app.get('/robots.txt').expect(200);
+ it('robots.txt works', async ()=>{
+ await request.get('/robots.txt').expect(200);
});
-});
+});
\ No newline at end of file
diff --git a/themes/Legacy/5ePHB/snippets.js b/themes/Legacy/5ePHB/snippets.js
index a6a9dbe88..5bf74b4ca 100644
--- a/themes/Legacy/5ePHB/snippets.js
+++ b/themes/Legacy/5ePHB/snippets.js
@@ -1,12 +1,12 @@
/* eslint-disable max-lines */
-import MagicGen from './snippets/magic.gen.js';
+import MagicGen from './snippets/magic.gen.js';
import ClassTableGen from './snippets/classtable.gen.js';
import MonsterBlockGen from './snippets/monsterblock.gen.js';
import ClassFeatureGen from './snippets/classfeature.gen.js';
import CoverPageGen from './snippets/coverpage.gen.js';
import TableOfContentsGen from './snippets/tableOfContents.gen.js';
-import dedent from 'dedent';
+import dedent from 'dedent';
export default [
diff --git a/themes/Legacy/5ePHB/snippets/classfeature.gen.js b/themes/Legacy/5ePHB/snippets/classfeature.gen.js
index b92d527f9..70085d647 100644
--- a/themes/Legacy/5ePHB/snippets/classfeature.gen.js
+++ b/themes/Legacy/5ePHB/snippets/classfeature.gen.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
-export default function(classname){
+function classFeatureGen(classname) {
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
@@ -49,4 +49,6 @@ export default function(classname){
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
'\n\n\n'
].join('\n');
-};
+}
+
+export default classFeatureGen;
diff --git a/themes/Legacy/5ePHB/snippets/coverpage.gen.js b/themes/Legacy/5ePHB/snippets/coverpage.gen.js
index 0cd0e50d5..c134930cf 100644
--- a/themes/Legacy/5ePHB/snippets/coverpage.gen.js
+++ b/themes/Legacy/5ePHB/snippets/coverpage.gen.js
@@ -98,8 +98,8 @@ const subtitles = [
];
-export default ()=>{
- return `
@@ -114,4 +114,6 @@ export default ()=>{
\\page`;
-};
\ No newline at end of file
+}
+
+export default coverPageGen;
\ No newline at end of file
diff --git a/themes/Legacy/5ePHB/snippets/fullclass.gen.js b/themes/Legacy/5ePHB/snippets/fullclass.gen.js
index 50d1ef578..33751574d 100644
--- a/themes/Legacy/5ePHB/snippets/fullclass.gen.js
+++ b/themes/Legacy/5ePHB/snippets/fullclass.gen.js
@@ -4,7 +4,7 @@ import ClassFeatureGen from './classfeature.gen.js';
import ClassTableGen from './classtable.gen.js';
-export default function(){
+function fullClassGen(){
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
@@ -40,4 +40,6 @@ export default function(){
].join('\n')}\n\n\n`;
-};
\ No newline at end of file
+}
+
+export default fullClassGen;
\ No newline at end of file
diff --git a/themes/Legacy/5ePHB/snippets/tableOfContents.gen.js b/themes/Legacy/5ePHB/snippets/tableOfContents.gen.js
index b37cca9ee..7a7f16ede 100644
--- a/themes/Legacy/5ePHB/snippets/tableOfContents.gen.js
+++ b/themes/Legacy/5ePHB/snippets/tableOfContents.gen.js
@@ -47,7 +47,8 @@ const getTOC = (pages)=>{
return res;
};
-export default function(props){
+function tableOfContentsGen(props){
+
const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
@@ -69,4 +70,6 @@ export default function(props){
##### Table Of Contents
${markdown}
\n`;
-};
\ No newline at end of file
+}
+
+export default tableOfContentsGen;
\ No newline at end of file
diff --git a/themes/V3/Blank/snippets/footer.gen.js b/themes/V3/Blank/snippets/footer.gen.js
index c97cf4cb3..596592629 100644
--- a/themes/V3/Blank/snippets/footer.gen.js
+++ b/themes/V3/Blank/snippets/footer.gen.js
@@ -1,4 +1,4 @@
-import Markdown from '../../../../shared/markdown.js';
+import Markdown from '@shared/markdown.js';
export default {
createFooterFunc : function(headerSize=1){
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 000000000..6025f8736
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,38 @@
+// vite.config.js
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import { generateAssetsPlugin } from "./vitePlugins/generateAssetsPlugin.js";
+
+export default defineConfig({
+ plugins: [react(), generateAssetsPlugin()],
+ resolve: {
+ alias: {
+ "@vitreum": path.resolve(__dirname, "./vitreum"),
+ "@shared": path.resolve(__dirname, "./shared"),
+ "@sharedStyles": path.resolve(__dirname, "./shared/naturalcrit/styles"),
+ "@navbar": path.resolve(__dirname, "./client/homebrew/navbar"),
+ "@themes": path.resolve(__dirname, "./themes"),
+ },
+ },
+ build: {
+ outDir: "build",
+ emptyOutDir: false,
+ rollupOptions: {
+ output: {
+ entryFileNames: "[name]/bundle.js",
+ chunkFileNames: "[name]/[name]-[hash].js",
+ assetFileNames: "[name]/[name].[ext]",
+ },
+ },
+ },
+ define: {
+ global: "window.__INITIAL_PROPS__",
+ },
+ server: {
+ port: 8000,
+ fs: {
+ allow: ["."],
+ },
+ },
+});
diff --git a/vitePlugins/generateAssetsPlugin.js b/vitePlugins/generateAssetsPlugin.js
new file mode 100644
index 000000000..eaf74509b
--- /dev/null
+++ b/vitePlugins/generateAssetsPlugin.js
@@ -0,0 +1,79 @@
+// vite-plugins/generateAssetsPlugin.js
+import fs from "fs-extra";
+import path from "path";
+import less from "less";
+
+export function generateAssetsPlugin(isDev = false) {
+ return {
+ name: "generate-assets",
+ async buildStart() {
+ const buildDir = path.resolve(process.cwd(), "build");
+
+ // Copy favicon
+ await fs.copy("./client/homebrew/favicon.ico", `${buildDir}/assets/favicon.ico`);
+
+ // Copy shared styles/fonts
+ const assets = fs.readdirSync("./shared/naturalcrit/styles");
+ for (const file of assets) {
+ await fs.copy(`./shared/naturalcrit/styles/${file}`, `${buildDir}/fonts/${file}`);
+ }
+
+ // Compile Legacy themes
+ const themes = { Legacy: {}, V3: {} };
+ const legacyDirs = fs.readdirSync("./themes/Legacy");
+ for (const dir of legacyDirs) {
+ const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`, "utf-8"));
+ themeData.path = dir;
+ themes.Legacy[dir] = themeData;
+
+ const src = `./themes/Legacy/${dir}/style.less`;
+ const outputDir = `${buildDir}/themes/Legacy/${dir}/style.css`;
+ const lessOutput = await less.render(fs.readFileSync(src, "utf-8"), { compress: !isDev });
+ await fs.outputFile(outputDir, lessOutput.css);
+ }
+
+ // Compile V3 themes
+ const v3Dirs = fs.readdirSync("./themes/V3");
+ for (const dir of v3Dirs) {
+ const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`, "utf-8"));
+ themeData.path = dir;
+ themes.V3[dir] = themeData;
+
+ await fs.copy(
+ `./themes/V3/${dir}/dropdownTexture.png`,
+ `${buildDir}/themes/V3/${dir}/dropdownTexture.png`,
+ );
+ await fs.copy(
+ `./themes/V3/${dir}/dropdownPreview.png`,
+ `${buildDir}/themes/V3/${dir}/dropdownPreview.png`,
+ );
+
+ const src = `./themes/V3/${dir}/style.less`;
+ const outputDir = `${buildDir}/themes/V3/${dir}/style.css`;
+ const lessOutput = await less.render(fs.readFileSync(src, "utf-8"), { compress: !isDev });
+ await fs.outputFile(outputDir, lessOutput.css);
+ }
+
+ // Write themes.json
+ await fs.outputFile("./themes/themes.json", JSON.stringify(themes, null, 2));
+
+ // Copy fonts/assets/icons
+ await fs.copy("./themes/fonts", `${buildDir}/fonts`);
+ await fs.copy("./themes/assets", `${buildDir}/assets`);
+ await fs.copy("./client/icons", `${buildDir}/icons`);
+
+ // Compile CodeMirror editor themes
+ const editorThemesBuildDir = `${buildDir}/homebrew/cm-themes`;
+ await fs.copy("./node_modules/codemirror/theme", editorThemesBuildDir);
+ await fs.copy("./themes/codeMirror/customThemes", editorThemesBuildDir);
+
+ const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
+ await fs.outputFile(`${buildDir}/homebrew/codeMirror/editorThemes.json`,
+ JSON.stringify(["default", ...editorThemeFiles.map((f) => f.slice(0, -4))], null, 2),
+ );
+
+ // Copy remaining CodeMirror assets
+ await fs.copy("./themes/codeMirror", `${buildDir}/homebrew/codeMirror`);
+ },
+ };
+}
diff --git a/vitreum/headtags.js b/vitreum/headtags.js
new file mode 100644
index 000000000..54cdf5922
--- /dev/null
+++ b/vitreum/headtags.js
@@ -0,0 +1,92 @@
+import React, { useEffect } from "react";
+
+//old vitreum file, still imported in some pages
+
+const injectTag = (tag, props, children) => {
+ const injectNode = document.createElement(tag);
+ Object.entries(props).forEach(([key, val]) => injectNode[key] = val);
+ if (children) injectNode.appendChild(document.createTextNode(children));
+ document.getElementsByTagName('head')[0].appendChild(injectNode);
+};
+
+const obj2props = (obj) =>
+ Object.entries(obj)
+ .map(([k, v]) => `${k}="${v}"`)
+ .join(" ");
+const toStr = (chld) => (Array.isArray(chld) ? chld.join("") : chld);
+const onServer = typeof window === "undefined";
+
+let NamedTags = {};
+let UnnamedTags = [];
+
+export const HeadComponents = {
+ Title({ children }) {
+ if (onServer) NamedTags.title = `${toStr(children)}`;
+ useEffect(() => {
+ document.title = toStr(children);
+ }, [children]);
+ return null;
+ },
+ Favicon({ type = "image/png", href = "", rel = "icon", id = "favicon" }) {
+ if (onServer) NamedTags.favicon = ``;
+ useEffect(() => {
+ document.getElementById(id).href = href;
+ }, [id, href]);
+ return null;
+ },
+ Description({ children }) {
+ if (onServer) NamedTags.description = ``;
+ return null;
+ },
+ Noscript({ children }) {
+ if (onServer) UnnamedTags.push(``);
+ return null;
+ },
+ Script({ children = [], ...props }) {
+ if (onServer) {
+ UnnamedTags.push(
+ children.length
+ ? ``
+ : ``,
+ );
+ }
+ return null;
+ },
+ Meta(props) {
+ let tag = ``;
+ props.property || props.name ? (NamedTags[props.property || props.name] = tag) : UnnamedTags.push(tag);
+ useEffect(() => {
+ document
+ .getElementsByTagName("head")[0]
+ .insertAdjacentHTML("beforeend", Object.values(NamedTags).join("\n"));
+ }, [NamedTags]);
+ return null;
+ },
+ Style({ children, type = "text/css" }) {
+ if (onServer) UnnamedTags.push(``);
+ return null;
+ },
+};
+
+export const Inject = ({ tag, children, ...props }) => {
+ useEffect(() => {
+ injectTag(tag, props, children);
+ }, []);
+ return null;
+};
+
+export const generate = () => Object.values(NamedTags).concat(UnnamedTags).join("\n");
+
+export const flush = () => {
+ NamedTags = {};
+ UnnamedTags = [];
+};
+
+export const Meta = HeadComponents.Meta;
+
+export default {
+ Inject,
+ ...HeadComponents,
+ generate,
+ flush,
+};