const NGX_VERSION = "0.2.0"; type ConfigNode = ConfigStatement | ConfigBlock | ConfigBreak | ConfigFile; class ConfigBuildable { build(): string { throw new Error("build(..) not implemented!"); } } class ConfigBlock extends ConfigBuildable { value: string; children: ConfigNode[]; constructor(value: string, children: ConfigNode[]) { super(); this.value = value; this.children = children; } override build(): string { let output = this.value; output += " {\n "; output += this.children .map((child) => child.build().split("\n").join("\n ")) .join("\n "); output += "\n}"; return output; } } class ConfigStatement extends ConfigBuildable { value: string; constructor(value: string) { super(); this.value = value; } override build(): string { return this.value + ";"; } } class ConfigBreak extends ConfigBuildable { override build(): string { return ""; } } class ConfigFile extends ConfigBuildable { nodes: ConfigNode[]; constructor(nodes: ConfigNode[]) { super(); this.nodes = nodes; } override build(): string { return this.nodes.map((n) => n.build()).join("\n"); } } type LooseConfigNode = ConfigNode | string | LooseConfigNode[]; function conform(looseNode: LooseConfigNode): ConfigNode[] { if (typeof looseNode === "string") { return [new ConfigStatement(looseNode)]; } if (typeof looseNode === "object" && looseNode instanceof Array) { if (looseNode.length === 0) { return [new ConfigBreak()]; } else if (looseNode.length === 1) { return conform(looseNode[0]); } else { return looseNode .map((n) => conform(n)) .reduceRight((b, a) => a.length === 1 ? [...a, ...b] : [...a, new ConfigBreak(), ...b], ); } } return [looseNode]; } export function ngx(value?: string, children?: LooseConfigNode[]): ConfigNode { const hasValue = value !== undefined && value !== ""; const hasChildren = children !== undefined; if (!hasValue && !hasChildren) { return new ConfigBreak(); } else if (hasValue && !hasChildren) { return new ConfigStatement(value); } else if (hasValue && hasChildren) { return new ConfigBlock(value, conform(children)); } else if (!hasValue && hasChildren) { return new ConfigFile(conform(children)); } throw new Error("unreachable"); } export const listen = (...extras: string[]) => conform([ `listen 443 ${["ssl", ...extras].join(" ")}`, `listen [::]:443 ${["ssl", ...extras].join(" ")}`, `http2 on`, ]); export const serverName = (name: string) => conform([`server_name ${name}`]); export const letsEncrypt = ( domain: string, liveDir = "/etc/letsencrypt/live", ) => conform([ `ssl_certificate ${liveDir}/${domain}/fullchain.pem`, `ssl_certificate_key ${liveDir}/${domain}/privkey.pem`, ]); // the default export is both the ngx function and a namespace: export default Object.assign( (value?: string, children?: LooseConfigNode[]) => ngx(value, children), { NGX_VERSION, listen, letsEncrypt, serverName }, );