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 +32 -0
- package/hmr.js +9 -0
- package/package.json +4 -1
- package/serve.js +126 -20
- package/about.html +0 -13
- package/index.html +0 -9
- package/test.css +0 -4
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spa-ssi",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
package/test.css
DELETED