@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/lib_js/html.js CHANGED
@@ -1,253 +1,239 @@
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
- export 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');
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
- get fsPath() {
30
- return this.#fsPath;
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
- get url() {
33
- return this.#url;
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
- async #html() {
36
- try {
37
- return await readFile(join(this.#build.dirs.pages, this.#fsPath), 'utf8');
38
- }
39
- catch (e) {
40
- // todo error handling
41
- errorExit(this.#fsPath + ' does not exist');
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
- // todo if partial changes, hot swap content in page
45
- #onChange = async (_partial) => {
46
- const update = (this.#update = Object());
47
- const html = await this.#html();
48
- const document = parse(html);
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
- // rewrite hrefs in a partial to be relative to the html entrypoint instead of the partial
101
- #rewritePartialRelativePaths(elem, partialPath) {
102
- let rewritePath = null;
103
- if (elem.nodeName === 'script') {
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
- #addDecorations(document) {
118
- if (!this.#decorations?.length) {
119
- return;
120
- }
121
- for (const decoration of this.#decorations) {
122
- switch (decoration.type) {
123
- case 'script':
124
- const scriptNode = parseFragment(`<script type="module">${decoration.js}</script>`).childNodes[0];
125
- const htmlNode = document.childNodes.find(node => node.nodeName === 'html');
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
- output(hrefs) {
133
- this.#injectPartials();
134
- this.#rewriteHrefs(hrefs);
135
- return serialize(this.#document);
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
- // rewrites hrefs to content hashed urls
138
- // call without hrefs to rewrite tsx? ext to js
139
- #rewriteHrefs(hrefs) {
140
- rewriteHrefs(this.#scripts, hrefs);
141
- for (const partial of this.#partials) {
142
- rewriteHrefs(partial.imports.scripts, hrefs);
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
- async #injectPartials() {
146
- for (const { commentNode, fragment } of this.#partials) {
147
- if (!this.#build.production) {
148
- defaultTreeAdapter.insertBefore(commentNode.parentNode, defaultTreeAdapter.createCommentNode(commentNode.data), commentNode);
149
- }
150
- for (const node of fragment.childNodes) {
151
- defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
152
- }
153
- if (this.#build.production) {
154
- defaultTreeAdapter.detachNode(commentNode);
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
- #collectImports(node, collection, forEach) {
159
- for (const childNode of node.childNodes) {
160
- if (forEach && 'tagName' in childNode) {
161
- forEach(childNode);
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
- #parseImport(type, href, elem) {
199
- const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href);
200
- if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
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
- return elem.attrs.find(attr => attr.name === name);
207
+ return elem.attrs.find((attr) => attr.name === name);
220
208
  }
221
209
  function hasAttr(elem, name, value) {
222
- return elem.attrs.some(attr => attr.name === name && attr.value === value);
210
+ return elem.attrs.some((attr) => attr.name === name && attr.value === value);
223
211
  }
224
212
  function mergeEntrypoints(...imports) {
225
- const entrypoints = [];
226
- for (const { scripts } of imports) {
227
- for (const script of scripts) {
228
- entrypoints.push(script.entrypoint);
229
- }
213
+ const entrypoints = [];
214
+ for (const { scripts } of imports) {
215
+ for (const script of scripts) {
216
+ entrypoints.push(script.entrypoint);
230
217
  }
231
- return entrypoints;
218
+ }
219
+ return entrypoints;
232
220
  }
233
221
  function rewriteHrefs(scripts, hrefs) {
234
- for (const { elem, entrypoint, type } of scripts) {
235
- const rewriteTo = hrefs ? hrefs.mappedHref(entrypoint.in) : null;
236
- if (type === 'script') {
237
- if (entrypoint.in.endsWith('.tsx') ||
238
- entrypoint.in.endsWith('.ts')) {
239
- elem.attrs.find(attr => attr.name === 'src').value =
240
- rewriteTo || `/${entrypoint.out}`;
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
- console.log(`\u001b[31merror:\u001b[0m`, msg);
252
- process.exit(1);
234
+ console.log(`\x1B[31merror:\x1B[0m`, msg);
235
+ process.exit(1);
253
236
  }
237
+ export {
238
+ HtmlEntrypoint
239
+ };