char/ngx

git clone https://git.t4t.associates/char/ngx

Charlotte Sombump version257f894

main
4.8 KiB194 linesraw
1const NGX_VERSION = "0.2.2";
2
3type ConfigNode = ConfigStatement | ConfigBlock | ConfigBreak | ConfigFile;
4
5class ConfigBuildable {
6  build(): string {
7    throw new Error("build(..) not implemented!");
8  }
9}
10
11class ConfigBlock extends ConfigBuildable {
12  value: string;
13  children: ConfigNode[];
14
15  constructor(value: string, children: ConfigNode[]) {
16    super();
17
18    this.value = value;
19    this.children = children;
20  }
21
22  override build(): string {
23    let output = this.value;
24    output += " {\n  ";
25    output += this.children
26      .map((child) => child.build().split("\n").join("\n  "))
27      .join("\n  ");
28    output += "\n}";
29    return output;
30  }
31}
32
33class ConfigStatement extends ConfigBuildable {
34  value: string;
35  constructor(value: string) {
36    super();
37
38    this.value = value;
39  }
40
41  override build(): string {
42    return this.value + ";";
43  }
44}
45
46class ConfigBreak extends ConfigBuildable {
47  override build(): string {
48    return "";
49  }
50}
51
52class ConfigFile extends ConfigBuildable {
53  nodes: ConfigNode[];
54  constructor(nodes: ConfigNode[]) {
55    super();
56
57    this.nodes = nodes;
58  }
59
60  override build(): string {
61    return this.nodes.map((n) => n.build()).join("\n");
62  }
63}
64
65type LooseConfigNode = ConfigNode | string | LooseConfigNode[];
66
67function conform(looseNode: LooseConfigNode): ConfigNode[] {
68  if (typeof looseNode === "string") {
69    return [new ConfigStatement(looseNode)];
70  }
71  if (typeof looseNode === "object" && looseNode instanceof Array) {
72    if (looseNode.length === 0) {
73      return [new ConfigBreak()];
74    } else if (looseNode.length === 1) {
75      return conform(looseNode[0]);
76    } else {
77      return looseNode
78        .map((n) => conform(n))
79        .reduceRight((b, a) =>
80          a.length === 1 ? [...a, ...b] : [...a, new ConfigBreak(), ...b],
81        );
82    }
83  }
84  return [looseNode];
85}
86
87/**
88 * create nginx config nodes. behavior depends on arguments:
89 *
90 * - no args - ConfigBreak
91 * - value only - ConfigStatement
92 * - value + children - ConfigBlock
93 * - children only - ConfigFile
94 *
95 * accept strings, ConfigNodes, or nested arrays that flatten automatically.
96 *
97 * ```ts
98 * ngx() // ConfigBreak
99 * ngx("worker_processes auto") // ConfigStatement
100 * ngx("location /", ["proxy_pass http://backend"]) // ConfigBlock
101 * ngx(["server { ... }", "server { ... }"]) // ConfigFile
102 * ```
103 */
104export function ngx(value?: string, children?: LooseConfigNode[]): ConfigNode {
105  const hasValue = value !== undefined && value !== "";
106  const hasChildren = children !== undefined;
107
108  if (!hasValue && !hasChildren) {
109    return new ConfigBreak();
110  } else if (hasValue && !hasChildren) {
111    return new ConfigStatement(value);
112  } else if (hasValue && hasChildren) {
113    return new ConfigBlock(value, conform(children));
114  } else if (!hasValue && hasChildren) {
115    return new ConfigFile(conform(children));
116  }
117
118  throw new Error("unreachable");
119}
120
121/**
122 * create ssl listen directives for ipv4/ipv6 with http/2 enabled.
123 *
124 * extras modify the listen directives.
125 *
126 * ```ts
127 * listen() // listen 443 ssl, listen [::]:443 ssl, http2 on
128 * listen("default_server") // includes "default_server"
129 * ```
130 */
131export const listen = (...extras: string[]) =>
132  conform([
133    `listen 443 ${["ssl", ...extras].join(" ")}`,
134    `listen [::]:443 ${["ssl", ...extras].join(" ")}`,
135    `http2 on`,
136  ]);
137
138/**
139 * create http/3 (quic) listen directives (+ supporting alt-svc header).
140 */
141export const http3 = (
142  opts: {
143    /** max age for alt-svc header */ ma?: number;
144  } = {},
145) =>
146  conform([
147    "listen 443 quic reuseport",
148    "listen [::]:443 quic reuseport",
149    `add_header Alt-Svc 'h3=":443"; ma=${opts.ma ?? 86400}'`,
150  ]);
151
152/**
153 * create a server_name nginx directive.
154 *
155 * ```ts
156 * serverName("example.com") // "server_name example.com;"
157 * ```
158 */
159export const serverName = (name: string) =>
160  new ConfigStatement(`server_name ${name}`);
161
162/**
163 * create ssl certificate directives for let's encrypt certificates.
164 *
165 * ```ts
166 * letsEncrypt("example.com")
167 * // ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem
168 * // ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem
169 * ```
170 */
171export const letsEncrypt = (
172  domain: string,
173  liveDir = "/etc/letsencrypt/live",
174) =>
175  conform([
176    `ssl_certificate ${liveDir}/${domain}/fullchain.pem`,
177    `ssl_certificate_key ${liveDir}/${domain}/privkey.pem`,
178  ]);
179
180// the default export is both the ngx function and a namespace:
181/**
182 * use as a function to create config nodes, or access helpers.
183 *
184 * ```ts
185 * import ngx from './ngx';
186 * const config = ngx("location /", ["proxy_pass http://backend"]);
187 * console.log(ngx.NGX_VERSION);
188 * const ssl = ngx.letsEncrypt("example.com");
189 * ```
190 */
191export default Object.assign(
192  (value?: string, children?: LooseConfigNode[]) => ngx(value, children),
193  { NGX_VERSION, listen, letsEncrypt, serverName, http3 },
194);