@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/lib_js/html.js CHANGED
@@ -1,258 +1,239 @@
1
- import EventEmitter from 'node:events';
2
- import { readFile } from 'node:fs/promises';
3
- import { dirname, join, relative, resolve } 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
- #fsPath;
13
- #partials = [];
14
- #scripts = [];
15
- #update = Object();
16
- #url;
17
- constructor(build, url, fsPath, decorations) {
18
- super({ captureRejections: true });
19
- this.#build = build;
20
- this.#decorations = decorations;
21
- this.#url = url;
22
- this.#fsPath = fsPath;
23
- this.on('change', this.#onChange);
24
- 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");
25
40
  }
26
- get fsPath() {
27
- 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;
28
55
  }
29
- get url() {
30
- 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());
31
66
  }
32
- async #html() {
33
- try {
34
- return await readFile(join(this.#build.dirs.pages, this.#fsPath), 'utf8');
35
- }
36
- catch (e) {
37
- // todo error handling
38
- errorExit(this.#fsPath + ' does not exist');
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
- // todo if partial changes, hot swap content in page
42
- #onChange = async (_partial) => {
43
- const update = (this.#update = Object());
44
- const html = await this.#html();
45
- const document = parse(html);
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
- // rewrite hrefs in a partial to be relative to the html entrypoint instead of the partial
98
- #rewritePartialRelativePaths(elem, partialPath) {
99
- let rewritePath = null;
100
- if (elem.nodeName === 'script') {
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
- #addDecorations(document) {
115
- if (!this.#decorations?.length) {
116
- return;
117
- }
118
- for (const decoration of this.#decorations) {
119
- switch (decoration.type) {
120
- case 'script':
121
- const scriptNode = parseFragment(`<script type="module">${decoration.js}</script>`).childNodes[0];
122
- const htmlNode = document.childNodes.find(node => node.nodeName === 'html');
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
- output(hrefs) {
130
- this.#injectPartials();
131
- this.#rewriteHrefs(hrefs);
132
- 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);
133
135
  }
134
- // rewrites hrefs to content hashed urls
135
- // call without hrefs to rewrite tsx? ext to js
136
- #rewriteHrefs(hrefs) {
137
- rewriteHrefs(this.#scripts, hrefs);
138
- for (const partial of this.#partials) {
139
- rewriteHrefs(partial.imports.scripts, hrefs);
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
- async #injectPartials() {
143
- for (const { commentNode, fragment } of this.#partials) {
144
- if (!this.#build.production) {
145
- defaultTreeAdapter.insertBefore(commentNode.parentNode, defaultTreeAdapter.createCommentNode(commentNode.data), commentNode);
146
- }
147
- for (const node of fragment.childNodes) {
148
- defaultTreeAdapter.insertBefore(commentNode.parentNode, node, commentNode);
149
- }
150
- if (this.#build.production) {
151
- defaultTreeAdapter.detachNode(commentNode);
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
- #collectImports(node, collection, forEach) {
156
- for (const childNode of node.childNodes) {
157
- if (forEach && 'tagName' in childNode) {
158
- forEach(childNode);
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
- #parseImport(type, href, elem) {
196
- const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href);
197
- if (!isPathInPagesDir(this.#build, inPath)) {
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
- // check if relative dir is a subpath of pages dir when joined with pages dir
216
- // used if the joined pages dir path is only used for the pages dir check
217
- function isPagesSubpathInPagesDir(build, subpath) {
218
- return isPathInPagesDir(build, join(build.dirs.pages, subpath));
219
- }
220
- // check if subpath joined with pages dir is a subpath of pages dir
221
- function isPathInPagesDir(build, p) {
222
- return resolve(p).startsWith(build.dirs.pagesResolved);
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
- return elem.attrs.find(attr => attr.name === name);
207
+ return elem.attrs.find((attr) => attr.name === name);
226
208
  }
227
209
  function hasAttr(elem, name, value) {
228
- return elem.attrs.some(attr => attr.name === name && attr.value === value);
210
+ return elem.attrs.some((attr) => attr.name === name && attr.value === value);
229
211
  }
230
212
  function mergeEntrypoints(...imports) {
231
- const entrypoints = [];
232
- for (const { scripts } of imports) {
233
- for (const script of scripts) {
234
- entrypoints.push(script.entrypoint);
235
- }
213
+ const entrypoints = [];
214
+ for (const { scripts } of imports) {
215
+ for (const script of scripts) {
216
+ entrypoints.push(script.entrypoint);
236
217
  }
237
- return entrypoints;
218
+ }
219
+ return entrypoints;
238
220
  }
239
221
  function rewriteHrefs(scripts, hrefs) {
240
- for (const { elem, entrypoint, type } of scripts) {
241
- const rewriteTo = hrefs ? hrefs.mappedHref(entrypoint.in) : null;
242
- if (type === 'script') {
243
- if (entrypoint.in.endsWith('.tsx') ||
244
- entrypoint.in.endsWith('.ts')) {
245
- elem.attrs.find(attr => attr.name === 'src').value =
246
- rewriteTo || `/${entrypoint.out}`;
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
- console.log(`\u001b[31merror:\u001b[0m`, msg);
257
- process.exit(1);
234
+ console.log(`\x1B[31merror:\x1B[0m`, msg);
235
+ process.exit(1);
258
236
  }
237
+ export {
238
+ HtmlEntrypoint
239
+ };