generated from corysanin/nodejs-web-template
Initial commit
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
docker-cache/
|
||||
node_modules/
|
||||
distribution/
|
||||
assets/css/
|
||||
assets/js/
|
||||
assets/images/webp/
|
||||
assets/images/avif/
|
||||
docker-compose.yml
|
99
.github/workflows/build_docker_image.yml
vendored
Normal file
99
.github/workflows/build_docker_image.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: App Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
||||
jobs:
|
||||
build_app_image:
|
||||
name: Build app image
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DH_REGISTRY: docker.io
|
||||
GH_REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
REPOSITORY: ${{ github.event.repository.name }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DH_REGISTRY }}
|
||||
username: ${{ env.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ env.GITHUB_SERVER_URL == 'https://github.com' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GH_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for release Docker image
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKER_USERNAME }}/${{ env.REPOSITORY }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Extract metadata for develop Docker image
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
id: meta-develop
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push release Docker image
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
target: deploy
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push develop Docker image
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
target: deploy
|
||||
push: ${{ env.GITHUB_SERVER_URL == 'https://github.com' }}
|
||||
tags: ${{ steps.meta-develop.outputs.tags }}
|
||||
labels: ${{ steps.meta-develop.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
27
.github/workflows/npm-audit.yml
vendored
Normal file
27
.github/workflows/npm-audit.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: NPM Audit Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 16 * * 5'
|
||||
|
||||
jobs:
|
||||
|
||||
npm_audit:
|
||||
name: Check NPM audit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: NPM Audit
|
||||
run: npm audit
|
110
.gitignore
vendored
Normal file
110
.gitignore
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
assets/css/
|
||||
assets/js/
|
||||
assets/webp/
|
||||
config/
|
||||
distribution/
|
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/distribution/index.js",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/distribution/**/*.js"]
|
||||
}
|
||||
]
|
||||
}
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
FROM base AS build-env
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache libwebp libwebp-tools libavif-apps
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npx tsc && npm run-script build && \
|
||||
npm ci --only=production && \
|
||||
ln -sf /usr/share/fonts assets/ && \
|
||||
chown -R node .
|
||||
|
||||
FROM base AS deploy
|
||||
|
||||
HEALTHCHECK --timeout=3s \
|
||||
CMD curl --fail http://localhost:8080/healthcheck || exit 1
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
COPY --from=build-env /usr/src/app /usr/src/app
|
||||
|
||||
USER node
|
||||
|
||||
CMD [ "node", "distribution/index.js"]
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
nodejs-web:
|
||||
container_name: nodejs-web
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
restart: "no"
|
||||
ports:
|
||||
- 8080:8080
|
1761
package-lock.json
generated
Normal file
1761
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "nodejs-web-template",
|
||||
"version": "0.0.1",
|
||||
"description": "A nodejs web project",
|
||||
"keywords": [
|
||||
"web",
|
||||
"template"
|
||||
],
|
||||
"homepage": "https://github.com/CorySanin/nodejs-web-template#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CorySanin/nodejs-web-template/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/CorySanin/nodejs-web-template.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Cory Sanin",
|
||||
"type": "module",
|
||||
"main": "./distribution/index.js",
|
||||
"files": [
|
||||
"distribution",
|
||||
"assets",
|
||||
"views"
|
||||
],
|
||||
"dependencies": {
|
||||
"ejs": "3.1.10",
|
||||
"express": "5.1.0",
|
||||
"express-session": "1.18.2",
|
||||
"json5": "2.2.3",
|
||||
"ky": "1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sindresorhus/tsconfig": "8.0.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/node": "^24.7.0",
|
||||
"forking-build-shit": "1.0.4",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npx build-shit",
|
||||
"watch": "npx build-shit --watch"
|
||||
}
|
||||
}
|
103
src/Web.ts
Normal file
103
src/Web.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as http from "http";
|
||||
import crypto from 'crypto';
|
||||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import ky from 'ky';
|
||||
import bodyParser from 'body-parser';
|
||||
import type { Express } from 'express';
|
||||
|
||||
interface WebConfig {
|
||||
sessionSecret?: string;
|
||||
port?: number;
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* I still hate typescript.
|
||||
*/
|
||||
function notStupidParseInt(v: string | undefined): number {
|
||||
return v === undefined ? NaN : parseInt(v);
|
||||
}
|
||||
|
||||
class Web {
|
||||
private _webserver: http.Server | null = null;
|
||||
private app: Express | null = null;
|
||||
private port: number;
|
||||
private options: WebConfig;
|
||||
|
||||
constructor(options: WebConfig = {}) {
|
||||
this.options = options;
|
||||
this.port = notStupidParseInt(process.env['PORT']) || options['port'] as number || 8080;
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
const options = this.options;
|
||||
const sessionSecret = process.env['SESSIONSECRET'] || options.sessionSecret;
|
||||
const app: Express = this.app = express();
|
||||
|
||||
if(!sessionSecret) {
|
||||
console.error('sessionSecret is required.');
|
||||
throw new Error('sessionSecret is required.');
|
||||
}
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('view options', { outputFunctionName: 'echo' });
|
||||
app.use('/assets', express.static('assets', { maxAge: '30 days' }));
|
||||
app.use(session({
|
||||
name: 'sessionId',
|
||||
secret: sessionSecret,
|
||||
resave: true,
|
||||
saveUninitialized: false,
|
||||
store: undefined,
|
||||
cookie: {
|
||||
maxAge: notStupidParseInt(process.env['COOKIETTL']) || 1000 * 60 * 60 * 24 * 30, // 30 days
|
||||
httpOnly: true,
|
||||
secure: !!options.secure
|
||||
}
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use((_req, res, next) => {
|
||||
crypto.randomBytes(32, (err, randomBytes) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
next(err);
|
||||
} else {
|
||||
res.locals['cspNonce'] = randomBytes.toString("hex");
|
||||
next();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/healthcheck', (_, res) => {
|
||||
res.send('Healthy');
|
||||
});
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
res.render('index', {
|
||||
page: {
|
||||
title: 'Web',
|
||||
titlesuffix: 'Home',
|
||||
description: 'Homepage'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/ky', async (_, res) => {
|
||||
res.send(await (await ky.get('https://sanin.dev')).text());
|
||||
});
|
||||
|
||||
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
|
||||
}
|
||||
|
||||
close = () => {
|
||||
if (this._webserver) {
|
||||
this._webserver.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Web;
|
||||
export { Web, notStupidParseInt };
|
||||
export type { WebConfig };
|
28
src/index.ts
Normal file
28
src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import JSON5 from 'json5';
|
||||
import { Web, type WebConfig } from './Web.js';
|
||||
|
||||
interface compositeConfig {
|
||||
web?: WebConfig
|
||||
}
|
||||
|
||||
async function readConfig(): Promise<compositeConfig> {
|
||||
try {
|
||||
return JSON5.parse(await fs.promises.readFile(process.env['config'] || process.env['CONFIG'] || path.join(process.cwd(), 'config', 'config.jsonc'), 'utf-8'));
|
||||
}
|
||||
catch (err) {
|
||||
console.error('No config file found, using default config');
|
||||
console.error(err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const config: compositeConfig = await readConfig();
|
||||
|
||||
const web = new Web(config.web);
|
||||
await web.initialize();
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
web.close();
|
||||
});
|
48
styles/00-reset.css
Normal file
48
styles/00-reset.css
Normal file
@@ -0,0 +1,48 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@sindresorhus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
22
views/head.ejs
Normal file
22
views/head.ejs
Normal file
@@ -0,0 +1,22 @@
|
||||
<title><%= page.title %> | <%= page.titlesuffix %></title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="og:title" content="<%= page.title%> | <%= page.titlesuffix%>" />
|
||||
<% if (page.description) { %>
|
||||
<meta name="description" content="<%= page.description%>" />
|
||||
<meta name="og:description" content="<%= page.description%>" />
|
||||
<% } %>
|
||||
<% if (page.image) { %>
|
||||
<meta name="og:image" content="<%= page.image%>"/>
|
||||
<link rel="image_src" href="<%= page.image%>"/>
|
||||
<% } %>
|
||||
<% if (page.canonical) { %>
|
||||
<link rel="canonical" href="<%= page.canonical%>"/>
|
||||
<% } %>
|
||||
<link rel="shortcut icon" href="/assets/svg/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v4">
|
||||
<script nonce="<%= cspNonce %>">
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.body.classList.remove('preload');
|
||||
});
|
||||
</script>
|
14
views/index.ejs
Normal file
14
views/index.ejs
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
|
||||
<head>
|
||||
<%- include("head", locals) %>
|
||||
</head>
|
||||
|
||||
<body class="preload">
|
||||
<%- include("navigation", locals) %>
|
||||
<div class="content">
|
||||
<h1>Hello World</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
11
views/navigation.ejs
Normal file
11
views/navigation.ejs
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="navigation">
|
||||
<div class="nav_logo">
|
||||
<a href="/"><img src="/assets/svg/logo.svg" alt="Logo" /></a>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="nav_links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/ky">Away</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
Reference in New Issue
Block a user