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