Merge pull request #20 from notclickable-jordan/static

Switching to an entirely static site
This commit is contained in:
Jordan Roher 2023-12-18 13:48:14 -08:00 committed by GitHub
commit 86f53bc546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 397 additions and 2911 deletions

6
.gitignore vendored
View File

@ -1,4 +1,8 @@
node_modules
.DS_Store
dist
dist-ssr
src/*.js
src/**/*.js
public/index.html
public/main.css
public/css

16
default.conf Normal file
View File

@ -0,0 +1,16 @@
server {
listen 4173;
root /app/public;
location /app {
try_files $uri $uri/ =404;
# Cache settings for static content
expires 7d; # Cache static content for 7 days
add_header Cache-Control "public, max-age=604800, immutable";
}
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
}

View File

@ -1,23 +1,8 @@
# Build site using Node JS
FROM node:21-slim
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y chromium fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_SKIP_DOWNLOAD true
# Install puppeteer so it's available in the container.
RUN npm init -y && \
npm i -g puppeteer
# Install nginx
RUN apt-get update && apt-get install -y nginx
ARG BUILD_DATE
@ -35,6 +20,9 @@ RUN npm i
COPY . .
# Copy the nginx config to the correct folder
COPY default.conf /etc/nginx/conf.d/default.conf
ENV NODE_ENV production
ENV TITLE "My Website"
@ -53,4 +41,4 @@ EXPOSE 4173
RUN chmod +x /app/docker-entrypoint.sh
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["npm", "run", "start"]
CMD ["sh", "-c", "npm run build && nginx -g 'daemon off;'"]

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>My Website</title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="./main.css" crossorigin="" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2568
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,35 +5,21 @@
"repository": {
"url": "https://github.com/notclickable-jordan/starbase-80/"
},
"version": "1.3.0",
"type": "module",
"version": "1.4.0",
"type": "commonjs",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"start": "node start.js",
"html": "html-minifier --remove-comments --collapse-whitespace --input-dir ./dist --output-dir ./dist --file-ext html",
"replace": "npm run replace:script",
"replace:script": "replace-in-file --configFile replace-script.cjs"
"build": "tsc && node ./dist/index.js && npm run html && npm run tailwind && npm run css-cache-break",
"html": "html-minifier --remove-comments --collapse-whitespace --input-dir ./public --output-dir ./public --file-ext html",
"tailwind": "npx tailwindcss -i ./src/tailwind.css -o ./public/main.css",
"css-cache-break": "node ./dist/css-cache-break.js"
},
"dependencies": {
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"@types/node": "^20.10.5",
"clean-css": "^5.3.3",
"html-minifier": "^4.0.0",
"js-yaml": "^4.1.0",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"puppeteer": "^21.6.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"replace-in-file": "^7.0.2",
"tailwindcss": "^3.3.6",
"typescript": "^5.3.3",
"vite": "^5.0.8"
"typescript": "^5.3.3"
},
"prettier": {
"arrowParens": "avoid",
@ -43,5 +29,8 @@
"tabWidth": 4,
"trailingComma": "es5",
"useTabs": true
},
"devDependencies": {
"@types/clean-css": "^4.2.11"
}
}

View File

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -32,6 +32,11 @@ Inspired by [Ben Phelps' Homepage](https://gethomepage.dev/) and [Umbrel](https:
# Change history
## 1.4.0
- Rewrote the entire application to not use React. Now it's just a Node application that emits static HTML.
- Removed lots of packages
## 1.3.0
- Removed all JavaScript as part of the build step. The image will be slightly larger and take longer to start up and shut down, but the page will be even lighter.

View File

@ -1,5 +0,0 @@
module.exports = {
files: "./dist/index.html",
from: /<script[^>]+><\/script>/gi,
to: "",
};

34
src/components/anchor.ts Normal file
View File

@ -0,0 +1,34 @@
import { is } from "../shared/is";
import { NEWWINDOW } from "../variables";
interface IProps {
uri: string;
title?: string;
className?: string;
children?: any;
newWindow?: boolean;
}
export const Anchor = function (props: IProps) {
const { uri, children, title, className, newWindow } = props;
let newWindowLocal = NEWWINDOW;
if (!is.null(newWindow)) {
newWindowLocal = newWindow as boolean;
}
if (newWindowLocal) {
return `
<a href="${uri}" target="_blank" rel="noreffer" title="${title || ""}" class="${className || ""}">
${children}
</a>
`;
}
return `
<a href="${uri}" title="${title || ""}" class="${className || ""}">
${children}
</a>
`;
};

View File

@ -1,33 +0,0 @@
import React from "react";
import { is } from "../shared/is";
import { NEWWINDOW } from "../variables";
interface IProps {
uri: string;
title?: string;
className?: string;
children?: React.ReactNode;
newWindow?: boolean;
}
export const Anchor: React.FunctionComponent<IProps> = ({ uri, children, title, className, newWindow }) => {
let newWindowLocal = NEWWINDOW;
if (!is.null(newWindow)) {
newWindowLocal = newWindow as boolean;
}
if (newWindowLocal) {
return (
<a href={uri} target="_blank" rel="noreffer" title={title} className={className}>
{children}
</a>
);
}
return (
<a href={uri} title={title} className={className}>
{children}
</a>
);
};

16
src/components/header.ts Normal file
View File

@ -0,0 +1,16 @@
import { is } from "../shared/is";
interface IProps {
title?: string;
icon?: string;
}
export const Header = function (props: IProps) {
const { icon, title } = props;
return `
<div class="p-2 xl:p-4 flex flex-nowrap justify-center items-center gap-2 xl:flex-wrap">
${!is.null(icon) && `<img src="${icon}" alt="${title || ""}" class="inline-block w-16 h-16" />`}
<h1>${title}</h1>
</div>
`;
};

View File

@ -1,16 +0,0 @@
import React from "react";
import { is } from "../shared/is";
interface IProps {
title?: string;
icon?: string;
}
export const Header: React.FunctionComponent<IProps> = ({ icon, title }) => {
return (
<div className="p-2 xl:p-4 flex flex-nowrap justify-center items-center gap-2 xl:flex-wrap">
{!is.null(icon) && <img src={icon} alt={title} className="inline-block w-16 h-16" />}
<h1>{title}</h1>
</div>
);
};

View File

@ -1,4 +1,3 @@
import React from "react";
import { is } from "../shared/is";
import { IconAspect } from "../shared/types";
import { Anchor } from "./anchor";
@ -36,67 +35,54 @@ interface IProps {
newWindow?: boolean;
}
export const Icon: React.FunctionComponent<IProps> = ({
name,
uri,
icon,
index,
iconBG,
iconBubble,
iconColor,
iconAspect,
newWindow,
}) => {
export const Icon = function (props: IProps): string {
const { name, uri, icon, index, iconBG, iconBubble, iconColor, iconAspect, newWindow } = props;
if (is.null(icon)) {
if (!is.null(uri)) {
return (
<Anchor uri={uri as string} title={name} newWindow={newWindow}>
<IconBlank index={index} />
</Anchor>
);
return Anchor({ uri: uri as string, title: name, newWindow, children: IconBlank({ index }) });
}
return <IconBlank index={index} />;
return IconBlank({ index });
}
if (!is.null(uri)) {
return (
<Anchor uri={uri as string} title={name} newWindow={newWindow} className="self-center">
<IconBase
icon={icon as string}
iconBG={iconBG}
iconColor={iconColor}
iconBubble={iconBubble}
iconAspect={iconAspect}
/>
</Anchor>
);
return Anchor({
uri: uri as string,
title: name,
newWindow,
className: "self-center",
children: IconBase({
icon: icon as string,
iconBG,
iconColor,
iconBubble,
iconAspect,
}),
});
}
return (
<IconBase
icon={icon as string}
iconBG={iconBG}
iconColor={iconColor}
iconBubble={iconBubble}
iconAspect={iconAspect}
/>
);
return IconBase({
icon: icon as string,
iconBG,
iconColor,
iconBubble,
iconAspect,
});
};
interface IIconBlankProps {
index: number;
}
const IconBlank: React.FunctionComponent<IIconBlankProps> = ({ index }) => {
return (
function IconBlank(props: IIconBlankProps) {
const { index } = props;
return `
<span
className={`block w-16 h-16 rounded-2xl border border-black/5 shadow-sm ${getIconColor(
index
)} overflow-hidden`}
class="${`block w-16 h-16 rounded-2xl border border-black/5 shadow-sm ${getIconColor(index)} overflow-hidden`}"
/>
);
};
`;
}
enum IconType {
uri,
@ -112,13 +98,9 @@ interface IIconBaseProps {
iconAspect?: IconAspect;
}
const IconBase: React.FunctionComponent<IIconBaseProps> = ({
icon,
iconBG,
iconBubble,
iconColor,
iconAspect = "square",
}) => {
function IconBase(props: IIconBaseProps) {
let { icon, iconBG, iconBubble, iconColor, iconAspect = "square" } = props;
let iconType: IconType = IconType.uri;
if (icon.startsWith("http") || icon.startsWith("/")) {
@ -152,7 +134,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
iconClassName += " rounded-2xl border border-black/5 shadow-sm";
}
const iconStyle: React.CSSProperties = {};
const iconStyle: string[] = [];
switch (iconType) {
case IconType.uri:
@ -160,7 +142,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
// Default to bubble and no background for URI and Dashboard icons
if (!is.null(iconBG)) {
if (iconBG?.startsWith("#")) {
iconStyle.backgroundColor = iconBG;
iconStyle.push(`background-color: ${iconBG}`);
} else {
iconClassName += ` bg-${iconBG}`;
}
@ -172,7 +154,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
iconClassName += ` bg-slate-200 dark:bg-gray-900`;
} else {
if (iconBG?.startsWith("#")) {
iconStyle.backgroundColor = iconBG;
iconStyle.push(`background-color: ${iconBG}`);
} else {
iconClassName += ` bg-${iconBG}`;
}
@ -184,41 +166,50 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
break;
}
const mdiIconStyle: React.CSSProperties = {};
const mdiIconStyle: string[] = [];
let mdiIconColorFull = "bg-" + iconColor;
if (!is.null(iconColor) && iconColor?.startsWith("#")) {
mdiIconColorFull = "";
mdiIconStyle.backgroundColor = iconColor;
mdiIconStyle.push(`background-color: ${iconColor}`);
}
switch (iconType) {
case IconType.uri:
return <img src={icon} alt="" className={iconClassName} style={{ ...iconStyle }} />;
return `<img src="${icon}" alt="" class="${iconClassName || ""}" style="${unwrapStyles(iconStyle)}" />`;
case IconType.dashboard:
icon = icon.replace(".png", "").replace(".jpg", "").replace(".svg", "");
return (
return `
<img
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`}
src=${`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`}
alt=""
className={iconClassName}
style={{ ...iconStyle }}
class="${iconClassName || ""}"
style="${unwrapStyles(iconStyle)}"
/>
);
`;
case IconType.material:
icon = icon.replace("mdi-", "").replace(".svg", "");
return (
<div className={iconClassName} style={{ ...iconStyle }}>
return `
<div class="${iconClassName || ""}" style="${{ ...iconStyle }}">
<div
className={`block ${iconWidthHeightClassName} ${mdiIconColorFull} overflow-hidden`}
style={{
...mdiIconStyle,
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
}}
className=${`block ${iconWidthHeightClassName} ${mdiIconColorFull} overflow-hidden`}
style="${unwrapStyles(
mdiIconStyle.concat(
`mask: url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
`webkit-mask: url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`
)
)}"
/>
</div>
);
`;
}
};
}
function unwrapStyles(styles: string[]): string {
if (is.null(styles)) {
return "";
}
return styles.join(";");
}

View File

@ -1,4 +1,3 @@
import React from "react";
import { IServiceCatalog } from "../shared/types";
import { CATEGORIES } from "../variables";
import { Services } from "./services";
@ -7,14 +6,14 @@ interface IProps {
catalogs: IServiceCatalog[];
}
export const ServiceCatalogList: React.FunctionComponent<IProps> = ({ catalogs }) => {
return (
export const ServiceCatalogList = function (props: IProps) {
const { catalogs } = props;
return `
<ul>
{catalogs.map((catalog, index) => (
<ServiceCatalog key={index} catalog={catalog} index={index} />
))}
${catalogs.map((catalog, index) => ServiceCatalog({ catalog, index })).join("")}
</ul>
);
`;
};
interface ICatalogProps {
@ -22,7 +21,9 @@ interface ICatalogProps {
index: number;
}
const ServiceCatalog: React.FunctionComponent<ICatalogProps> = ({ catalog, index }) => {
const ServiceCatalog = function (props: ICatalogProps) {
const { catalog, index } = props;
let categoryClassName = "dark:text-slate-200";
switch (CATEGORIES as string) {
@ -41,10 +42,10 @@ const ServiceCatalog: React.FunctionComponent<ICatalogProps> = ({ catalog, index
liClassName += " bg-white dark:bg-black rounded-2xl px-6 py-6 ring-1 ring-slate-900/5 shadow-xl";
}
return (
<li key={index} className={liClassName}>
<h2 className={categoryClassName}>{catalog.category}</h2>
<Services services={catalog.services} />
return `
<li class="${liClassName}">
<h2 class="${categoryClassName}">${catalog.category}</h2>
${Services({ services: catalog.services })}
</li>
);
`;
};

View File

@ -0,0 +1,55 @@
import { is } from "../shared/is";
import { IService } from "../shared/types";
import { Anchor } from "./anchor";
import { Icon } from "./icon";
interface IServicesProps {
services: IService[];
}
export const Services = function (props: IServicesProps) {
const { services } = props;
return `
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-1 lg:gap-2 lg:gap-y-4">
${services.map((service, index) => Service({ index, service })).join("")}
</ul>
`;
};
interface IServiceProps {
service: IService;
index: number;
}
function Service(props: IServiceProps) {
const { service, index } = props;
const { name, uri, description, icon, iconBG, iconBubble, iconColor, iconAspect, newWindow } = service;
return `
<li class="p-4 flex gap-4">
${
!is.null(icon) &&
`
<span class="flex-shrink-0 flex">
${Icon({ name, icon, uri, index, iconColor, iconBG, iconBubble, iconAspect, newWindow })}
</span>
`
}
<div>
<h3 class="text-lg mt-1 font-semibold line-clamp-1">
${Anchor({ uri, newWindow, children: name })}
</h3>
${
!is.null(description) &&
`
<p class="text-sm text-black/50 dark:text-white/50 line-clamp-1">
${Anchor({ uri, newWindow, children: description })}
</p>
`
}
</div>
</li>
`;
}

View File

@ -1,62 +0,0 @@
import React from "react";
import { is } from "../shared/is";
import { IService } from "../shared/types";
import { Anchor } from "./anchor";
import { Icon } from "./icon";
interface IServicesProps {
services: IService[];
}
export const Services: React.FunctionComponent<IServicesProps> = ({ services }) => {
return (
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-1 lg:gap-2 lg:gap-y-4">
{services.map((service, index) => (
<Service key={index} service={service} index={index} />
))}
</ul>
);
};
interface IServiceProps {
service: IService;
index: number;
}
const Service: React.FunctionComponent<IServiceProps> = ({ service, index }) => {
const { name, uri, description, icon, iconBG, iconBubble, iconColor, iconAspect, newWindow } = service;
return (
<li className="p-4 flex gap-4">
{!is.null(icon) && (
<span className="flex-shrink-0 flex">
<Icon
name={name}
icon={icon}
uri={uri}
index={index}
iconColor={iconColor}
iconBG={iconBG}
iconBubble={iconBubble}
iconAspect={iconAspect}
newWindow={newWindow}
/>
</span>
)}
<div>
<h3 className="text-lg mt-1 font-semibold line-clamp-1">
<Anchor uri={uri} newWindow={newWindow}>
{name}
</Anchor>
</h3>
{!is.null(description) && (
<p className="text-sm text-black/50 dark:text-white/50 line-clamp-1">
<Anchor uri={uri} newWindow={newWindow}>
{description}
</Anchor>
</p>
)}
</div>
</li>
);
};

26
src/css-cache-break.ts Normal file
View File

@ -0,0 +1,26 @@
import CleanCSS from "clean-css";
import * as path from "path";
import { sbMakeFolder, sbReadFile, sbRemoveAllCSS, sbWriteFile } from "./shared/files";
const mainCSSFilename = `main.${new Date().getTime()}.css`;
const cssFilePath = path.join(__dirname, "../public/css");
const cssFileInPath = path.join(__dirname, "../public", "main.css");
const cssFileOutPath = path.join(cssFilePath, mainCSSFilename);
const indexFileInOutPath = path.join(__dirname, "../public", "index.html");
async function start(): Promise<void> {
await sbRemoveAllCSS(cssFilePath);
await sbMakeFolder(cssFilePath);
const css = await sbReadFile(cssFileInPath);
await sbWriteFile(cssFileOutPath, new CleanCSS().minify(css).styles);
const index = await sbReadFile(indexFileInOutPath);
const cssLink = `<link rel="stylesheet" href="./main.css" crossorigin="">`;
const cssLinkReplacement = `<link rel="stylesheet" href="./css/${mainCSSFilename}" crossorigin="" />`;
await sbWriteFile(indexFileInOutPath, index.replace(cssLink, cssLinkReplacement));
}
start();

21
src/index.ts Normal file
View File

@ -0,0 +1,21 @@
import * as path from "path";
import { IndexPage } from "./pages/index";
import { sbReadFile, sbWriteFile } from "./shared/files";
import { PAGEICON, PAGETITLE } from "./variables";
const indexFileInPath = path.join(__dirname, "../", "index.html");
const indexFileOutPath = path.join(__dirname, "../", "./public", "index.html");
async function start(): Promise<void> {
const index = await sbReadFile(indexFileInPath);
const newText = IndexPage({ icon: PAGEICON, title: PAGETITLE });
const rootDiv = '<div id="root"></div>';
const rootDivReplacement = '<div id="root">$1</div>';
const newIndex = index.replace(rootDiv, rootDivReplacement.replace("$1", newText));
await sbWriteFile(indexFileOutPath, newIndex);
}
start();

View File

@ -1,11 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { IndexPage } from "./pages";
import "./tailwind.css";
import { PAGEICON, PAGETITLE } from "./variables";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<IndexPage title={PAGETITLE} icon={PAGEICON} />
</React.StrictMode>
);

View File

@ -1,4 +1,3 @@
import React from "react";
import { Header } from "../components/header";
import { ServiceCatalogList } from "../components/service-catalogs";
import userServicesOld from "../config.json";
@ -12,8 +11,9 @@ interface IProps {
icon?: string;
}
export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
const mySerices = (is.null(userServicesOld) ? userServices : userServicesOld) as IServiceCatalog[];
export const IndexPage = function (props: IProps): string {
const { icon, title } = props;
const myServices = (is.null(userServicesOld) ? userServices : userServicesOld) as IServiceCatalog[];
let headerClassName = "p-4";
@ -45,18 +45,21 @@ export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
serviceCatalogListWrapperClassName += " min-h-screen";
}
return (
<div className="min-h-screen">
<div className={pageWrapperClassName}>
{SHOWHEADER && (
<div className={headerClassName}>
<Header title={title} icon={icon} />
return `
<div class="min-h-screen">
<div class="${pageWrapperClassName}">
${
SHOWHEADER &&
`
<div class="${headerClassName}">
${Header({ icon, title })}
</div>
)}
<div className={serviceCatalogListWrapperClassName}>
<ServiceCatalogList catalogs={mySerices} />
`
}
<div class="${serviceCatalogListWrapperClassName}">
${ServiceCatalogList({ catalogs: myServices })}
</div>
</div>
</div>
);
`;
};

65
src/shared/files.ts Normal file
View File

@ -0,0 +1,65 @@
import * as fs from "fs/promises";
export async function sbReadFile(fileName: string): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const index = await fs.readFile(fileName);
return resolve(index.toString());
} catch (exception) {
console.error(`Could not read file: ${fileName}`);
reject(exception);
}
});
}
export async function sbWriteFile(fileName: string, contents: string): Promise<boolean> {
return new Promise(async (resolve, reject) => {
try {
await fs.writeFile(fileName, contents);
return resolve(true);
} catch (exception) {
console.error(`Could not write file: ${fileName}`);
reject(exception);
}
});
}
export async function sbRename(fileNameOld: string, fileNameNew: string): Promise<boolean> {
return new Promise(async (resolve, reject) => {
try {
await fs.rename(fileNameOld, fileNameNew);
return resolve(true);
} catch (exception) {
console.error(`Could not rename file: ${fileNameOld} to ${fileNameNew}`);
reject(exception);
}
});
}
export async function sbMakeFolder(path: string): Promise<boolean> {
return new Promise(async (resolve, reject) => {
try {
await fs.mkdir(path, { recursive: true });
return resolve(true);
} catch (exception) {
console.error(`Could not make folder: ${path}`, exception);
reject(exception);
}
});
}
export async function sbRemoveAllCSS(path: string): Promise<boolean> {
return new Promise(async (resolve, reject) => {
try {
await fs.rm(path, {
recursive: true,
force: true,
});
return resolve(true);
} catch (exception) {
console.error(`Could not remove all CSS from path: ${path}`);
reject(exception);
}
});
}

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,61 +0,0 @@
import { exec } from "child_process";
import fs from "fs/promises";
import puppeteer from "puppeteer";
function runNpmCommand(command) {
return new Promise((resolve, reject) => {
exec(`npm ${command}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing command: ${error}`);
reject(error);
} else {
console.log(`Command output: ${stdout}`);
resolve(stdout);
}
});
});
}
async function renderHomePage() {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
executablePath: "chromium", // Remove this when running locally
});
const page = await browser.newPage();
await page.goto("http://localhost:4173", { waitUntil: "networkidle0" });
// Wait for any additional content to load
await page.waitForNetworkIdle();
// Get the rendered HTML content
const renderedHTML = await page.content();
// Save the rendered HTML to a file
fs.writeFile("./dist/index.html", renderedHTML);
await browser.close();
}
async function runCommands() {
try {
console.log("Building React site");
await runNpmCommand("run build");
setTimeout(async () => {
console.log("Taking snapshot");
await renderHomePage();
await runNpmCommand("run html");
await runNpmCommand("run replace");
}, 2000);
console.log("Starting web server");
await runNpmCommand("run serve");
} catch (err) {
// Handle errors
console.error("An error occurred:", err);
}
}
runCommands();

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: ["./public/index.html"],
safelist: [
/* Built from icon.tsx */
{

View File

@ -1,21 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"module": "CommonJS"
},
"include": ["src", "public"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src/index.ts", "src/css-cache-break.ts"]
}

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -1 +1 @@
1.3.0
1.4.0

View File

@ -1,15 +0,0 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
watch: {
usePolling: true,
},
host: true,
strictPort: true,
port: 4173,
},
});