@eighty4/dank 0.0.4-1 → 0.0.4-2
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/client/esbuild.js +1 -91
- package/lib/bin.ts +1 -1
- package/lib/build.ts +2 -4
- package/lib/config.ts +211 -5
- package/lib/dank.ts +14 -150
- package/lib/developer.ts +117 -0
- package/lib/flags.ts +1 -1
- package/lib/serve.ts +93 -10
- package/lib_js/bin.js +80 -84
- package/lib_js/build.js +72 -81
- package/lib_js/build_tag.js +20 -21
- package/lib_js/config.js +158 -18
- package/lib_js/dank.js +5 -122
- package/lib_js/define.js +8 -5
- package/lib_js/esbuild.js +156 -166
- package/lib_js/flags.js +115 -119
- package/lib_js/html.js +214 -228
- package/lib_js/http.js +174 -193
- package/lib_js/metadata.js +181 -201
- package/lib_js/public.js +45 -46
- package/lib_js/serve.js +212 -230
- package/lib_js/services.js +152 -171
- package/lib_types/dank.d.ts +8 -1
- package/package.json +5 -1
package/lib_js/html.js
CHANGED
|
@@ -1,253 +1,239 @@
|
|
|
1
|
-
import EventEmitter from
|
|
2
|
-
import { readFile } from
|
|
3
|
-
import { dirname, join, relative } from
|
|
4
|
-
import { extname } from
|
|
5
|
-
import { defaultTreeAdapter, parse, parseFragment, serialize
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { extname } from "node:path/posix";
|
|
5
|
+
import { defaultTreeAdapter, parse, parseFragment, serialize } from "parse5";
|
|
6
|
+
class HtmlEntrypoint extends EventEmitter {
|
|
7
|
+
#build;
|
|
8
|
+
#decorations;
|
|
9
|
+
#document = defaultTreeAdapter.createDocument();
|
|
10
|
+
// todo cache entrypoints set for quicker diffing
|
|
11
|
+
// #entrypoints: Set<string> = new Set()
|
|
12
|
+
// path within pages dir omitting pages/ segment
|
|
13
|
+
#fsPath;
|
|
14
|
+
#partials = [];
|
|
15
|
+
#resolver;
|
|
16
|
+
#scripts = [];
|
|
17
|
+
#update = Object();
|
|
18
|
+
#url;
|
|
19
|
+
constructor(build, resolver, url, fsPath, decorations) {
|
|
20
|
+
super({ captureRejections: true });
|
|
21
|
+
this.#build = build;
|
|
22
|
+
this.#resolver = resolver;
|
|
23
|
+
this.#decorations = decorations;
|
|
24
|
+
this.#url = url;
|
|
25
|
+
this.#fsPath = fsPath;
|
|
26
|
+
this.on("change", this.#onChange);
|
|
27
|
+
this.emit("change");
|
|
28
|
+
}
|
|
29
|
+
get fsPath() {
|
|
30
|
+
return this.#fsPath;
|
|
31
|
+
}
|
|
32
|
+
get url() {
|
|
33
|
+
return this.#url;
|
|
34
|
+
}
|
|
35
|
+
async #html() {
|
|
36
|
+
try {
|
|
37
|
+
return await readFile(join(this.#build.dirs.pages, this.#fsPath), "utf8");
|
|
38
|
+
} catch (e) {
|
|
39
|
+
errorExit(this.#fsPath + " does not exist");
|
|
28
40
|
}
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
}
|
|
42
|
+
// todo if partial changes, hot swap content in page
|
|
43
|
+
#onChange = async (_partial) => {
|
|
44
|
+
const update = this.#update = Object();
|
|
45
|
+
const html = await this.#html();
|
|
46
|
+
const document = parse(html);
|
|
47
|
+
const imports = {
|
|
48
|
+
partials: [],
|
|
49
|
+
scripts: []
|
|
50
|
+
};
|
|
51
|
+
this.#collectImports(document, imports);
|
|
52
|
+
const partials = await this.#resolvePartialContent(imports.partials);
|
|
53
|
+
if (update !== this.#update) {
|
|
54
|
+
return;
|
|
31
55
|
}
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
this.#addDecorations(document);
|
|
57
|
+
this.#update = update;
|
|
58
|
+
this.#document = document;
|
|
59
|
+
this.#partials = partials;
|
|
60
|
+
this.#scripts = imports.scripts;
|
|
61
|
+
const entrypoints = mergeEntrypoints(imports, ...partials.map((p) => p.imports));
|
|
62
|
+
this.emit("entrypoints", entrypoints);
|
|
63
|
+
this.emit("partials", this.#partials.map((p) => p.fsPath));
|
|
64
|
+
if (this.listenerCount("output")) {
|
|
65
|
+
this.emit("output", this.output());
|
|
34
66
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
};
|
|
68
|
+
// Emits `partial` on detecting a partial reference for `dank serve` file watches
|
|
69
|
+
// to respond to dependent changes
|
|
70
|
+
// todo safeguard recursive partials that cause circular imports
|
|
71
|
+
async #resolvePartialContent(partials) {
|
|
72
|
+
return await Promise.all(partials.map(async (p) => {
|
|
73
|
+
this.emit("partial", p.fsPath);
|
|
74
|
+
const html = await readFile(join(this.#build.dirs.pages, p.fsPath), "utf8");
|
|
75
|
+
const fragment = parseFragment(html);
|
|
76
|
+
const imports = {
|
|
77
|
+
partials: [],
|
|
78
|
+
scripts: []
|
|
79
|
+
};
|
|
80
|
+
this.#collectImports(fragment, imports, (node) => {
|
|
81
|
+
this.#rewritePartialRelativePaths(node, p.fsPath);
|
|
82
|
+
});
|
|
83
|
+
if (imports.partials.length) {
|
|
84
|
+
errorExit(`partial ${p.fsPath} cannot recursively import partials`);
|
|
85
|
+
}
|
|
86
|
+
const content = {
|
|
87
|
+
...p,
|
|
88
|
+
fragment,
|
|
89
|
+
imports
|
|
90
|
+
};
|
|
91
|
+
return content;
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
// rewrite hrefs in a partial to be relative to the html entrypoint instead of the partial
|
|
95
|
+
#rewritePartialRelativePaths(elem, partialPath) {
|
|
96
|
+
let rewritePath = null;
|
|
97
|
+
if (elem.nodeName === "script") {
|
|
98
|
+
rewritePath = "src";
|
|
99
|
+
} else if (elem.nodeName === "link" && hasAttr(elem, "rel", "stylesheet")) {
|
|
100
|
+
rewritePath = "href";
|
|
43
101
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const imports = {
|
|
50
|
-
partials: [],
|
|
51
|
-
scripts: [],
|
|
52
|
-
};
|
|
53
|
-
this.#collectImports(document, imports);
|
|
54
|
-
const partials = await this.#resolvePartialContent(imports.partials);
|
|
55
|
-
if (update !== this.#update) {
|
|
56
|
-
// another update has started so aborting this one
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
this.#addDecorations(document);
|
|
60
|
-
this.#update = update;
|
|
61
|
-
this.#document = document;
|
|
62
|
-
this.#partials = partials;
|
|
63
|
-
this.#scripts = imports.scripts;
|
|
64
|
-
const entrypoints = mergeEntrypoints(imports, ...partials.map(p => p.imports));
|
|
65
|
-
// this.#entrypoints = new Set(entrypoints.map(entrypoint => entrypoint.in))
|
|
66
|
-
this.emit('entrypoints', entrypoints);
|
|
67
|
-
this.emit('partials', this.#partials.map(p => p.fsPath));
|
|
68
|
-
if (this.listenerCount('output')) {
|
|
69
|
-
this.emit('output', this.output());
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
// Emits `partial` on detecting a partial reference for `dank serve` file watches
|
|
73
|
-
// to respond to dependent changes
|
|
74
|
-
// todo safeguard recursive partials that cause circular imports
|
|
75
|
-
async #resolvePartialContent(partials) {
|
|
76
|
-
return await Promise.all(partials.map(async (p) => {
|
|
77
|
-
this.emit('partial', p.fsPath);
|
|
78
|
-
const html = await readFile(join(this.#build.dirs.pages, p.fsPath), 'utf8');
|
|
79
|
-
const fragment = parseFragment(html);
|
|
80
|
-
const imports = {
|
|
81
|
-
partials: [],
|
|
82
|
-
scripts: [],
|
|
83
|
-
};
|
|
84
|
-
this.#collectImports(fragment, imports, node => {
|
|
85
|
-
this.#rewritePartialRelativePaths(node, p.fsPath);
|
|
86
|
-
});
|
|
87
|
-
if (imports.partials.length) {
|
|
88
|
-
// todo recursive partials?
|
|
89
|
-
// await this.#resolvePartialContent(imports.partials)
|
|
90
|
-
errorExit(`partial ${p.fsPath} cannot recursively import partials`);
|
|
91
|
-
}
|
|
92
|
-
const content = {
|
|
93
|
-
...p,
|
|
94
|
-
fragment,
|
|
95
|
-
imports,
|
|
96
|
-
};
|
|
97
|
-
return content;
|
|
98
|
-
}));
|
|
102
|
+
if (rewritePath !== null) {
|
|
103
|
+
const attr = getAttr(elem, rewritePath);
|
|
104
|
+
if (attr) {
|
|
105
|
+
attr.value = join(relative(dirname(this.#fsPath), dirname(partialPath)), attr.value);
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
rewritePath = 'src';
|
|
105
|
-
}
|
|
106
|
-
else if (elem.nodeName === 'link' &&
|
|
107
|
-
hasAttr(elem, 'rel', 'stylesheet')) {
|
|
108
|
-
rewritePath = 'href';
|
|
109
|
-
}
|
|
110
|
-
if (rewritePath !== null) {
|
|
111
|
-
const attr = getAttr(elem, rewritePath);
|
|
112
|
-
if (attr) {
|
|
113
|
-
attr.value = join(relative(dirname(this.#fsPath), dirname(partialPath)), attr.value);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
108
|
+
}
|
|
109
|
+
#addDecorations(document) {
|
|
110
|
+
if (!this.#decorations?.length) {
|
|
111
|
+
return;
|
|
116
112
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const headNode = htmlNode.childNodes.find(node => node.nodeName === 'head');
|
|
127
|
-
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
113
|
+
for (const decoration of this.#decorations) {
|
|
114
|
+
switch (decoration.type) {
|
|
115
|
+
case "script":
|
|
116
|
+
const scriptNode = parseFragment(`<script type="module">${decoration.js}</script>`).childNodes[0];
|
|
117
|
+
const htmlNode = document.childNodes.find((node) => node.nodeName === "html");
|
|
118
|
+
const headNode = htmlNode.childNodes.find((node) => node.nodeName === "head");
|
|
119
|
+
defaultTreeAdapter.appendChild(headNode || htmlNode, scriptNode);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
131
122
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
}
|
|
124
|
+
output(hrefs) {
|
|
125
|
+
this.#injectPartials();
|
|
126
|
+
this.#rewriteHrefs(hrefs);
|
|
127
|
+
return serialize(this.#document);
|
|
128
|
+
}
|
|
129
|
+
// rewrites hrefs to content hashed urls
|
|
130
|
+
// call without hrefs to rewrite tsx? ext to js
|
|
131
|
+
#rewriteHrefs(hrefs) {
|
|
132
|
+
rewriteHrefs(this.#scripts, hrefs);
|
|
133
|
+
for (const partial of this.#partials) {
|
|
134
|
+
rewriteHrefs(partial.imports.scripts, hrefs);
|
|
136
135
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
}
|
|
137
|
+
async #injectPartials() {
|
|
138
|
+
for (const { commentNode, fragment } of this.#partials) {
|
|
139
|
+
if (!this.#build.production) {
|
|
140
|
+
defaultTreeAdapter.insertBefore(commentNode.parentNode, defaultTreeAdapter.createCommentNode(commentNode.data), commentNode);
|
|
141
|
+
}
|
|
142
|
+
for (const node of fragment.childNodes) {
|
|
143
|
+
defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
|
|
144
|
+
}
|
|
145
|
+
if (this.#build.production) {
|
|
146
|
+
defaultTreeAdapter.detachNode(commentNode);
|
|
147
|
+
}
|
|
144
148
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
}
|
|
150
|
+
#collectImports(node, collection, forEach) {
|
|
151
|
+
for (const childNode of node.childNodes) {
|
|
152
|
+
if (forEach && "tagName" in childNode) {
|
|
153
|
+
forEach(childNode);
|
|
154
|
+
}
|
|
155
|
+
if (childNode.nodeName === "#comment" && "data" in childNode) {
|
|
156
|
+
const partialMatch = childNode.data.match(/\{\{(?<pp>.+)\}\}/);
|
|
157
|
+
if (partialMatch) {
|
|
158
|
+
const partialSpecifier = partialMatch.groups.pp.trim();
|
|
159
|
+
if (partialSpecifier.startsWith("/")) {
|
|
160
|
+
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be an absolute path`);
|
|
161
|
+
}
|
|
162
|
+
const partialPath = join(dirname(this.#fsPath), partialSpecifier);
|
|
163
|
+
if (!this.#resolver.isPagesSubpathInPagesDir(partialPath)) {
|
|
164
|
+
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`);
|
|
165
|
+
}
|
|
166
|
+
collection.partials.push({
|
|
167
|
+
fsPath: partialPath,
|
|
168
|
+
commentNode: childNode
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
} else if (childNode.nodeName === "script") {
|
|
172
|
+
const srcAttr = childNode.attrs.find((attr) => attr.name === "src");
|
|
173
|
+
if (srcAttr) {
|
|
174
|
+
collection.scripts.push(this.#parseImport("script", srcAttr.value, childNode));
|
|
175
|
+
}
|
|
176
|
+
} else if (childNode.nodeName === "link" && hasAttr(childNode, "rel", "stylesheet")) {
|
|
177
|
+
const hrefAttr = getAttr(childNode, "href");
|
|
178
|
+
if (hrefAttr) {
|
|
179
|
+
collection.scripts.push(this.#parseImport("style", hrefAttr.value, childNode));
|
|
180
|
+
}
|
|
181
|
+
} else if ("childNodes" in childNode) {
|
|
182
|
+
this.#collectImports(childNode, collection);
|
|
183
|
+
}
|
|
157
184
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (childNode.nodeName === '#comment' && 'data' in childNode) {
|
|
164
|
-
const partialMatch = childNode.data.match(/\{\{(?<pp>.+)\}\}/);
|
|
165
|
-
if (partialMatch) {
|
|
166
|
-
const partialSpecifier = partialMatch.groups.pp.trim();
|
|
167
|
-
if (partialSpecifier.startsWith('/')) {
|
|
168
|
-
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be an absolute path`);
|
|
169
|
-
}
|
|
170
|
-
const partialPath = join(dirname(this.#fsPath), partialSpecifier);
|
|
171
|
-
if (!this.#resolver.isPagesSubpathInPagesDir(partialPath)) {
|
|
172
|
-
errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`);
|
|
173
|
-
}
|
|
174
|
-
collection.partials.push({
|
|
175
|
-
fsPath: partialPath,
|
|
176
|
-
commentNode: childNode,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
else if (childNode.nodeName === 'script') {
|
|
181
|
-
const srcAttr = childNode.attrs.find(attr => attr.name === 'src');
|
|
182
|
-
if (srcAttr) {
|
|
183
|
-
collection.scripts.push(this.#parseImport('script', srcAttr.value, childNode));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else if (childNode.nodeName === 'link' &&
|
|
187
|
-
hasAttr(childNode, 'rel', 'stylesheet')) {
|
|
188
|
-
const hrefAttr = getAttr(childNode, 'href');
|
|
189
|
-
if (hrefAttr) {
|
|
190
|
-
collection.scripts.push(this.#parseImport('style', hrefAttr.value, childNode));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else if ('childNodes' in childNode) {
|
|
194
|
-
this.#collectImports(childNode, collection);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
185
|
+
}
|
|
186
|
+
#parseImport(type, href, elem) {
|
|
187
|
+
const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href);
|
|
188
|
+
if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
|
|
189
|
+
errorExit(`href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`);
|
|
197
190
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
errorExit(`href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`);
|
|
202
|
-
}
|
|
203
|
-
let outPath = join(dirname(this.#fsPath), href);
|
|
204
|
-
if (type === 'script' && !outPath.endsWith('.js')) {
|
|
205
|
-
outPath = outPath.replace(new RegExp(extname(outPath).substring(1) + '$'), 'js');
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
type,
|
|
209
|
-
href,
|
|
210
|
-
elem,
|
|
211
|
-
entrypoint: {
|
|
212
|
-
in: inPath,
|
|
213
|
-
out: outPath,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
191
|
+
let outPath = join(dirname(this.#fsPath), href);
|
|
192
|
+
if (type === "script" && !outPath.endsWith(".js")) {
|
|
193
|
+
outPath = outPath.replace(new RegExp(extname(outPath).substring(1) + "$"), "js");
|
|
216
194
|
}
|
|
195
|
+
return {
|
|
196
|
+
type,
|
|
197
|
+
href,
|
|
198
|
+
elem,
|
|
199
|
+
entrypoint: {
|
|
200
|
+
in: inPath,
|
|
201
|
+
out: outPath
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
217
205
|
}
|
|
218
206
|
function getAttr(elem, name) {
|
|
219
|
-
|
|
207
|
+
return elem.attrs.find((attr) => attr.name === name);
|
|
220
208
|
}
|
|
221
209
|
function hasAttr(elem, name, value) {
|
|
222
|
-
|
|
210
|
+
return elem.attrs.some((attr) => attr.name === name && attr.value === value);
|
|
223
211
|
}
|
|
224
212
|
function mergeEntrypoints(...imports) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
213
|
+
const entrypoints = [];
|
|
214
|
+
for (const { scripts } of imports) {
|
|
215
|
+
for (const script of scripts) {
|
|
216
|
+
entrypoints.push(script.entrypoint);
|
|
230
217
|
}
|
|
231
|
-
|
|
218
|
+
}
|
|
219
|
+
return entrypoints;
|
|
232
220
|
}
|
|
233
221
|
function rewriteHrefs(scripts, hrefs) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
else if (type === 'style') {
|
|
244
|
-
elem.attrs.find(attr => attr.name === 'href').value =
|
|
245
|
-
rewriteTo || `/${entrypoint.out}`;
|
|
246
|
-
}
|
|
222
|
+
for (const { elem, entrypoint, type } of scripts) {
|
|
223
|
+
const rewriteTo = hrefs ? hrefs.mappedHref(entrypoint.in) : null;
|
|
224
|
+
if (type === "script") {
|
|
225
|
+
if (entrypoint.in.endsWith(".tsx") || entrypoint.in.endsWith(".ts")) {
|
|
226
|
+
elem.attrs.find((attr) => attr.name === "src").value = rewriteTo || `/${entrypoint.out}`;
|
|
227
|
+
}
|
|
228
|
+
} else if (type === "style") {
|
|
229
|
+
elem.attrs.find((attr) => attr.name === "href").value = rewriteTo || `/${entrypoint.out}`;
|
|
247
230
|
}
|
|
231
|
+
}
|
|
248
232
|
}
|
|
249
|
-
// todo evented error handling so HtmlEntrypoint can be unit tested
|
|
250
233
|
function errorExit(msg) {
|
|
251
|
-
|
|
252
|
-
|
|
234
|
+
console.log(`\x1B[31merror:\x1B[0m`, msg);
|
|
235
|
+
process.exit(1);
|
|
253
236
|
}
|
|
237
|
+
export {
|
|
238
|
+
HtmlEntrypoint
|
|
239
|
+
};
|