Merge pull request #7 from LordMathis/feat/vite

Feat/vite
This commit is contained in:
2024-10-28 21:51:12 +01:00
committed by GitHub
35 changed files with 1451 additions and 6246 deletions

3
.gitignore vendored
View File

@@ -129,6 +129,9 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Vite
.vite
##### Go ##### ##### Go #####

View File

@@ -4,8 +4,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -32,20 +30,16 @@ func main() {
} }
}() }()
// Workdir // Initialize filesystem
workdir := os.Getenv("NOVAMD_WORKDIR") workdir := os.Getenv("NOVAMD_WORKDIR")
if workdir == "" { if workdir == "" {
workdir = "./data" workdir = "./data"
} }
fs := filesystem.New(workdir) fs := filesystem.New(workdir)
// User service // Initialize user service
userService := user.NewUserService(database, fs) userService := user.NewUserService(database, fs)
if _, err := userService.SetupAdminUser(); err != nil {
// Admin user
_, err = userService.SetupAdminUser()
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -54,44 +48,26 @@ func main() {
r.Use(middleware.Logger) r.Use(middleware.Logger)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
// Set up API routes // API routes
r.Route("/api/v1", func(r chi.Router) { r.Route("/api/v1", func(r chi.Router) {
api.SetupRoutes(r, database, fs) api.SetupRoutes(r, database, fs)
}) })
// Set up static file server with path validation // Static file serving
staticPath := os.Getenv("NOVAMD_STATIC_PATH") staticPath := os.Getenv("NOVAMD_STATIC_PATH")
if staticPath == "" { if staticPath == "" {
staticPath = "../frontend/dist" staticPath = "../frontend/dist"
} }
fileServer := http.FileServer(http.Dir(staticPath))
r.Get(
"/*",
func(w http.ResponseWriter, r *http.Request) {
requestedPath := r.URL.Path
fullPath := filepath.Join(staticPath, requestedPath) // Handle all other routes with static file server
cleanPath := filepath.Clean(fullPath) r.Get("/*", api.NewStaticHandler(staticPath).ServeHTTP)
if !strings.HasPrefix(cleanPath, staticPath) {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
_, err = os.Stat(cleanPath)
if os.IsNotExist(err) {
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
return
}
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
},
)
// Start server // Start server
port := os.Getenv("NOVAMD_PORT") port := os.Getenv("NOVAMD_PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"
} }
log.Printf("Server starting on port %s", port) log.Printf("Server starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, r)) log.Fatal(http.ListenAndServe(":"+port, r))
} }

View File

@@ -0,0 +1,70 @@
package api
import (
"net/http"
"os"
"path/filepath"
"strings"
)
// StaticHandler serves static files with support for SPA routing and pre-compressed files
type StaticHandler struct {
staticPath string
}
func NewStaticHandler(staticPath string) *StaticHandler {
return &StaticHandler{
staticPath: staticPath,
}
}
func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the requested path
requestedPath := r.URL.Path
fullPath := filepath.Join(h.staticPath, requestedPath)
cleanPath := filepath.Clean(fullPath)
// Security check to prevent directory traversal
if !strings.HasPrefix(cleanPath, h.staticPath) {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
// Set cache headers for assets
if strings.HasPrefix(requestedPath, "/assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
}
// Check if file exists (not counting .gz files)
stat, err := os.Stat(cleanPath)
if err != nil || stat.IsDir() {
// Serve index.html for SPA routing
indexPath := filepath.Join(h.staticPath, "index.html")
http.ServeFile(w, r, indexPath)
return
}
// Check for pre-compressed version
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gzPath := cleanPath + ".gz"
if _, err := os.Stat(gzPath); err == nil {
w.Header().Set("Content-Encoding", "gzip")
// Set proper content type based on original file
switch filepath.Ext(cleanPath) {
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".html":
w.Header().Set("Content-Type", "text/html")
}
http.ServeFile(w, r, gzPath)
return
}
}
// Serve original file
http.ServeFile(w, r, cleanPath)
}

View File

@@ -1,10 +0,0 @@
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }]
],
"plugins": [
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-runtime"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,11 @@
"name": "novamd-frontend", "name": "novamd-frontend",
"version": "0.1.0", "version": "0.1.0",
"description": "Yet another markdown editor", "description": "Yet another markdown editor",
"main": "index.js", "type": "module",
"scripts": { "scripts": {
"start": "webpack serve --mode development --open", "start": "vite",
"build": "webpack --mode production" "build": "vite build",
"preview": "vite preview"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -43,11 +44,16 @@
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-math": "^6.0.0" "remark-math": "^6.0.0"
}, },
"eslintConfig": { "devDependencies": {
"extends": [ "@types/react": "^18.2.67",
"react-app", "@types/react-dom": "^18.2.22",
"react-app/jest" "@vitejs/plugin-react": "^4.2.1",
] "postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"sass": "^1.80.4",
"vite": "^5.4.10",
"vite-plugin-compression2": "^1.3.0"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -60,26 +66,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@babel/core": "^7.25.7",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-class-properties": "^7.25.7",
"@babel/plugin-transform-runtime": "^7.25.7",
"@babel/preset-env": "^7.25.7",
"@babel/preset-react": "^7.25.7",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.0",
"postcss": "^8.4.47",
"postcss-loader": "^8.1.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"sass": "^1.79.4",
"sass-loader": "^16.0.2",
"style-loader": "^4.0.0",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
} }
} }

View File

@@ -1,14 +0,0 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};

View File

@@ -1,12 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NovaMD</title> <title>NovaMD</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.jsx"></script>
</body> </body>
</html> </html>

136
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,136 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import postcssPresetMantine from 'postcss-preset-mantine';
import postcssSimpleVars from 'postcss-simple-vars';
import { compression } from 'vite-plugin-compression2';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
plugins: [
react({
include: ['**/*.jsx', '**/*.js'],
}),
compression(),
],
root: 'src',
publicDir: '../public',
build: {
outDir: '../dist',
emptyOutDir: true,
assetsDir: 'assets',
sourcemap: mode === 'development',
rollupOptions: {
input: {
main: path.resolve(__dirname, 'src/index.html'),
},
output: {
manualChunks: {
// React core libraries
'react-core': ['react', 'react-dom'],
// Mantine UI components and related
mantine: [
'@mantine/core',
'@mantine/hooks',
'@mantine/modals',
'@mantine/notifications',
],
// Editor related packages
editor: [
'codemirror',
'@codemirror/commands',
'@codemirror/lang-markdown',
'@codemirror/state',
'@codemirror/theme-one-dark',
'@codemirror/view',
],
// Markdown processing
markdown: [
'react-markdown',
'react-syntax-highlighter',
'rehype-katex',
'remark-math',
'katex',
],
// Icons and utilities
utils: [
'@tabler/icons-react',
'@react-hook/resize-observer',
'react-arborist',
],
},
// Optimize chunk naming for better caching
chunkFileNames: (chunkInfo) => {
const name = chunkInfo.name;
if (name === 'react-core') return 'assets/react.[hash].js';
if (name === 'mantine') return 'assets/mantine.[hash].js';
if (name === 'editor') return 'assets/editor.[hash].js';
if (name === 'markdown') return 'assets/markdown.[hash].js';
if (name === 'utils') return 'assets/utils.[hash].js';
return 'assets/[name].[hash].js';
},
// Optimize asset naming
assetFileNames: 'assets/[name].[hash][extname]',
},
},
},
server: {
port: 3000,
open: true,
},
define: {
'window.API_BASE_URL': JSON.stringify(
mode === 'production' ? '/api/v1' : 'http://localhost:8080/api/v1'
),
},
css: {
preprocessorOptions: {
scss: {
api: 'modern',
},
},
postcss: {
plugins: [
postcssPresetMantine(),
postcssSimpleVars({
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
}),
],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
extensions: ['.js', '.jsx', '.json'],
},
// Add performance optimization options
optimizeDeps: {
include: [
'react',
'react-dom',
'@mantine/core',
'@mantine/hooks',
'codemirror',
'react-markdown',
],
},
}));

View File

@@ -1,49 +0,0 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new webpack.DefinePlugin({
'window.API_BASE_URL': JSON.stringify(
isProduction ? '/api/v1' : 'http://localhost:8080/api/v1'
),
}),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
open: true,
},
};
};