@eighty4/dank 0.0.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
package/lib/bin.ts ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { buildWebsite } from './build.ts'
4
+ import { loadConfig } from './config.ts'
5
+ import { serveWebsite } from './serve.ts'
6
+
7
+ function printHelp(task?: 'build' | 'serve'): never {
8
+ if (!task || task === 'build') {
9
+ console.log('dank build [--minify] [--production]')
10
+ }
11
+ if (!task || task === 'serve') {
12
+ console.log(
13
+ // 'dank serve [--minify] [--preview] [--production]',
14
+ 'dank serve [--minify] [--production]',
15
+ )
16
+ }
17
+ console.log('\nOPTIONS:')
18
+ console.log(' --minify minify sources')
19
+ // if (!task || task === 'serve') {
20
+ // console.log(' --preview pre-bundle and build ServiceWorker')
21
+ // }
22
+ console.log(' --production build for production release')
23
+ if (task) {
24
+ console.log()
25
+ console.log('use `dank -h` for details on all commands')
26
+ }
27
+ process.exit(1)
28
+ }
29
+
30
+ const args = (function collectProgramArgs(): Array<string> {
31
+ const programNames: Array<string> = ['dank', 'bin.js', 'bin.ts']
32
+ let args = [...process.argv]
33
+ while (true) {
34
+ const shifted = args.shift()
35
+ if (!shifted || programNames.some(name => shifted.endsWith(name))) {
36
+ return args
37
+ }
38
+ }
39
+ })()
40
+
41
+ const task: 'build' | 'serve' = (function resolveTask() {
42
+ const showHelp = args.some(arg => arg === '-h' || arg === '--help')
43
+ const task = (() => {
44
+ while (true) {
45
+ const shifted = args.shift()
46
+ switch (shifted) {
47
+ case '-h':
48
+ case '--help':
49
+ break
50
+ case 'build':
51
+ return 'build'
52
+ case 'dev':
53
+ case 'serve':
54
+ return 'serve'
55
+ default:
56
+ if (showHelp) {
57
+ printHelp()
58
+ } else if (typeof shifted === 'undefined') {
59
+ printError('missing command')
60
+ printHelp()
61
+ } else {
62
+ printError(shifted + " isn't a command")
63
+ printHelp()
64
+ }
65
+ }
66
+ }
67
+ })()
68
+ if (showHelp) {
69
+ printHelp(task)
70
+ }
71
+ return task
72
+ })()
73
+
74
+ const c = await loadConfig()
75
+
76
+ try {
77
+ switch (task) {
78
+ case 'build':
79
+ await buildWebsite(c)
80
+ console.log(green('done'))
81
+ process.exit(0)
82
+ case 'serve':
83
+ await serveWebsite(c)
84
+ }
85
+ } catch (e: unknown) {
86
+ errorExit(e)
87
+ }
88
+
89
+ function printError(e: unknown) {
90
+ if (e !== null) {
91
+ if (typeof e === 'string') {
92
+ console.error(red('error:'), e)
93
+ } else if (e instanceof Error) {
94
+ console.error(red('error:'), e.message)
95
+ if (e.stack) {
96
+ console.error(e.stack)
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ function green(s: string): string {
103
+ return `\u001b[32m${s}\u001b[0m`
104
+ }
105
+
106
+ function red(s: string): string {
107
+ return `\u001b[31m${s}\u001b[0m`
108
+ }
109
+
110
+ function errorExit(e?: unknown): never {
111
+ printError(e)
112
+ process.exit(1)
113
+ }
package/lib/build.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { mkdir, rm } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import type { DankConfig } from './dank.ts'
4
+ import { isProductionBuild, willMinify } from './flags.ts'
5
+ import { copyAssets } from './public.ts'
6
+ import { createBuildTag } from './tag.ts'
7
+ import { writeBuildManifest, writeMetafile } from './manifest.ts'
8
+ import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
9
+ import { HtmlEntrypoint } from './html.ts'
10
+ import { esbuildWebpages } from './esbuild.ts'
11
+
12
+ export type DankBuild = {
13
+ dir: string
14
+ files: Set<string>
15
+ }
16
+
17
+ export async function buildWebsite(c: DankConfig): Promise<DankBuild> {
18
+ const buildDir = 'build'
19
+ const distDir = join(buildDir, 'dist')
20
+ const buildTag = await createBuildTag()
21
+ console.log(
22
+ willMinify()
23
+ ? isProductionBuild()
24
+ ? 'minified production'
25
+ : 'minified'
26
+ : 'unminified',
27
+ 'build',
28
+ buildTag,
29
+ 'building in ./build/dist',
30
+ )
31
+ await rm(buildDir, { recursive: true, force: true })
32
+ await mkdir(distDir, { recursive: true })
33
+ await mkdir(join(buildDir, 'metafiles'), { recursive: true })
34
+ const staticAssets = await copyAssets(distDir)
35
+ const buildUrls: Array<string> = []
36
+ buildUrls.push(
37
+ ...(await buildWebpages(distDir, createGlobalDefinitions(), c.pages)),
38
+ )
39
+ if (staticAssets) {
40
+ buildUrls.push(...staticAssets)
41
+ }
42
+ const result = new Set(buildUrls)
43
+ await writeBuildManifest(buildTag, result)
44
+ return {
45
+ dir: buildDir,
46
+ files: result,
47
+ }
48
+ }
49
+
50
+ async function buildWebpages(
51
+ distDir: string,
52
+ define: DefineDankGlobal,
53
+ pages: Record<string, string>,
54
+ ): Promise<Array<string>> {
55
+ const entryPointUrls: Set<string> = new Set()
56
+ const entryPoints: Array<{ in: string; out: string }> = []
57
+ const htmlEntrypoints: Array<HtmlEntrypoint> = await Promise.all(
58
+ Object.entries(pages).map(async ([urlPath, fsPath]) => {
59
+ const html = await HtmlEntrypoint.readFrom(
60
+ urlPath,
61
+ join('pages', fsPath),
62
+ )
63
+ await html.injectPartials()
64
+ if (urlPath !== '/') {
65
+ await mkdir(join(distDir, urlPath), { recursive: true })
66
+ }
67
+ html.collectScripts()
68
+ .filter(scriptImport => !entryPointUrls.has(scriptImport.in))
69
+ .forEach(scriptImport => {
70
+ entryPointUrls.add(scriptImport.in)
71
+ entryPoints.push({
72
+ in: scriptImport.in,
73
+ out: scriptImport.out,
74
+ })
75
+ })
76
+ return html
77
+ }),
78
+ )
79
+ const metafile = await esbuildWebpages(define, entryPoints, distDir)
80
+ await writeMetafile(`pages.json`, metafile)
81
+ // todo these hrefs would have \ path separators on windows
82
+ const buildUrls = [...Object.keys(pages)]
83
+ const mapInToOutHrefs: Record<string, string> = {}
84
+ for (const [outputFile, { entryPoint }] of Object.entries(
85
+ metafile.outputs,
86
+ )) {
87
+ const outputUrl = outputFile.replace(/^build\/dist/, '')
88
+ buildUrls.push(outputUrl)
89
+ mapInToOutHrefs[entryPoint!] = outputUrl
90
+ }
91
+ await Promise.all(
92
+ htmlEntrypoints.map(async html => {
93
+ html.rewriteHrefs(mapInToOutHrefs)
94
+ await html.writeTo(distDir)
95
+ }),
96
+ )
97
+ return buildUrls
98
+ }
package/lib/config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { isAbsolute, resolve } from 'node:path'
2
+ import type { DankConfig } from './dank.ts'
3
+
4
+ const CFG_P = './dank.config.ts'
5
+
6
+ export async function loadConfig(path: string = CFG_P): Promise<DankConfig> {
7
+ const module = await import(resolveConfigPath(path))
8
+ return await module.default
9
+ }
10
+
11
+ export function resolveConfigPath(path: string): string {
12
+ if (isAbsolute(path)) {
13
+ return path
14
+ } else {
15
+ return resolve(process.cwd(), path)
16
+ }
17
+ }
package/lib/dank.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type DankConfig = {
2
+ // used for releases and service worker caching
3
+ // buildTag?: (() => Promise<string> | string) | string
4
+ // mapping url to fs paths of webpages to build
5
+ pages: Record<`/${string}`, `./${string}.html`>
6
+ }
7
+
8
+ export async function defineConfig(
9
+ c: Partial<DankConfig>,
10
+ ): Promise<DankConfig> {
11
+ if (typeof c.pages === 'undefined' || Object.keys(c.pages).length === 0) {
12
+ throw Error('DankConfig.pages is required')
13
+ }
14
+ for (const [urlPath, htmlPath] of Object.entries(c.pages)) {
15
+ if (typeof htmlPath !== 'string' || !htmlPath.endsWith('.html')) {
16
+ throw Error(
17
+ `DankConfig.pages['${urlPath}'] must configure an html file`,
18
+ )
19
+ }
20
+ }
21
+ return c as DankConfig
22
+ }
package/lib/define.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { isProductionBuild } from './flags.ts'
2
+
3
+ export type DankGlobal = {
4
+ IS_DEV: boolean
5
+ IS_PROD: boolean
6
+ }
7
+
8
+ type DefineDankGlobalKey = 'dank.IS_DEV' | 'dank.IS_PROD'
9
+
10
+ export type DefineDankGlobal = Record<DefineDankGlobalKey, string>
11
+
12
+ export function createGlobalDefinitions(): DefineDankGlobal {
13
+ const isProduction = isProductionBuild()
14
+ return {
15
+ 'dank.IS_DEV': JSON.stringify(!isProduction),
16
+ 'dank.IS_PROD': JSON.stringify(isProduction),
17
+ }
18
+ }
package/lib/esbuild.ts ADDED
@@ -0,0 +1,87 @@
1
+ import esbuild, {
2
+ type BuildContext,
3
+ type BuildOptions,
4
+ type BuildResult,
5
+ type Message,
6
+ type Metafile,
7
+ } from 'esbuild'
8
+ import type { DefineDankGlobal } from './define.ts'
9
+ import { willMinify } from './flags.ts'
10
+
11
+ const jsBuildOptions: BuildOptions & { metafile: true; write: true } = {
12
+ bundle: true,
13
+ metafile: true,
14
+ minify: willMinify(),
15
+ platform: 'browser',
16
+ splitting: false,
17
+ treeShaking: true,
18
+ write: true,
19
+ }
20
+
21
+ const webpageBuildOptions: BuildOptions & { metafile: true; write: true } = {
22
+ assetNames: 'assets/[name]-[hash]',
23
+ format: 'esm',
24
+ ...jsBuildOptions,
25
+ }
26
+
27
+ export async function esbuildDevContext(
28
+ define: DefineDankGlobal,
29
+ entryPoints: Array<{ in: string; out: string }>,
30
+ outdir: string,
31
+ ): Promise<BuildContext> {
32
+ return await esbuild.context({
33
+ define,
34
+ entryNames: '[dir]/[name]',
35
+ entryPoints,
36
+ outdir,
37
+ ...webpageBuildOptions,
38
+ })
39
+ }
40
+
41
+ export async function esbuildWebpages(
42
+ define: DefineDankGlobal,
43
+ entryPoints: Array<{ in: string; out: string }>,
44
+ outdir: string,
45
+ ): Promise<Metafile> {
46
+ const buildResult = await esbuild.build({
47
+ define,
48
+ entryNames: '[dir]/[name]-[hash]',
49
+ entryPoints: removeEntryPointOutExt(entryPoints),
50
+ outdir,
51
+ ...webpageBuildOptions,
52
+ })
53
+ esbuildResultChecks(buildResult)
54
+ return buildResult.metafile
55
+ }
56
+
57
+ function removeEntryPointOutExt(
58
+ entryPoints: Array<{ in: string; out: string }>,
59
+ ) {
60
+ return entryPoints.map(entryPoint => {
61
+ return {
62
+ in: entryPoint.in,
63
+ out: entryPoint.out.replace(/\.(tsx?|jsx?|css)$/, ''),
64
+ }
65
+ })
66
+ }
67
+
68
+ function esbuildResultChecks(buildResult: BuildResult) {
69
+ if (buildResult.errors.length) {
70
+ buildResult.errors.forEach(msg => esbuildPrintMessage(msg, 'warning'))
71
+ process.exit(1)
72
+ }
73
+ if (buildResult.warnings.length) {
74
+ buildResult.warnings.forEach(msg => esbuildPrintMessage(msg, 'warning'))
75
+ }
76
+ }
77
+
78
+ function esbuildPrintMessage(msg: Message, category: 'error' | 'warning') {
79
+ const location = msg.location
80
+ ? ` (${msg.location.file}L${msg.location.line}:${msg.location.column})`
81
+ : ''
82
+ console.error(`esbuild ${category}${location}:`, msg.text)
83
+ msg.notes.forEach(note => {
84
+ console.error(' ', note.text)
85
+ if (note.location) console.error(' ', note.location)
86
+ })
87
+ }
package/lib/flags.ts ADDED
@@ -0,0 +1,18 @@
1
+ // `dank serve` will pre-bundle and use service worker
2
+ export const isPreviewBuild = () =>
3
+ process.env.PREVIEW === 'true' || process.argv.includes('--preview')
4
+
5
+ // `dank build` will minify sources and append git release tag to build tag
6
+ // `dank serve` will pre-bundle with service worker and minify
7
+ export const isProductionBuild = () =>
8
+ process.env.PRODUCTION === 'true' || process.argv.includes('--production')
9
+
10
+ export const willMinify = () =>
11
+ isProductionBuild() ||
12
+ process.env.MINIFY === 'true' ||
13
+ process.argv.includes('--minify')
14
+
15
+ export const willTsc = () =>
16
+ isProductionBuild() ||
17
+ process.env.TSC === 'true' ||
18
+ process.argv.includes('--tsc')
package/lib/html.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import {
4
+ defaultTreeAdapter,
5
+ type DefaultTreeAdapterTypes,
6
+ parse,
7
+ parseFragment,
8
+ serialize,
9
+ } from 'parse5'
10
+
11
+ type CommentNode = DefaultTreeAdapterTypes.CommentNode
12
+ type Document = DefaultTreeAdapterTypes.Document
13
+ type Element = DefaultTreeAdapterTypes.Element
14
+ type ParentNode = DefaultTreeAdapterTypes.ParentNode
15
+
16
+ export type ImportedScript = {
17
+ type: 'script' | 'style'
18
+ href: string
19
+ elem: Element
20
+ in: string
21
+ out: string
22
+ }
23
+
24
+ // unenforced but necessary sequence:
25
+ // injectPartials
26
+ // collectScripts
27
+ // rewriteHrefs
28
+ // writeTo
29
+ export class HtmlEntrypoint {
30
+ static async readFrom(
31
+ urlPath: string,
32
+ fsPath: string,
33
+ ): Promise<HtmlEntrypoint> {
34
+ let html: string
35
+ try {
36
+ html = await readFile(fsPath, 'utf-8')
37
+ } catch (e) {
38
+ console.log(`\u001b[31merror:\u001b[0m`, fsPath, 'does not exist')
39
+ process.exit(1)
40
+ }
41
+ return new HtmlEntrypoint(urlPath, html, fsPath)
42
+ }
43
+
44
+ #document: Document
45
+ #fsPath: string
46
+ #partials: Array<CommentNode> = []
47
+ #scripts: Array<ImportedScript> = []
48
+ #url: string
49
+
50
+ constructor(url: string, html: string, fsPath: string) {
51
+ this.#url = url
52
+ this.#document = parse(html)
53
+ this.#fsPath = fsPath
54
+ }
55
+
56
+ async injectPartials() {
57
+ this.#collectPartials(this.#document)
58
+ await this.#injectPartials()
59
+ }
60
+
61
+ collectScripts(): Array<ImportedScript> {
62
+ this.#collectScripts(this.#document)
63
+ return this.#scripts
64
+ }
65
+
66
+ // rewrites hrefs to content hashed urls
67
+ // call without hrefs to rewrite tsx? ext to js
68
+ rewriteHrefs(hrefs?: Record<string, string>) {
69
+ for (const importScript of this.#scripts) {
70
+ const rewriteTo = hrefs ? hrefs[importScript.in] : null
71
+ if (importScript.type === 'script') {
72
+ if (
73
+ importScript.in.endsWith('.tsx') ||
74
+ importScript.in.endsWith('.ts')
75
+ ) {
76
+ importScript.elem.attrs.find(
77
+ attr => attr.name === 'src',
78
+ )!.value = rewriteTo || `/${importScript.out}.js`
79
+ }
80
+ } else if (importScript.type === 'style') {
81
+ importScript.elem.attrs.find(
82
+ attr => attr.name === 'href',
83
+ )!.value = rewriteTo || `/${importScript.out}.css`
84
+ }
85
+ }
86
+ }
87
+
88
+ async writeTo(buildDir: string): Promise<void> {
89
+ await writeFile(
90
+ join(buildDir, this.#url, 'index.html'),
91
+ serialize(this.#document),
92
+ )
93
+ }
94
+
95
+ async #injectPartials() {
96
+ for (const commentNode of this.#partials) {
97
+ const pp = commentNode.data
98
+ .match(/\{\{(?<pp>.+)\}\}/)!
99
+ .groups!.pp.trim()
100
+ const fragment = parseFragment(await readFile(pp, 'utf-8'))
101
+ for (const node of fragment.childNodes) {
102
+ if (node.nodeName === 'script') {
103
+ this.#rewritePathFromPartial(pp, node, 'src')
104
+ } else if (
105
+ node.nodeName === 'link' &&
106
+ hasAttr(node, 'rel', 'stylesheet')
107
+ ) {
108
+ this.#rewritePathFromPartial(pp, node, 'href')
109
+ }
110
+ defaultTreeAdapter.insertBefore(
111
+ commentNode.parentNode!,
112
+ node,
113
+ commentNode,
114
+ )
115
+ }
116
+ defaultTreeAdapter.detachNode(commentNode)
117
+ }
118
+ }
119
+
120
+ // rewrite a ts or css href relative to an html partial to be relative to the html entrypoint
121
+ #rewritePathFromPartial(
122
+ pp: string,
123
+ elem: Element,
124
+ attrName: 'href' | 'src',
125
+ ) {
126
+ const attr = getAttr(elem, attrName)
127
+ if (attr) {
128
+ attr.value = join(
129
+ relative(dirname(this.#fsPath), dirname(pp)),
130
+ attr.value,
131
+ )
132
+ }
133
+ }
134
+
135
+ #collectPartials(node: ParentNode) {
136
+ for (const childNode of node.childNodes) {
137
+ if (childNode.nodeName === '#comment' && 'data' in childNode) {
138
+ if (/\{\{.+\}\}/.test(childNode.data)) {
139
+ this.#partials.push(childNode)
140
+ }
141
+ } else if ('childNodes' in childNode) {
142
+ this.#collectPartials(childNode)
143
+ }
144
+ }
145
+ }
146
+
147
+ #collectScripts(node: ParentNode) {
148
+ for (const childNode of node.childNodes) {
149
+ if (childNode.nodeName === 'script') {
150
+ const srcAttr = childNode.attrs.find(
151
+ attr => attr.name === 'src',
152
+ )
153
+ if (srcAttr) {
154
+ this.#addScript('script', srcAttr.value, childNode)
155
+ }
156
+ } else if (
157
+ childNode.nodeName === 'link' &&
158
+ hasAttr(childNode, 'rel', 'stylesheet')
159
+ ) {
160
+ const hrefAttr = getAttr(childNode, 'href')
161
+ if (hrefAttr) {
162
+ this.#addScript('style', hrefAttr.value, childNode)
163
+ }
164
+ } else if ('childNodes' in childNode) {
165
+ this.#collectScripts(childNode)
166
+ }
167
+ }
168
+ }
169
+
170
+ #addScript(type: ImportedScript['type'], href: string, elem: Element) {
171
+ const inPath = join(dirname(this.#fsPath), href)
172
+ this.#scripts.push({
173
+ type,
174
+ href,
175
+ elem,
176
+ in: inPath,
177
+ out: inPath.replace(/^pages\//, ''),
178
+ })
179
+ }
180
+ }
181
+
182
+ function getAttr(elem: Element, name: string) {
183
+ return elem.attrs.find(attr => attr.name === name)
184
+ }
185
+
186
+ function hasAttr(elem: Element, name: string, value: string): boolean {
187
+ return elem.attrs.some(attr => attr.name === name && attr.value === value)
188
+ }
package/lib/http.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { createReadStream } from 'node:fs'
2
+ import {
3
+ createServer,
4
+ type IncomingHttpHeaders,
5
+ type IncomingMessage,
6
+ type OutgoingHttpHeaders,
7
+ type ServerResponse,
8
+ } from 'node:http'
9
+ import { extname, join as fsJoin } from 'node:path'
10
+ import { isProductionBuild } from './flags.ts'
11
+
12
+ export type FrontendFetcher = (
13
+ url: URL,
14
+ headers: Headers,
15
+ res: ServerResponse,
16
+ ) => void
17
+
18
+ export function createWebServer(
19
+ port: number,
20
+ frontendFetcher: FrontendFetcher,
21
+ ): ReturnType<typeof createServer> {
22
+ const serverAddress = 'http://localhost:' + port
23
+ return createServer((req: IncomingMessage, res: ServerResponse) => {
24
+ if (!req.url || !req.method) {
25
+ res.end()
26
+ } else {
27
+ const url = new URL(serverAddress + req.url)
28
+ if (req.method !== 'GET') {
29
+ res.writeHead(405)
30
+ res.end()
31
+ } else {
32
+ frontendFetcher(url, convertHeadersToFetch(req.headers), res)
33
+ }
34
+ }
35
+ })
36
+ }
37
+
38
+ export function createBuiltDistFilesFetcher(
39
+ dir: string,
40
+ files: Set<string>,
41
+ ): FrontendFetcher {
42
+ return (url: URL, _headers: Headers, res: ServerResponse) => {
43
+ if (!files.has(url.pathname)) {
44
+ res.writeHead(404)
45
+ res.end()
46
+ } else {
47
+ const mimeType = resolveMimeType(url)
48
+ res.setHeader('Content-Type', mimeType)
49
+ const reading = createReadStream(
50
+ mimeType === 'text/html'
51
+ ? fsJoin(dir, url.pathname, 'index.html')
52
+ : fsJoin(dir, url.pathname),
53
+ )
54
+ reading.pipe(res)
55
+ reading.on('error', err => {
56
+ console.error(
57
+ `${url.pathname} file read ${reading.path} error ${err.message}`,
58
+ )
59
+ res.statusCode = 500
60
+ res.end()
61
+ })
62
+ }
63
+ }
64
+ }
65
+
66
+ export function createLocalProxyFilesFetcher(port: number): FrontendFetcher {
67
+ const proxyAddress = 'http://127.0.0.1:' + port
68
+ return (url: URL, _headers: Headers, res: ServerResponse) => {
69
+ fetch(proxyAddress + url.pathname).then(fetchResponse => {
70
+ res.writeHead(
71
+ fetchResponse.status,
72
+ convertHeadersFromFetch(fetchResponse.headers),
73
+ )
74
+ fetchResponse.bytes().then(data => res.end(data))
75
+ })
76
+ }
77
+ }
78
+
79
+ function resolveMimeType(url: URL): string {
80
+ switch (extname(url.pathname)) {
81
+ case '':
82
+ return 'text/html'
83
+ case '.js':
84
+ return 'text/javascript'
85
+ case '.json':
86
+ return 'application/json'
87
+ case '.css':
88
+ return 'text/css'
89
+ case '.svg':
90
+ return 'image/svg+xml'
91
+ case '.png':
92
+ return 'image/png'
93
+ default:
94
+ console.warn('? mime type for', url.pathname)
95
+ if (!isProductionBuild()) process.exit(1)
96
+ return 'application/octet-stream'
97
+ }
98
+ }
99
+
100
+ function convertHeadersFromFetch(from: Headers): OutgoingHttpHeaders {
101
+ const to: OutgoingHttpHeaders = {}
102
+ for (const name of from.keys()) {
103
+ to[name] = from.get(name)!
104
+ }
105
+ return to
106
+ }
107
+
108
+ function convertHeadersToFetch(from: IncomingHttpHeaders): Headers {
109
+ const to = new Headers()
110
+ for (const [name, values] of Object.entries(from)) {
111
+ if (Array.isArray(values)) {
112
+ for (const value of values) to.append(name, value)
113
+ } else if (values) {
114
+ to.set(name, values)
115
+ }
116
+ }
117
+ return to
118
+ }