Browse Source

Removing most of custom Pug setup in favor of Hugo

master
Mobius K 10 months ago
parent
commit
4e77e813cf
  1. 105
      cli/cli.ts
  2. 147
      cli/client.ts
  3. 115
      cli/release.ts
  4. 35
      cli/server.ts
  5. 5521
      package-lock.json
  6. 27
      package.json
  7. 94
      rollup.config.js
  8. 2
      shared/config/build.dev.js
  9. 2
      shared/config/build.primary.js
  10. 2
      shared/config/build.secondary.js
  11. 48
      shared/scripts/dom.ts
  12. 122
      shared/scripts/service-worker-cache.ts

105
cli/cli.ts

@ -1,9 +1,5 @@
import { execSync } from "child_process";
import concurrently from "concurrently";
import { exit } from "process";
import { Client } from "./client";
import { ImageProcesser } from "./images";
import { Release } from "./release";
// High-level scripts for common development processes
@ -39,118 +35,17 @@ if (environment == null || ! environments.includes(environment)) {
}
// Useful script values
const publicDir = `/srv/${project}/public`;
const client = new Client(project, publicDir);
const imageProcessor = new ImageProcesser(project);
const release = new Release(project, environment, publicDir);
// Dynamic build values based on target environment
let buildOptions: Record<string, unknown> = {
basedir: "./",
...require(`../shared/config/build.${environment}.js`)
};
try {
buildOptions = {
...buildOptions,
...require(`../${project}/config/build.all.js`),
...require(`../${project}/config/build.${environment}.js`)
};
} catch (e) {
// We don't really care if the project doesn't have custom build configs
}
// Clean public build directory to prepare for a new build
function clean(): void {
console.log(`Cleaning out ${publicDir}`);
execSync(`rm -rf ${publicDir}/*`);
}
// Start a development session with auto-updates
function develop(): void {
concurrently([
{ command: "npm:start assetsWatch", name: "Assets" },
{ command: "npm:start cssWatch", name: "CSS " },
{ command: "npm:start htmlWatch", name: "HTML " },
{ command: "npm:start jsWatch", name: "JS " },
{ command: "npm:start browserSync", name: "Sync " },
{ command: "npm:start serverRun", name: "Server" },
]);
}
// Check all of our invocation arguments for recognized tasks to run
// Ignore the first two arguments, as that is the Node + script paths
process.argv.slice(2).forEach((arg: string) => {
switch(arg) {
case "assets":
client.assets("Running");
break;
case "assetsWatch":
client.assets("Watching", true);
break;
case "browserSync":
client.browserSync(publicDir);
break;
case "build":
clean();
release.build();
break;
case "clean":
clean();
break;
case "images":
imageProcessor.process();
break;
case "css":
client.css("Running");
break;
case "cssWatch":
client.css("Watching", true);
break;
case "develop":
develop();
break;
case "html":
client.html(buildOptions, "Running");
break;
case "htmlWatch":
client.html(buildOptions, "Watching", true);
break;
case "js":
client.js();
break;
case "jsWatch":
client.js("--watch");
break;
case "serverBuild":
case "serverDebug":
case "serverRun":
console.log("Server executables currently not supported");
break;
case "transfer":
release.transfer();
break;
case "transferCheck":
release.transfer("-n");
break;
default:
console.error(`Unknown argument: ${arg}`);
exit(1);

147
cli/client.ts

@ -1,147 +0,0 @@
import { create } from "browser-sync";
import { execSync } from "child_process";
import { FSWatcher } from "chokidar";
import { writeFileSync } from "fs";
import { renderSync } from "node-sass";
import { renderFile } from "pug";
/**
* Scripts for building web clients
*
* Builds and transfers assets, CSS, HTML, and JS to local web server.
* Watches file changes to trigger rebuilds.
* Sets up browser reload on file changes.
*/
export class Client {
// Constructor field(s)
private readonly project: string | undefined;
private readonly outputDir: string;
public constructor(project: string | undefined, outputDir: string) {
this.project = project;
this.outputDir = outputDir;
}
/**
* Rsync asset files to web server directory
*
* @param reasonForRunning Logged reason this function was executed
* @param watch Whether files should be watched (changed files are fed into `reasonForRunning`)
*/
public assets(reasonForRunning: string, watch = false): void {
console.log(`Syncing assets: ${reasonForRunning}`);
const assetDirectories = `shared/assets/ shared/assets/.well-known ${(this.project)}/assets/`;
const rsyncOutput = execSync(`rsync -aiO --chmod=D775,F664 --chown=:${(this.project)} ${assetDirectories} ${(this.outputDir)}`).toString();
console.log("\t" + rsyncOutput.replace(/\n/g, "\n\t"));
if (watch) {
Client.setupWatcher(
["shared/assets", `${(this.project)}/assets`],
(event: string, path: string) => this.assets(`${event} ${path}`)
);
}
}
/**
* Compile SCSS into CSS
*
* @param reasonForRunning Logged reason this function was executed
* @param watch Whether files should be watched (changed files are fed into `reasonForRunning`)
*/
public css(reasonForRunning: string, watch = false): void {
console.log(`Compiling styles: ${reasonForRunning}`);
writeFileSync(`${this.outputDir}/${this.project}.css`, renderSync({
file: `${this.project}/styles/index.scss`,
includePaths: ["node_modules"],
}).css);
if (watch) {
Client.setupWatcher(
`${this.project}/styles`,
(event: string, path: string) => this.css(`${event} ${path}`)
);
}
}
/**
* Compile Pug markup into HTML
*
* @param buildOptions Object map passed to the Pug compiler to adjust template output
* @param reasonForRunning Logged reason this function was executed
* @param watch Whether files should be watched (changed files are fed into `reasonForRunning`)
*/
public html(
buildOptions: Record<string, unknown>,
reasonForRunning: string,
watch = false
): void {
console.log(`Compiling HTML: ${reasonForRunning}`);
const dir = `${this.project}/html/routes/`;
execSync(`find ${dir} -type f`)
.toString()
.split("\n")
.filter((route: string) => route.length > 0)
.forEach((route: string) => {
const html = renderFile(route, buildOptions);
route = route.replace(dir, "").replace(".pug", ".html");
const routeDir = route.split("/").slice(0, -1).join("/");
execSync(`mkdir -p ${this.outputDir}/${routeDir}`);
writeFileSync(`${this.outputDir}/${route}`, html);
});
if (watch) {
Client.setupWatcher(
`${this.project}/html`,
(event: string, path: string) => this.html(buildOptions, `${event} ${path}`)
);
}
}
/**
* Compily TypeScript into JavaScript via Rollup invocation
*
* @param flags Flags passed directly to rollup, like "--watch"
*/
public js(flags = ""): void {
console.log(`Compiling scripts ${flags ? ": " + flags : ""}`);
execSync(`npx rollup --config ${flags}`);
}
/**
* Syncing server proxy to auto-reload on changes
*
* @param watchDir Directory to watch for BrowserSync reloads
*/
public browserSync(watchDir: string): void {
const proxy = `${this.project}.local`;
console.log(`Running BrowserSync server proxy: ${proxy}`);
const bSync = create();
bSync.init({
files: `${watchDir}/**/*`,
online: false,
open: false,
proxy: proxy,
watch: true,
});
}
/**
* Setup chokidar to watch file system paths and execute an action on file change
*
* @param paths Path(s) to watch for the given onEvent handler
* @param onEvent Action to take when any file change is detected (usually re-invoking caller)
*/
private static setupWatcher(
paths: string|string[],
onEvent: (eventName: string, path: string) => void
): void {
const watcher: FSWatcher = new FSWatcher({ignoreInitial: true});
watcher.add(paths);
watcher.on("all", onEvent);
}
}

115
cli/release.ts

@ -1,115 +0,0 @@
// Scripts for building and transferring QA or production releases
import concurrently from "concurrently";
import { execSync } from "child_process";
import { exit } from "process";
import purifycss from "purify-css";
/**
* Scripts for building and release websites
*/
export class Release {
// Timestamp key for when this build was started
private readonly timestamp: number = Date.now();
// Constructor field(s)
private readonly project: string;
private readonly environment: string;
private readonly publicDir: string;
public constructor(
project: string,
environment: string,
publicDir: string
) {
this.project = project;
this.environment = environment;
this.publicDir = publicDir;
}
/**
* Create a build for release
*/
public async build(): Promise<void> {
return new Promise(resolve => {
concurrently([
{ command: "npm:start assets", name: "Assets" },
{ command: "npm:start css", name: "CSS " },
{ command: "npm:start html", name: "HTML " },
{ command: "npm:start js", name: "JS " },
{ command: "npm:start serverBuild", name: "Server" },
], {
killOthers: ["failure"],
}).then(
() => {
this.minimizeCss();
this.applyBuildKeys();
resolve();
},
(failure) => {
console.error(failure);
exit(1);
},
);
});
}
/**
* Transfer builds to remote servers
*/
public transfer(extraFlags = ""): void {
console.log(`Transferring build: ${this.project} for ${this.environment} environment`);
const rsyncFlags = `-aivzO --delete --exclude 'analytics.html' --chmod=D775,F664 --chown=:${this.project}`;
const rsyncCommand = `rsync ${rsyncFlags} ${extraFlags} ${this.publicDir}/ ${this.environment}:${this.publicDir}`;
const rsyncOutput: string = execSync(rsyncCommand).toString();
console.log("\t" + rsyncOutput.replace(/\n/g, "\n\t"));
}
/**
* Minimize CSS output (except for UIKit SVG icon strokes)
*/
private minimizeCss(): void {
console.log("Minimizing CSS");
const cssOutput = `${this.publicDir}/${this.project}.css`;
purifycss(
[`${this.publicDir}/**/*.html`, `${this.publicDir}/**/*.js`],
[cssOutput],
{
info: true,
minify: true,
output: cssOutput,
whitelist: ["*lightbox*", "*stroke*"],
}
);
}
/**
* Set unique build keys like timestamps and file hashes based on pattern matching
*/
private applyBuildKeys(): void {
console.log("Applying build keys");
const nonBinaryFiles = execSync(`find ${this.publicDir} -type f -exec grep -Il . {} \\;`)
.toString()
.split("\n")
.filter(file => file != "")
.join(" ");
execSync(`sed -i "s/replaceAtBuildWithDate/${this.timestamp}/g" ${nonBinaryFiles}`);
const hashKey = "replaceAtBuildWithHash";
execSync(`grep -hor '[^/ ]*?key=${hashKey}' ${nonBinaryFiles} | sort -u`)
.toString()
.split("\n")
.filter((file: string) => file != "")
.map((file: string) => file.replace(`?key=${hashKey}`, ""))
.forEach((file: string) => {
const hash = execSync(`md5sum ${this.publicDir}/${file} | head -c 16`);
console.log(`\tHashing: ${hash} ${file}`);
execSync(`sed -i -r "s|${file}\\?key=${hashKey}|${file}?key=${hash}|g" ${nonBinaryFiles}`);
});
}
}

35
cli/server.ts

@ -1,35 +0,0 @@
// Scripts for building web servers: Building, running, debugging
export class Server {
// Build a server executable if one exists
public build(): void {
/*
if [ -f $(project)/server/main/main.go ]; then \
go build -o $(build)/server $(project)/server/main/main.go; \
fi
*/
}
// Wait for debug client attachment and then run server
public debug(): void {
/*
execSync(`dlv --listen localhost:2345 --headless --api-version=2 debug ./${project}/server/main`);
*/
}
// Start a server executable (not to be confused with Nginx web server)
public run(): void {
/*
@if [ -f $(project)/server/main/main.go ]; then \
while true; do \
go run ./$(project)/server/main & PID=$$!; \
$(inotifywait) $(project)/server shared/server; \
pkill -P $$PID; \
done \
fi
*/
}
}

5521
package-lock.json
File diff suppressed because it is too large
View File

27
package.json

@ -4,7 +4,7 @@
"start": "node ./cli/cli.js"
},
"dependencies": {
"@sentry/browser": "^5.27.3",
"@sentry/browser": "^5.27.6",
"uikit": "^3.5.9"
},
"devDependencies": {
@ -12,31 +12,16 @@
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.12.7",
"@babel/preset-typescript": "^7.12.7",
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-typescript": "^6.1.0",
"@types/browser-sync": "^2.26.1",
"@types/concurrently": "^5.2.1",
"@types/gapi.auth2": "0.0.52",
"@types/node-sass": "^4.11.1",
"@types/pug": "^2.0.4",
"@types/node": "^14.14.10",
"@types/uikit": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"browser-sync": "^2.26.13",
"chokidar": "^3.4.3",
"concurrently": "^5.3.0",
"eslint": "^7.13.0",
"node-sass": "^5.0.0",
"pug": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.8.2",
"@typescript-eslint/parser": "^4.8.2",
"eslint": "^7.14.0",
"purify-css": "^1.2.5",
"rollup": "^2.33.1",
"rollup-plugin-svg": "^2.0.0",
"rollup-plugin-terser": "^7.0.2",
"tinify": "^1.6.0-beta.2",
"tslib": "^2.0.3",
"typescript": "^4.0.5"
"typescript": "^4.1.2"
},
"license": "UNLICENSED",
"name": "mobius_k",

94
rollup.config.js

@ -1,94 +0,0 @@
import resolve from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import typescript from "@rollup/plugin-typescript";
import commonjs from "@rollup/plugin-commonjs";
import svg from "rollup-plugin-svg";
import fs from "fs";
import { terser } from "rollup-plugin-terser";
const scripts = [];
const project = process.env.project;
const env = process.env.env;
// Determining if this is a release build or not
const release = env != "dev";
// If the given input file exists, use Rollup plugins to process it as output file
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function parse(inputFile, outputFile) {
// Process the file given to us if it exists
if (fs.existsSync(inputFile)) {
scripts.push({
// Input and output locations/types
input: `${inputFile}`,
output: {
file: `/srv/${project}/public/${outputFile}`,
format: "cjs",
sourcemap: !release,
},
// Input transformers
plugins: [
// UIkit needs a little work to have pick-and-choose component builds
replace({
"uikit-util": "uikit/src/js/util",
"UIkit.version = VERSION": "UIkit.version = 'custom'",
}),
// Production environments use different build files
release && replace({"/config/build.dev": `/config/build.${env}`}),
// Importing SVG files as base64
svg(),
// Resolve external dependencies from NPM
resolve(),
// Convert any CommonJS modules to ES6 for Rollup bundling
commonjs(),
// Compile Typescript to JavaScript
typescript({
sourceMap: !release
}),
// Output minification
release && terser(),
],
// Delete dead code
treeshake: true,
// If in watch mode, don't clear the screen on new output
// We have processes usually running in parallel with important output too
watch: {
clearScreen: false,
}
});
}
}
// All projects should have one entry point, if any
parse(`${project}/scripts/index.ts`, `${project}.js`);
// Projects may also have a separate service worker and error monitor for release builds
if (release) {
parse(`${project}/scripts/service-worker.ts`, "service-worker.js");
scripts.push({
input: "node_modules/@sentry/browser/build/bundle.min.js",
output: {
file: `/srv/${project}/public/sentry.js`,
format: "cjs"
}
});
}
export default scripts;

2
shared/config/build.dev.js

@ -1,2 +0,0 @@
// Are we building a release?
exports.release = false;

2
shared/config/build.primary.js

@ -1,2 +0,0 @@
// Are we building a release?
exports.release = true;

2
shared/config/build.secondary.js

@ -1,2 +0,0 @@
// Are we building a release?
exports.release = true;

48
shared/scripts/dom.ts

@ -1,48 +0,0 @@
/**
* Document object model manipulations
*/
export class DOM {
/**
* Replace all current content of an element
*
* @param id Unique identifier of element being manipulated
* @param html New element content
*/
public changeInnerHtml(id: string, html: string): void {
const element: HTMLElement | null = document.getElementById(id);
if (element != null) {
element.innerHTML = html;
}
}
/**
* Retrieve the current value of an input element
*
* @param id Unique identifier of input element being read
*/
public getValue(id: string): any {
let value: any;
const element: HTMLElement | null = document.getElementById(id);
if (element != null) {
const input: HTMLInputElement = element as HTMLInputElement;
value = input.value;
}
return value;
}
/**
* Set the current value of an input element
*
* @param id Unique identifier of input element being updated
* @param value New input element value
*/
public setValue(id: string, value: any): void {
const element: HTMLElement | null = document.getElementById(id);
if (element != null) {
const input: HTMLInputElement = element as HTMLInputElement;
input.value = value;
}
}
}

122
shared/scripts/service-worker-cache.ts

@ -1,122 +0,0 @@
/**
* Caching API for service workers
*/
export class ServiceWorkerCache {
/**
* Ever-increasing ID replaced at build time to ensure builds are separated
*/
private readonly cacheId;
constructor(cacheId: string) {
this.cacheId = cacheId;
}
/**
* Delete items in cache that don't match these requests, and add them if missing
*
* @param requests Requests to ensure are in the cache, with everything else cleaned out
*/
public async initialize(requests: RequestInfo[]): Promise<void> {
const cache: Cache = await this.getCache();
// Map requests, which could be a string, to a Request and back to URL to resolve absolute references
requests = requests.map(request => new Request(request).url);
// Check the current cache to make sure we aren't doing unnecessary work
// Delete items in cache that don't match given requests or are HTML pages
// Remove items from given requests that are already in cache
const currentCacheKeys: ReadonlyArray<Request> = await cache.keys();
for (const currentCacheKey of currentCacheKeys) {
if ( ! requests.includes(currentCacheKey.url) || currentCacheKey.url.endsWith(".html")) {
await cache.delete(currentCacheKey);
} else {
requests = requests.filter(request => request != currentCacheKey.url);
}
}
// Add remaining items from given requests to cache
try {
await cache.addAll(requests);
} catch (e) {
// Firefox seems to have network issues sometimes when adding files to cache
// We don't want to completely destroy service worker installation because one file fails though
}
}
/**
* Put given request and response pair into cache
*
* We only want to cache GET requests for static data,
* so some qualifications are checked here
*
* @param request Request to store in cache
* @param response Response to request in cache
*/
public async put(
request: Request,
response: Response | undefined
): Promise<Response | undefined> {
if (request.method == "GET"
&& !request.url.includes("/analytics")
&& response != undefined
) {
const cache: Cache = await this.getCache();
await cache.put(request, response.clone());
}
return response;
}
/**
* Delete all caches that don't match the generated cache ID
*/
public async deletePreviousCaching(): Promise<void> {
const cacheNames: string[] = await caches.keys();
for (const cacheName of cacheNames) {
if (cacheName != this.cacheId) {
await caches.delete(cacheName);
}
}
}
/**
* Return response from cache that matches
*
* @param request Request to check cache for
*/
public async match(request: RequestInfo): Promise<Response | undefined> {
const cache = await this.getCache();
return await cache.match(request);
}
/**
* Fetch a response from cache, with fallbacks for cache miss or error
*
* @param request Request being searched for in the cache
* @param cacheMiss What to do when the request is not in cache
* @param cacheFail What to do when an error is thrown
*/
public async fetchOrFallback(
request: Request,
cacheMiss: (request: Request) => Promise<Response | undefined>,
cacheFail: () => Promise<Response | undefined>
): Promise<Response | undefined> {
const cache = await this.getCache();
return cache.match(request)
.then((response: Response | undefined) => response || cacheMiss(request))
.catch(() => cacheFail());
}
/**
* Get an instance of the cache with the provided ID for this instance
*/
private getCache(): Promise<Cache> {
return caches.open(this.cacheId);
}
}
Loading…
Cancel
Save