commit ff8fd8eec42e2f65feab20ab12dd369fa8946ce1 Author: Cory Sanin Date: Thu Jan 18 04:24:51 2024 -0500 initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b1975bb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +docker-cache/ +node_modules/ +assets/css/ +assets/js/ +assets/webp/ +config/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bcd94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +# 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/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76444fe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM oven/bun:alpine AS baseimg + +FROM baseimg AS dependencies +WORKDIR /build + +COPY ./package*json ./ +COPY ./bun.lockb ./ +RUN bun install --production --no-progress && \ + chown -R bun . + + +FROM dependencies as build-env +WORKDIR /build + +RUN apk add --no-cache libwebp libwebp-tools + +RUN bun install --no-progress + +COPY . . + +RUN bun run build.ts && \ + chown -R bun . + + +FROM baseimg as deploy +WORKDIR /usr/src/bitch +HEALTHCHECK --timeout=3s \ + CMD curl --fail http://localhost:8080/healthcheck || exit 1 +RUN apk add --no-cache curl +COPY --from=dependencies /build . +COPY --from=build-env /build/assets ./assets +COPY . . +USER bun + +EXPOSE 8080 +CMD [ "bun", "run", "index.ts"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be392d2 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# jakehurwitzisabitch + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.0.23. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/assets/images/bitch.jpg b/assets/images/bitch.jpg new file mode 100644 index 0000000..d8e3d24 Binary files /dev/null and b/assets/images/bitch.jpg differ diff --git a/assets/images/cache-me.png b/assets/images/cache-me.png new file mode 100644 index 0000000..e7f9353 Binary files /dev/null and b/assets/images/cache-me.png differ diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..95cdb47 --- /dev/null +++ b/build.ts @@ -0,0 +1,171 @@ +import fs from 'fs'; +import path from 'path'; +import child_process from 'child_process'; +import uglifyjs from "uglify-js"; +import * as sass from 'sass'; +import * as csso from 'csso'; + +const spawn = child_process.spawn; +const fsp = fs.promises; +const STYLESDIR = 'styles'; +const SCRIPTSDIR = 'scripts'; +const IMAGESDIR = path.join('assets', 'images'); +const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join(import.meta.dir, 'assets', 'css'); +const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join(import.meta.dir, 'assets', 'js'); +const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join(import.meta.dir, 'assets', 'webp'); +const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css'; +const SQUASH = new RegExp('^[0-9]+-'); + +async function emptyDir(dir: string) { + await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, { + recursive: true, + force: true + }))); +} + +async function mkdir(dir: string | string[]) { + if (typeof dir === 'string') { + await fsp.mkdir(dir, { recursive: true }); + } + else { + await Promise.all(dir.map(mkdir)); + } +} + +// Process styles +async function styles() { + await mkdir([STYLEOUTDIR, STYLESDIR]); + await emptyDir(STYLEOUTDIR); + let styles: string[] = []; + let files = await fsp.readdir(STYLESDIR); + await Promise.all(files.map(f => new Promise(async (res, reject) => { + let p = path.join(STYLESDIR, f); + console.log(`Processing style ${p}`); + let style = sass.compile(p).css; + if (f.charAt(0) !== '_') { + if (SQUASH.test(f)) { + styles.push(style); + } + else { + let o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css'); + await fsp.writeFile(o, csso.minify(style).css); + console.log(`Wrote ${o}`); + } + } + res(0); + }))); + let out = csso.minify(styles.join('\n')).css; + let outpath = path.join(STYLEOUTDIR, STYLEOUTFILE); + await fsp.writeFile(outpath, out); + console.log(`Wrote ${outpath}`); +} + +// Process scripts +async function scripts() { + await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]); + await emptyDir(SCRIPTSOUTDIR); + let files = await fsp.readdir(SCRIPTSDIR); + await Promise.all(files.map(f => new Promise(async (res, reject) => { + let p = path.join(SCRIPTSDIR, f); + let o = path.join(SCRIPTSOUTDIR, f); + console.log(`Processing script ${p}`); + try { + await fsp.writeFile(o, uglifyjs.minify((await fsp.readFile(p)).toString()).code); + console.log(`Wrote ${o}`); + } + catch (ex) { + console.log(`error writing ${o}: ${ex}`); + } + res(0); + }))); +} + +// Process images +async function images(dir = '') { + let p = path.join(IMAGESDIR, dir); + await mkdir(p); + if (dir.length === 0) { + await mkdir(IMAGESOUTDIR) + await emptyDir(IMAGESOUTDIR); + } + let files = await fsp.readdir(p, { + withFileTypes: true + }); + if (files.length) { + await Promise.all(files.map(f => new Promise(async (res, reject) => { + if (f.isFile()) { + let outDir = path.join(IMAGESOUTDIR, dir); + let infile = path.join(p, f.name); + let outfile = path.join(outDir, f.name.substring(0, f.name.lastIndexOf('.')) + '.webp'); + await mkdir(outDir); + console.log(`Processing image ${infile}`) + let process = spawn('cwebp', ['-mt', '-q', '50', infile, '-o', outfile]); + let timeout = setTimeout(() => { + reject('Timed out'); + process.kill(); + }, 30000); + process.on('exit', async (code) => { + clearTimeout(timeout); + if (code === 0) { + console.log(`Wrote ${outfile}`); + res(null); + } + else { + reject(code); + } + }); + } + else if (f.isDirectory()) { + images(path.join(dir, f.name)).then(res).catch(reject); + } + }))); + } +} + +function isAbortError(err: unknown): boolean { + return typeof err === 'object' && err !== null && 'name' in err && err.name === 'AbortError'; +} + +(async function () { + await Promise.all([styles(), scripts(), images()]); + if (process.argv.indexOf('--watch') >= 0) { + console.log('watching for changes...'); + (async () => { + try { + const watcher = fsp.watch(STYLESDIR); + for await (const _ of watcher) + await styles(); + } catch (err) { + if (isAbortError(err)) + return; + throw err; + } + })(); + + (async () => { + try { + const watcher = fsp.watch(SCRIPTSDIR); + for await (const _ of watcher) + await scripts(); + } catch (err) { + if (isAbortError(err)) + return; + throw err; + } + })(); + + (async () => { + try { + const watcher = fsp.watch(IMAGESDIR, { + recursive: true // no Linux ☹️ + }); + for await (const _ of watcher) + await images(); + } catch (err) { + if (isAbortError(err)) + return; + throw err; + } + })(); + } +})(); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ed0145c Binary files /dev/null and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cfe8e9e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '2' + +services: + bitch: + container_name: bitch + build: + context: ./ + dockerfile: Dockerfile + restart: "no" + ports: + - 8080:8080 + volumes: + - ./config:/usr/src/bitch/config \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..4d2da52 --- /dev/null +++ b/index.ts @@ -0,0 +1,58 @@ +import type { Express } from "express"; +import express, { application } from 'express'; +import Path from 'path'; + +function notStupidParseInt(v: string | undefined): number { + return v === undefined ? NaN : parseInt(v); +} + +const app: Express = express(); +const port: number = notStupidParseInt(process.env.PORT) || 8080; +const dataPath: string = process.env.CONFIG || Path.join('config', 'config.json'); + +app.set('trust proxy', 1); +app.set('view engine', 'ejs'); +app.set('view options', { outputFunctionName: 'echo' }); +app.use('/assets', express.static('assets')); + +let visitCount = (await Bun.file(dataPath).json())['visits'] as number || 0; + +app.get('/', (_, res) => { + res.render('index', + { + visitCount + }, + function (err, html) { + if (!err) { + res.send(html); + } + else { + console.error(err); + res.status(500).send('Something went wrong. Please try again later.'); + } + }); +}); + +app.get('/cache-me.webp', (_, res) => { + res.set('Cache-Control', 'public, max-age=31557600'); // one year + visitCount++; + res.sendFile(Path.join(import.meta.dir, 'assets', 'webp', 'cache-me.webp')); +}); + +const webserver = app.listen(port, () => console.log(`jakehurwitzisabitch.com running on port ${port}`)); + +async function saveVisits() { + await Bun.write(dataPath, JSON.stringify({ + visits: visitCount + })); +} + +let inverval = setInterval(saveVisits, 1800000); + +process.on('SIGTERM', async () => { + clearInterval(inverval); + console.log('writing visit count...'); + await saveVisits(); + console.log('done. shutting down...'); + webserver.close(); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b87e0fe --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "jakehurwitzisabitch", + "module": "index.ts", + "type": "module", + "dependencies": { + "ejs": "3.1.9", + "express": "4.18.2" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "4.17.21", + "@types/csso": "5.0.4", + "@types/uglify-js": "3.17.4", + "csso": "5.0.5", + "sass": "1.66.1", + "uglify-js": "3.17.4" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/styles/00-reset.css b/styles/00-reset.css new file mode 100644 index 0000000..af94440 --- /dev/null +++ b/styles/00-reset.css @@ -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; +} \ No newline at end of file diff --git a/styles/01-poetry-n-style.scss b/styles/01-poetry-n-style.scss new file mode 100644 index 0000000..cd66399 --- /dev/null +++ b/styles/01-poetry-n-style.scss @@ -0,0 +1,55 @@ +html, +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font: normal 14px 'Helvetica Neue', Arial, sans-serif; +} + +body { + background: url('/assets/webp/bitch.webp'); +} + +#w { + max-width: 620px; + margin: 0 auto; + padding: 125px 0 50px; + text-transform: uppercase; + + h1 { + margin: 0 0 100px; + font-weight: normal; + font-size: 79px; + text-align: center; + color: #FFFC00; + text-shadow: 0 0 5px #333; + } + + p { + display: inline-block; + padding: .2em .5em; + color: #00F900; + line-height: 1.5em; + letter-spacing: 1px; + background: #000; + + } + + #pixel { + background: url('/cache-me.webp'); + position: fixed; + left: -99999999999999px; + top: -20px; + } +} + +@media screen and (max-width: 640px) { + body { + background-size: 50%; + } + + #w h1 { + font-size: 40px; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dcd8fc5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..5658f1a --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,21 @@ + + + + + + + JAKE HURWITZ IS A BITCH + + + + + + +
+

Welcome to Jake Hurwitz is a bitch .com

+

<%= visitCount %> People have seen this site and agreed Jake Hurwitz is a bitch

+ +
+ + + \ No newline at end of file