@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 +0 -0
- package/lib/bin.ts +113 -0
- package/lib/build.ts +98 -0
- package/lib/config.ts +17 -0
- package/lib/dank.ts +22 -0
- package/lib/define.ts +18 -0
- package/lib/esbuild.ts +87 -0
- package/lib/flags.ts +18 -0
- package/lib/html.ts +188 -0
- package/lib/http.ts +118 -0
- package/lib/manifest.ts +61 -0
- package/lib/public.ts +48 -0
- package/lib/serve.ts +96 -0
- package/lib/tag.ts +25 -0
- package/lib_js/bin.js +106 -0
- package/lib_js/build.js +70 -0
- package/lib_js/config.js +22 -0
- package/lib_js/dank.js +11 -0
- package/lib_js/define.js +8 -0
- package/lib_js/esbuild.js +64 -0
- package/lib_js/flags.js +11 -0
- package/lib_js/html.js +132 -0
- package/lib_js/http.js +93 -0
- package/lib_js/manifest.js +37 -0
- package/lib_js/public.js +43 -0
- package/lib_js/serve.js +67 -0
- package/lib_js/tag.js +23 -0
- package/lib_types/dank.d.ts +4 -0
- package/package.json +33 -0
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
|
+
}
|