@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/lib/manifest.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
import { extname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// catalog of build's filesystem output
|
|
5
|
+
export type BuildManifest = {
|
|
6
|
+
buildTag: string
|
|
7
|
+
files: Array<string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// functional data for service worker
|
|
11
|
+
export type CacheManifest = {
|
|
12
|
+
apiRoutes: Array<string>
|
|
13
|
+
buildTag: string
|
|
14
|
+
files: Array<string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function writeBuildManifest(buildTag: string, files: Set<string>) {
|
|
18
|
+
await writeJsonToBuildDir('manifest.json', {
|
|
19
|
+
buildTag,
|
|
20
|
+
files: Array.from(files).map(f =>
|
|
21
|
+
extname(f).length
|
|
22
|
+
? f
|
|
23
|
+
: f === '/'
|
|
24
|
+
? '/index.html'
|
|
25
|
+
: f + '/index.html',
|
|
26
|
+
),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeJsonToBuildDir(
|
|
31
|
+
filename: `${string}.json`,
|
|
32
|
+
json: any,
|
|
33
|
+
) {
|
|
34
|
+
await writeFile(join('./build', filename), JSON.stringify(json, null, 4))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function writeMetafile(filename: `${string}.json`, json: any) {
|
|
38
|
+
await writeJsonToBuildDir(
|
|
39
|
+
join('metafiles', filename) as `${string}.json`,
|
|
40
|
+
json,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function writeCacheManifest(buildTag: string, files: Set<string>) {
|
|
45
|
+
await writeJsonToBuildDir('cache.json', {
|
|
46
|
+
apiRoutes: [],
|
|
47
|
+
buildTag,
|
|
48
|
+
files: Array.from(files).map(filenameToWebappPath),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// drops index.html from path
|
|
53
|
+
function filenameToWebappPath(p: string): string {
|
|
54
|
+
if (p === '/index.html') {
|
|
55
|
+
return '/'
|
|
56
|
+
} else if (p.endsWith('/index.html')) {
|
|
57
|
+
return p.substring(0, p.length - '/index.html'.length) as `/${string}`
|
|
58
|
+
} else {
|
|
59
|
+
return p
|
|
60
|
+
}
|
|
61
|
+
}
|
package/lib/public.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export async function copyAssets(
|
|
5
|
+
outRoot: string,
|
|
6
|
+
): Promise<Array<string> | null> {
|
|
7
|
+
try {
|
|
8
|
+
const stats = await stat('public')
|
|
9
|
+
if (stats.isDirectory()) {
|
|
10
|
+
await mkdir(outRoot, { recursive: true })
|
|
11
|
+
return await recursiveCopyAssets(outRoot)
|
|
12
|
+
} else {
|
|
13
|
+
throw Error('./public cannot be a file')
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function recursiveCopyAssets(
|
|
21
|
+
outRoot: string,
|
|
22
|
+
dir: string = '',
|
|
23
|
+
): Promise<Array<string>> {
|
|
24
|
+
const copied: Array<string> = []
|
|
25
|
+
const to = join(outRoot, dir)
|
|
26
|
+
let madeDir = dir === ''
|
|
27
|
+
for (const p of await readdir(join('public', dir))) {
|
|
28
|
+
try {
|
|
29
|
+
const stats = await stat(join('public', dir, p))
|
|
30
|
+
if (stats.isDirectory()) {
|
|
31
|
+
copied.push(
|
|
32
|
+
...(await recursiveCopyAssets(outRoot, join(dir, p))),
|
|
33
|
+
)
|
|
34
|
+
} else {
|
|
35
|
+
if (!madeDir) {
|
|
36
|
+
await mkdir(join(outRoot, dir))
|
|
37
|
+
madeDir = true
|
|
38
|
+
}
|
|
39
|
+
await copyFile(join('public', dir, p), join(to, p))
|
|
40
|
+
copied.push('/' + join(dir, p).replaceAll('\\', '/'))
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('stat error', e)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return copied
|
|
48
|
+
}
|
package/lib/serve.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { buildWebsite } from './build.ts'
|
|
4
|
+
import type { DankConfig } from './dank.ts'
|
|
5
|
+
import { createGlobalDefinitions } from './define.ts'
|
|
6
|
+
import { esbuildDevContext } from './esbuild.ts'
|
|
7
|
+
import { isPreviewBuild } from './flags.ts'
|
|
8
|
+
import { HtmlEntrypoint } from './html.ts'
|
|
9
|
+
import {
|
|
10
|
+
createBuiltDistFilesFetcher,
|
|
11
|
+
createLocalProxyFilesFetcher,
|
|
12
|
+
createWebServer,
|
|
13
|
+
type FrontendFetcher,
|
|
14
|
+
} from './http.ts'
|
|
15
|
+
import { copyAssets } from './public.ts'
|
|
16
|
+
|
|
17
|
+
const isPreview = isPreviewBuild()
|
|
18
|
+
|
|
19
|
+
// alternate port for --preview bc of service worker
|
|
20
|
+
const PORT = isPreview ? 4000 : 3000
|
|
21
|
+
|
|
22
|
+
// port for esbuild.serve
|
|
23
|
+
const ESBUILD_PORT = 2999
|
|
24
|
+
|
|
25
|
+
export async function serveWebsite(c: DankConfig): Promise<never> {
|
|
26
|
+
await rm('build', { force: true, recursive: true })
|
|
27
|
+
let frontend: FrontendFetcher
|
|
28
|
+
if (isPreview) {
|
|
29
|
+
const { dir, files } = await buildWebsite(c)
|
|
30
|
+
frontend = createBuiltDistFilesFetcher(dir, files)
|
|
31
|
+
} else {
|
|
32
|
+
const { port } = await startEsbuildWatch(c)
|
|
33
|
+
frontend = createLocalProxyFilesFetcher(port)
|
|
34
|
+
}
|
|
35
|
+
createWebServer(PORT, frontend).listen(PORT)
|
|
36
|
+
console.log(
|
|
37
|
+
isPreview ? 'preview' : 'dev server',
|
|
38
|
+
`is live at http://127.0.0.1:${PORT}`,
|
|
39
|
+
)
|
|
40
|
+
return new Promise(() => {})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function startEsbuildWatch(c: DankConfig): Promise<{ port: number }> {
|
|
44
|
+
const watchDir = join('build', 'watch')
|
|
45
|
+
await mkdir(watchDir, { recursive: true })
|
|
46
|
+
await copyAssets(watchDir)
|
|
47
|
+
|
|
48
|
+
const entryPointUrls: Set<string> = new Set()
|
|
49
|
+
const entryPoints: Array<{ in: string; out: string }> = []
|
|
50
|
+
|
|
51
|
+
await Promise.all(
|
|
52
|
+
Object.entries(c.pages).map(async ([url, srcPath]) => {
|
|
53
|
+
const html = await HtmlEntrypoint.readFrom(
|
|
54
|
+
url,
|
|
55
|
+
join('pages', srcPath),
|
|
56
|
+
)
|
|
57
|
+
await html.injectPartials()
|
|
58
|
+
if (url !== '/') {
|
|
59
|
+
await mkdir(join(watchDir, url), { recursive: true })
|
|
60
|
+
}
|
|
61
|
+
html.collectScripts()
|
|
62
|
+
.filter(scriptImport => !entryPointUrls.has(scriptImport.in))
|
|
63
|
+
.forEach(scriptImport => {
|
|
64
|
+
entryPointUrls.add(scriptImport.in)
|
|
65
|
+
entryPoints.push({
|
|
66
|
+
in: scriptImport.in,
|
|
67
|
+
out: scriptImport.out,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
html.rewriteHrefs()
|
|
71
|
+
await html.writeTo(watchDir)
|
|
72
|
+
return html
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.log(entryPoints)
|
|
77
|
+
|
|
78
|
+
const ctx = await esbuildDevContext(
|
|
79
|
+
createGlobalDefinitions(),
|
|
80
|
+
entryPoints,
|
|
81
|
+
watchDir,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
await ctx.watch()
|
|
85
|
+
|
|
86
|
+
await ctx.serve({
|
|
87
|
+
host: '127.0.0.1',
|
|
88
|
+
port: ESBUILD_PORT,
|
|
89
|
+
servedir: watchDir,
|
|
90
|
+
cors: {
|
|
91
|
+
origin: 'http://127.0.0.1:' + PORT,
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return { port: ESBUILD_PORT }
|
|
96
|
+
}
|
package/lib/tag.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { isProductionBuild } from './flags.ts'
|
|
3
|
+
|
|
4
|
+
export async function createBuildTag(): Promise<string> {
|
|
5
|
+
const now = new Date()
|
|
6
|
+
const ms =
|
|
7
|
+
now.getUTCMilliseconds() +
|
|
8
|
+
now.getUTCSeconds() * 1000 +
|
|
9
|
+
now.getUTCMinutes() * 1000 * 60 +
|
|
10
|
+
now.getUTCHours() * 1000 * 60 * 60
|
|
11
|
+
const date = now.toISOString().substring(0, 10)
|
|
12
|
+
const time = String(ms).padStart(8, '0')
|
|
13
|
+
const when = `${date}-${time}`
|
|
14
|
+
if (isProductionBuild()) {
|
|
15
|
+
const gitHash = await new Promise((res, rej) =>
|
|
16
|
+
exec('git rev-parse --short HEAD', (err, stdout) => {
|
|
17
|
+
if (err) rej(err)
|
|
18
|
+
res(stdout.trim())
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
return `${when}-${gitHash}`
|
|
22
|
+
} else {
|
|
23
|
+
return when
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib_js/bin.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildWebsite } from "./build.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { serveWebsite } from "./serve.js";
|
|
5
|
+
function printHelp(task) {
|
|
6
|
+
if (!task || task === 'build') {
|
|
7
|
+
console.log('dank build [--minify] [--production]');
|
|
8
|
+
}
|
|
9
|
+
if (!task || task === 'serve') {
|
|
10
|
+
console.log(
|
|
11
|
+
// 'dank serve [--minify] [--preview] [--production]',
|
|
12
|
+
'dank serve [--minify] [--production]');
|
|
13
|
+
}
|
|
14
|
+
console.log('\nOPTIONS:');
|
|
15
|
+
console.log(' --minify minify sources');
|
|
16
|
+
// if (!task || task === 'serve') {
|
|
17
|
+
// console.log(' --preview pre-bundle and build ServiceWorker')
|
|
18
|
+
// }
|
|
19
|
+
console.log(' --production build for production release');
|
|
20
|
+
if (task) {
|
|
21
|
+
console.log();
|
|
22
|
+
console.log('use `dank -h` for details on all commands');
|
|
23
|
+
}
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const args = (function collectProgramArgs() {
|
|
27
|
+
const programNames = ['dank', 'bin.js', 'bin.ts'];
|
|
28
|
+
let args = [...process.argv];
|
|
29
|
+
while (true) {
|
|
30
|
+
const shifted = args.shift();
|
|
31
|
+
if (!shifted || programNames.some(name => shifted.endsWith(name))) {
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
const task = (function resolveTask() {
|
|
37
|
+
const showHelp = args.some(arg => arg === '-h' || arg === '--help');
|
|
38
|
+
const task = (() => {
|
|
39
|
+
while (true) {
|
|
40
|
+
const shifted = args.shift();
|
|
41
|
+
switch (shifted) {
|
|
42
|
+
case '-h':
|
|
43
|
+
case '--help':
|
|
44
|
+
break;
|
|
45
|
+
case 'build':
|
|
46
|
+
return 'build';
|
|
47
|
+
case 'dev':
|
|
48
|
+
case 'serve':
|
|
49
|
+
return 'serve';
|
|
50
|
+
default:
|
|
51
|
+
if (showHelp) {
|
|
52
|
+
printHelp();
|
|
53
|
+
}
|
|
54
|
+
else if (typeof shifted === 'undefined') {
|
|
55
|
+
printError('missing command');
|
|
56
|
+
printHelp();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
printError(shifted + " isn't a command");
|
|
60
|
+
printHelp();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
if (showHelp) {
|
|
66
|
+
printHelp(task);
|
|
67
|
+
}
|
|
68
|
+
return task;
|
|
69
|
+
})();
|
|
70
|
+
const c = await loadConfig();
|
|
71
|
+
try {
|
|
72
|
+
switch (task) {
|
|
73
|
+
case 'build':
|
|
74
|
+
await buildWebsite(c);
|
|
75
|
+
console.log(green('done'));
|
|
76
|
+
process.exit(0);
|
|
77
|
+
case 'serve':
|
|
78
|
+
await serveWebsite(c);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
errorExit(e);
|
|
83
|
+
}
|
|
84
|
+
function printError(e) {
|
|
85
|
+
if (e !== null) {
|
|
86
|
+
if (typeof e === 'string') {
|
|
87
|
+
console.error(red('error:'), e);
|
|
88
|
+
}
|
|
89
|
+
else if (e instanceof Error) {
|
|
90
|
+
console.error(red('error:'), e.message);
|
|
91
|
+
if (e.stack) {
|
|
92
|
+
console.error(e.stack);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function green(s) {
|
|
98
|
+
return `\u001b[32m${s}\u001b[0m`;
|
|
99
|
+
}
|
|
100
|
+
function red(s) {
|
|
101
|
+
return `\u001b[31m${s}\u001b[0m`;
|
|
102
|
+
}
|
|
103
|
+
function errorExit(e) {
|
|
104
|
+
printError(e);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
package/lib_js/build.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isProductionBuild, willMinify } from "./flags.js";
|
|
4
|
+
import { copyAssets } from "./public.js";
|
|
5
|
+
import { createBuildTag } from "./tag.js";
|
|
6
|
+
import { writeBuildManifest, writeMetafile } from "./manifest.js";
|
|
7
|
+
import { createGlobalDefinitions } from "./define.js";
|
|
8
|
+
import { HtmlEntrypoint } from "./html.js";
|
|
9
|
+
import { esbuildWebpages } from "./esbuild.js";
|
|
10
|
+
export async function buildWebsite(c) {
|
|
11
|
+
const buildDir = 'build';
|
|
12
|
+
const distDir = join(buildDir, 'dist');
|
|
13
|
+
const buildTag = await createBuildTag();
|
|
14
|
+
console.log(willMinify()
|
|
15
|
+
? isProductionBuild()
|
|
16
|
+
? 'minified production'
|
|
17
|
+
: 'minified'
|
|
18
|
+
: 'unminified', 'build', buildTag, 'building in ./build/dist');
|
|
19
|
+
await rm(buildDir, { recursive: true, force: true });
|
|
20
|
+
await mkdir(distDir, { recursive: true });
|
|
21
|
+
await mkdir(join(buildDir, 'metafiles'), { recursive: true });
|
|
22
|
+
const staticAssets = await copyAssets(distDir);
|
|
23
|
+
const buildUrls = [];
|
|
24
|
+
buildUrls.push(...(await buildWebpages(distDir, createGlobalDefinitions(), c.pages)));
|
|
25
|
+
if (staticAssets) {
|
|
26
|
+
buildUrls.push(...staticAssets);
|
|
27
|
+
}
|
|
28
|
+
const result = new Set(buildUrls);
|
|
29
|
+
await writeBuildManifest(buildTag, result);
|
|
30
|
+
return {
|
|
31
|
+
dir: buildDir,
|
|
32
|
+
files: result,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function buildWebpages(distDir, define, pages) {
|
|
36
|
+
const entryPointUrls = new Set();
|
|
37
|
+
const entryPoints = [];
|
|
38
|
+
const htmlEntrypoints = await Promise.all(Object.entries(pages).map(async ([urlPath, fsPath]) => {
|
|
39
|
+
const html = await HtmlEntrypoint.readFrom(urlPath, join('pages', fsPath));
|
|
40
|
+
await html.injectPartials();
|
|
41
|
+
if (urlPath !== '/') {
|
|
42
|
+
await mkdir(join(distDir, urlPath), { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
html.collectScripts()
|
|
45
|
+
.filter(scriptImport => !entryPointUrls.has(scriptImport.in))
|
|
46
|
+
.forEach(scriptImport => {
|
|
47
|
+
entryPointUrls.add(scriptImport.in);
|
|
48
|
+
entryPoints.push({
|
|
49
|
+
in: scriptImport.in,
|
|
50
|
+
out: scriptImport.out,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return html;
|
|
54
|
+
}));
|
|
55
|
+
const metafile = await esbuildWebpages(define, entryPoints, distDir);
|
|
56
|
+
await writeMetafile(`pages.json`, metafile);
|
|
57
|
+
// todo these hrefs would have \ path separators on windows
|
|
58
|
+
const buildUrls = [...Object.keys(pages)];
|
|
59
|
+
const mapInToOutHrefs = {};
|
|
60
|
+
for (const [outputFile, { entryPoint }] of Object.entries(metafile.outputs)) {
|
|
61
|
+
const outputUrl = outputFile.replace(/^build\/dist/, '');
|
|
62
|
+
buildUrls.push(outputUrl);
|
|
63
|
+
mapInToOutHrefs[entryPoint] = outputUrl;
|
|
64
|
+
}
|
|
65
|
+
await Promise.all(htmlEntrypoints.map(async (html) => {
|
|
66
|
+
html.rewriteHrefs(mapInToOutHrefs);
|
|
67
|
+
await html.writeTo(distDir);
|
|
68
|
+
}));
|
|
69
|
+
return buildUrls;
|
|
70
|
+
}
|
package/lib_js/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
10
|
+
const CFG_P = './dank.config.ts';
|
|
11
|
+
export async function loadConfig(path = CFG_P) {
|
|
12
|
+
const module = await import(__rewriteRelativeImportExtension(resolveConfigPath(path)));
|
|
13
|
+
return await module.default;
|
|
14
|
+
}
|
|
15
|
+
export function resolveConfigPath(path) {
|
|
16
|
+
if (isAbsolute(path)) {
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return resolve(process.cwd(), path);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib_js/dank.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function defineConfig(c) {
|
|
2
|
+
if (typeof c.pages === 'undefined' || Object.keys(c.pages).length === 0) {
|
|
3
|
+
throw Error('DankConfig.pages is required');
|
|
4
|
+
}
|
|
5
|
+
for (const [urlPath, htmlPath] of Object.entries(c.pages)) {
|
|
6
|
+
if (typeof htmlPath !== 'string' || !htmlPath.endsWith('.html')) {
|
|
7
|
+
throw Error(`DankConfig.pages['${urlPath}'] must configure an html file`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return c;
|
|
11
|
+
}
|
package/lib_js/define.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import esbuild, {} from 'esbuild';
|
|
2
|
+
import { willMinify } from "./flags.js";
|
|
3
|
+
const jsBuildOptions = {
|
|
4
|
+
bundle: true,
|
|
5
|
+
metafile: true,
|
|
6
|
+
minify: willMinify(),
|
|
7
|
+
platform: 'browser',
|
|
8
|
+
splitting: false,
|
|
9
|
+
treeShaking: true,
|
|
10
|
+
write: true,
|
|
11
|
+
};
|
|
12
|
+
const webpageBuildOptions = {
|
|
13
|
+
assetNames: 'assets/[name]-[hash]',
|
|
14
|
+
format: 'esm',
|
|
15
|
+
...jsBuildOptions,
|
|
16
|
+
};
|
|
17
|
+
export async function esbuildDevContext(define, entryPoints, outdir) {
|
|
18
|
+
return await esbuild.context({
|
|
19
|
+
define,
|
|
20
|
+
entryNames: '[dir]/[name]',
|
|
21
|
+
entryPoints,
|
|
22
|
+
outdir,
|
|
23
|
+
...webpageBuildOptions,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function esbuildWebpages(define, entryPoints, outdir) {
|
|
27
|
+
const buildResult = await esbuild.build({
|
|
28
|
+
define,
|
|
29
|
+
entryNames: '[dir]/[name]-[hash]',
|
|
30
|
+
entryPoints: removeEntryPointOutExt(entryPoints),
|
|
31
|
+
outdir,
|
|
32
|
+
...webpageBuildOptions,
|
|
33
|
+
});
|
|
34
|
+
esbuildResultChecks(buildResult);
|
|
35
|
+
return buildResult.metafile;
|
|
36
|
+
}
|
|
37
|
+
function removeEntryPointOutExt(entryPoints) {
|
|
38
|
+
return entryPoints.map(entryPoint => {
|
|
39
|
+
return {
|
|
40
|
+
in: entryPoint.in,
|
|
41
|
+
out: entryPoint.out.replace(/\.(tsx?|jsx?|css)$/, ''),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function esbuildResultChecks(buildResult) {
|
|
46
|
+
if (buildResult.errors.length) {
|
|
47
|
+
buildResult.errors.forEach(msg => esbuildPrintMessage(msg, 'warning'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (buildResult.warnings.length) {
|
|
51
|
+
buildResult.warnings.forEach(msg => esbuildPrintMessage(msg, 'warning'));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function esbuildPrintMessage(msg, category) {
|
|
55
|
+
const location = msg.location
|
|
56
|
+
? ` (${msg.location.file}L${msg.location.line}:${msg.location.column})`
|
|
57
|
+
: '';
|
|
58
|
+
console.error(`esbuild ${category}${location}:`, msg.text);
|
|
59
|
+
msg.notes.forEach(note => {
|
|
60
|
+
console.error(' ', note.text);
|
|
61
|
+
if (note.location)
|
|
62
|
+
console.error(' ', note.location);
|
|
63
|
+
});
|
|
64
|
+
}
|
package/lib_js/flags.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `dank serve` will pre-bundle and use service worker
|
|
2
|
+
export const isPreviewBuild = () => process.env.PREVIEW === 'true' || process.argv.includes('--preview');
|
|
3
|
+
// `dank build` will minify sources and append git release tag to build tag
|
|
4
|
+
// `dank serve` will pre-bundle with service worker and minify
|
|
5
|
+
export const isProductionBuild = () => process.env.PRODUCTION === 'true' || process.argv.includes('--production');
|
|
6
|
+
export const willMinify = () => isProductionBuild() ||
|
|
7
|
+
process.env.MINIFY === 'true' ||
|
|
8
|
+
process.argv.includes('--minify');
|
|
9
|
+
export const willTsc = () => isProductionBuild() ||
|
|
10
|
+
process.env.TSC === 'true' ||
|
|
11
|
+
process.argv.includes('--tsc');
|
package/lib_js/html.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { defaultTreeAdapter, parse, parseFragment, serialize, } from 'parse5';
|
|
4
|
+
// unenforced but necessary sequence:
|
|
5
|
+
// injectPartials
|
|
6
|
+
// collectScripts
|
|
7
|
+
// rewriteHrefs
|
|
8
|
+
// writeTo
|
|
9
|
+
export class HtmlEntrypoint {
|
|
10
|
+
static async readFrom(urlPath, fsPath) {
|
|
11
|
+
let html;
|
|
12
|
+
try {
|
|
13
|
+
html = await readFile(fsPath, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.log(`\u001b[31merror:\u001b[0m`, fsPath, 'does not exist');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return new HtmlEntrypoint(urlPath, html, fsPath);
|
|
20
|
+
}
|
|
21
|
+
#document;
|
|
22
|
+
#fsPath;
|
|
23
|
+
#partials = [];
|
|
24
|
+
#scripts = [];
|
|
25
|
+
#url;
|
|
26
|
+
constructor(url, html, fsPath) {
|
|
27
|
+
this.#url = url;
|
|
28
|
+
this.#document = parse(html);
|
|
29
|
+
this.#fsPath = fsPath;
|
|
30
|
+
}
|
|
31
|
+
async injectPartials() {
|
|
32
|
+
this.#collectPartials(this.#document);
|
|
33
|
+
await this.#injectPartials();
|
|
34
|
+
}
|
|
35
|
+
collectScripts() {
|
|
36
|
+
this.#collectScripts(this.#document);
|
|
37
|
+
return this.#scripts;
|
|
38
|
+
}
|
|
39
|
+
// rewrites hrefs to content hashed urls
|
|
40
|
+
// call without hrefs to rewrite tsx? ext to js
|
|
41
|
+
rewriteHrefs(hrefs) {
|
|
42
|
+
for (const importScript of this.#scripts) {
|
|
43
|
+
const rewriteTo = hrefs ? hrefs[importScript.in] : null;
|
|
44
|
+
if (importScript.type === 'script') {
|
|
45
|
+
if (importScript.in.endsWith('.tsx') ||
|
|
46
|
+
importScript.in.endsWith('.ts')) {
|
|
47
|
+
importScript.elem.attrs.find(attr => attr.name === 'src').value = rewriteTo || `/${importScript.out}.js`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (importScript.type === 'style') {
|
|
51
|
+
importScript.elem.attrs.find(attr => attr.name === 'href').value = rewriteTo || `/${importScript.out}.css`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async writeTo(buildDir) {
|
|
56
|
+
await writeFile(join(buildDir, this.#url, 'index.html'), serialize(this.#document));
|
|
57
|
+
}
|
|
58
|
+
async #injectPartials() {
|
|
59
|
+
for (const commentNode of this.#partials) {
|
|
60
|
+
const pp = commentNode.data
|
|
61
|
+
.match(/\{\{(?<pp>.+)\}\}/)
|
|
62
|
+
.groups.pp.trim();
|
|
63
|
+
const fragment = parseFragment(await readFile(pp, 'utf-8'));
|
|
64
|
+
for (const node of fragment.childNodes) {
|
|
65
|
+
if (node.nodeName === 'script') {
|
|
66
|
+
this.#rewritePathFromPartial(pp, node, 'src');
|
|
67
|
+
}
|
|
68
|
+
else if (node.nodeName === 'link' &&
|
|
69
|
+
hasAttr(node, 'rel', 'stylesheet')) {
|
|
70
|
+
this.#rewritePathFromPartial(pp, node, 'href');
|
|
71
|
+
}
|
|
72
|
+
defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
|
|
73
|
+
}
|
|
74
|
+
defaultTreeAdapter.detachNode(commentNode);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// rewrite a ts or css href relative to an html partial to be relative to the html entrypoint
|
|
78
|
+
#rewritePathFromPartial(pp, elem, attrName) {
|
|
79
|
+
const attr = getAttr(elem, attrName);
|
|
80
|
+
if (attr) {
|
|
81
|
+
attr.value = join(relative(dirname(this.#fsPath), dirname(pp)), attr.value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#collectPartials(node) {
|
|
85
|
+
for (const childNode of node.childNodes) {
|
|
86
|
+
if (childNode.nodeName === '#comment' && 'data' in childNode) {
|
|
87
|
+
if (/\{\{.+\}\}/.test(childNode.data)) {
|
|
88
|
+
this.#partials.push(childNode);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if ('childNodes' in childNode) {
|
|
92
|
+
this.#collectPartials(childNode);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
#collectScripts(node) {
|
|
97
|
+
for (const childNode of node.childNodes) {
|
|
98
|
+
if (childNode.nodeName === 'script') {
|
|
99
|
+
const srcAttr = childNode.attrs.find(attr => attr.name === 'src');
|
|
100
|
+
if (srcAttr) {
|
|
101
|
+
this.#addScript('script', srcAttr.value, childNode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (childNode.nodeName === 'link' &&
|
|
105
|
+
hasAttr(childNode, 'rel', 'stylesheet')) {
|
|
106
|
+
const hrefAttr = getAttr(childNode, 'href');
|
|
107
|
+
if (hrefAttr) {
|
|
108
|
+
this.#addScript('style', hrefAttr.value, childNode);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if ('childNodes' in childNode) {
|
|
112
|
+
this.#collectScripts(childNode);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
#addScript(type, href, elem) {
|
|
117
|
+
const inPath = join(dirname(this.#fsPath), href);
|
|
118
|
+
this.#scripts.push({
|
|
119
|
+
type,
|
|
120
|
+
href,
|
|
121
|
+
elem,
|
|
122
|
+
in: inPath,
|
|
123
|
+
out: inPath.replace(/^pages\//, ''),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function getAttr(elem, name) {
|
|
128
|
+
return elem.attrs.find(attr => attr.name === name);
|
|
129
|
+
}
|
|
130
|
+
function hasAttr(elem, name, value) {
|
|
131
|
+
return elem.attrs.some(attr => attr.name === name && attr.value === value);
|
|
132
|
+
}
|