@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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { isProductionBuild } from "./flags.js";
2
+ export function createGlobalDefinitions() {
3
+ const isProduction = isProductionBuild();
4
+ return {
5
+ 'dank.IS_DEV': JSON.stringify(!isProduction),
6
+ 'dank.IS_PROD': JSON.stringify(isProduction),
7
+ };
8
+ }
@@ -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
+ }
@@ -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
+ }