spa-ssi 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -29,3 +29,35 @@ Single Page App / Server Side Include Simple File Web Server
29
29
 
30
30
  If a page request doesn't resolve to a file, it defaults to /index.html
31
31
 
32
+ ## HMR Support
33
+
34
+ Okay, it isn't true HMR, but in my experience it is as good as.
35
+
36
+ Add this to index.html:
37
+
38
+ ```html
39
+ <script>
40
+ const localhosts = ['localhost', '127.0.0.1', '[::1]'];
41
+ const {hostname} = location;
42
+ if(localhosts.includes(hostname)){
43
+ window.addEventListener("focus", () => {
44
+ location.reload();
45
+ });
46
+ }
47
+ </script>
48
+ ```
49
+
50
+ or:
51
+
52
+ ```html
53
+ <script type=module>
54
+ import 'spa-ssi/hmr.js';
55
+ </script>
56
+ ```
57
+
58
+
59
+
60
+ ## Directory Listing support
61
+
62
+ /sitemap lists all the html links within the site.
63
+
package/hmr.js ADDED
@@ -0,0 +1,9 @@
1
+ //@ts-check
2
+
3
+ const localhosts = ['localhost', '127.0.0.1', '[::1]'];
4
+ const {hostname} = location;
5
+ if(localhosts.includes(hostname)){
6
+ window.addEventListener("focus", () => {
7
+ location.reload();
8
+ });
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spa-ssi",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Single Page App / Server Side Include Simple File Web Server",
5
5
  "keywords": [
6
6
  "single-page-app",
@@ -10,6 +10,9 @@
10
10
  "bugs": {
11
11
  "url": "https://github.com/bahrus/spa-ssi/issues"
12
12
  },
13
+ "files": [
14
+ "*.js"
15
+ ],
13
16
  "repository": {
14
17
  "type": "git",
15
18
  "url": "git+https://github.com/bahrus/spa-ssi.git"
package/serve.js CHANGED
@@ -30,6 +30,13 @@ class SimpleHTTPRequestHandler {
30
30
  async handleRequest(req, res) {
31
31
  try {
32
32
  const parsedUrl = url.parse(req.url);
33
+ if(parsedUrl.pathname?.toLowerCase() === '/sitemap'){
34
+ const siteMapContent = await this.renderSiteMap();
35
+ // Send response
36
+ res.writeHead(200, { "Content-Type": 'text/html' });
37
+ res.end(siteMapContent);
38
+ return;
39
+ }
33
40
  let pathname = decodeURIComponent(parsedUrl.pathname);
34
41
 
35
42
  // Normalize path to prevent directory traversal
@@ -73,6 +80,105 @@ class SimpleHTTPRequestHandler {
73
80
  }
74
81
  }
75
82
 
83
+ /**
84
+ * Recursively find all .html files
85
+ * @param {*} dir
86
+ * @returns
87
+ */
88
+ async findHtmlFiles(dir) {
89
+ /** @type {string[]} */
90
+ const results = [];
91
+ const dirEntris = await fs.readdir(dir, { withFileTypes: true });
92
+ for (const entry of dirEntris) {
93
+ const fullPath = path.join(dir, entry.name);
94
+
95
+ if (entry.isDirectory()) {
96
+ const subFiles = await this.findHtmlFiles(fullPath);
97
+ results.push(...subFiles);
98
+ } else if (entry.isFile() && entry.name.endsWith('.html')) {
99
+ results.push(fullPath);
100
+ }
101
+ }
102
+
103
+ return results;
104
+ }
105
+
106
+ /**
107
+ * Extract <title> from HTML file
108
+ * @param {string} filePath
109
+ * @returns
110
+ */
111
+ async extractTitle(filePath) {
112
+ try {
113
+ const content = await fs.readFile(filePath, 'utf8');
114
+ const match = content.match(/<title>(.*?)<\/title>/i);
115
+ return match ? match[1] : path.basename(filePath);
116
+ } catch {
117
+ return path.basename(filePath);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Build HTML list
123
+ * @param {string[]} files
124
+ * @param {string} baseDir
125
+ */
126
+ buildHtmlList(files, baseDir) {
127
+ /**
128
+ * @type {any}
129
+ */
130
+ const tree = {};
131
+
132
+ // Build nested structure
133
+ for (const file of files) {
134
+ const relativePath = path.relative(baseDir, file);
135
+ const parts = relativePath.split(path.sep);
136
+ let current = tree;
137
+
138
+ for (let i = 0; i < parts.length; i++) {
139
+ const part = parts[i];
140
+ if (i === parts.length - 1) {
141
+ current[part] = file; // Leaf node: file path
142
+ } else {
143
+ current[part] = current[part] || {};
144
+ current = current[part];
145
+ }
146
+ }
147
+ }
148
+ return this.renderTree(tree, baseDir);
149
+ }
150
+
151
+ // Render HTML
152
+ /**
153
+ *
154
+ * @param {any} node
155
+ * @param {string} baseDir
156
+ * @returns
157
+ */
158
+ async renderTree(node, baseDir) {
159
+ let html = '<ul>';
160
+ for (const key in node) {
161
+ const value = node[key];
162
+ if (typeof value === 'string') {
163
+ const title = await this.extractTitle(value);
164
+ const href = path.relative(baseDir, value).replace(/\\/g, '/');
165
+ html += `<li><a href="${href}">${title}</a></li>`;
166
+ } else {
167
+ html += `<li>${key}${await this.renderTree(value, baseDir)}</li>`;
168
+ }
169
+ }
170
+ html += '</ul>';
171
+ return html;
172
+ }
173
+
174
+ async renderSiteMap(){
175
+ // Run it
176
+ const baseDir = process.cwd();
177
+ const htmlFiles = await this.findHtmlFiles(baseDir);
178
+ const htmlOutput = this.buildHtmlList(htmlFiles, baseDir);
179
+ return htmlOutput;
180
+ }
181
+
76
182
  /**
77
183
  * Process SSI includes
78
184
  * @param {string} html
@@ -95,7 +201,7 @@ class SimpleHTTPRequestHandler {
95
201
  return html;
96
202
  }
97
203
 
98
-
204
+
99
205
  /**
100
206
  * Basic MIME type mapping
101
207
  * @param {string} ext
@@ -113,26 +219,26 @@ class SimpleHTTPRequestHandler {
113
219
  * @returns
114
220
  */
115
221
  function getAvailablePort(startingAt) {
116
- /**
117
- *
118
- * @param {number} currentPort
119
- * @param {(value: any) => void} cb
120
- */
121
- function getNextAvailablePort(currentPort, cb) {
122
- const server = net.createServer();
123
- server.listen(currentPort, () => {
124
- server.once('close', () => {
125
- cb(currentPort);
126
- });
127
- server.close();
128
- });
129
- server.on('error', _ => {
130
- getNextAvailablePort(++currentPort, cb);
131
- });
132
- }
133
- return new Promise(resolve => {
134
- getNextAvailablePort(startingAt, resolve);
222
+ /**
223
+ *
224
+ * @param {number} currentPort
225
+ * @param {(value: any) => void} cb
226
+ */
227
+ function getNextAvailablePort(currentPort, cb) {
228
+ const server = net.createServer();
229
+ server.listen(currentPort, () => {
230
+ server.once('close', () => {
231
+ cb(currentPort);
232
+ });
233
+ server.close();
234
+ });
235
+ server.on('error', _ => {
236
+ getNextAvailablePort(++currentPort, cb);
135
237
  });
238
+ }
239
+ return new Promise(resolve => {
240
+ getNextAvailablePort(startingAt, resolve);
241
+ });
136
242
  }
137
243
 
138
244
  /**
package/about.html DELETED
@@ -1,13 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-16">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Document</title>
7
- <link rel="stylesheet" href="test.css">
8
- </head>
9
- <body>
10
- <p>ssi_server is like Python's SimpleHTTPServer, but with minimal support for <a href="http://en.wikipedia.org/wiki/Server_Side_Includes">Server Side Includes</a> (SSI).</p>
11
-
12
- </body>
13
- </html>
package/index.html DELETED
@@ -1,9 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>ssi_server demo</title>
5
- </head>
6
- <body>
7
- <!--#include virtual="about.html"-->
8
- </body>
9
- </html>
package/test.css DELETED
@@ -1,4 +0,0 @@
1
- @charset "UTF-8";
2
- p {
3
- color: green;
4
- }