host-mdx 2.0.1 → 2.1.1
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/LICENSE +21 -0
- package/README.md +30 -21
- package/index.js +270 -159
- package/mdx-to-html.js +36 -10
- package/package.json +15 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Manas Ravindra Makde <manasmakde@gmail.com> (https://manasmakde.github.io/)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# host-mdx
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/host-mdx )
|
|
4
|
-
|
|
2
|
+
[](https://www.npmjs.com/package/host-mdx)
|
|
5
3
|
A cli tool to create and serve a static html website from a given mdx directory
|
|
6
4
|
|
|
7
|
-
## Usage
|
|
8
5
|
|
|
6
|
+
## 🛠️ Usage
|
|
9
7
|
```
|
|
10
8
|
host-mdx [options]
|
|
11
9
|
|
|
@@ -18,28 +16,29 @@ Options:
|
|
|
18
16
|
--track-changes, -t Tracks any changes made & auto reloads
|
|
19
17
|
--verobse, -v Shows additional log messages
|
|
20
18
|
```
|
|
19
|
+
|
|
20
|
+
> If `--input-path` is not provided it will default to `./` i.e. current working directory
|
|
21
|
+
> If `--output-path` is not provided a temp folder will be created automatically & deleted upon exit
|
|
21
22
|
|
|
22
23
|
Add a file by the name `.hostmdxignore` at the root of your project to filter out which files/folders to skip while generating html
|
|
23
24
|
(similar to [.gitignore](https://git-scm.com/docs/gitignore))
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
Add a file by the name `host-mdx.js` at the root of your
|
|
27
|
+
Add a file by the name `host-mdx.js` at the root of your input folder as a config file with the following:
|
|
27
28
|
|
|
28
29
|
```js
|
|
29
|
-
// Modify
|
|
30
|
-
modBundleMDXSettings(settings)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Hooks
|
|
34
30
|
onSiteCreateStart(inputPath, outputPath)
|
|
35
|
-
onSiteCreateEnd(inputPath, outputPath)
|
|
31
|
+
onSiteCreateEnd(inputPath, outputPath, wasInterrupted)
|
|
36
32
|
onFileCreateStart(inputFilePath, outputFilePath)
|
|
37
33
|
onFileCreateEnd(inputFilePath, outputFilePath)
|
|
34
|
+
modBundleMDXSettings(inputPath, outputPath, settings)
|
|
35
|
+
toTriggerRecreate(event, path)
|
|
38
36
|
```
|
|
39
|
-
> Note: Any changes made to `host-mdx.js` require complete restart otherwise changes will not reflect
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
> **Note:** Any changes made to `host-mdx.js` or any new package added requires complete restart otherwise changes will not reflect due to [this bug](https://github.com/nodejs/node/issues/49442)
|
|
42
39
|
|
|
40
|
+
|
|
41
|
+
## 📖 Example
|
|
43
42
|
Command:
|
|
44
43
|
```bash
|
|
45
44
|
npx host-mdx --input-path="path/to/my-website-template" --output-path="path/to/my-website" --port=3113 -t
|
|
@@ -81,25 +80,33 @@ static/temp.jpg
|
|
|
81
80
|
export function onSiteCreateStart(inputPath, outputPath) {
|
|
82
81
|
console.log("onSiteCreateStart", inputPath, outputPath)
|
|
83
82
|
}
|
|
84
|
-
export function onSiteCreateEnd(inputPath, outputPath, wasSuccessful){
|
|
85
|
-
console.log("onSiteCreateEnd", inputPath, outputPath)
|
|
83
|
+
export function onSiteCreateEnd(inputPath, outputPath, wasSuccessful) {
|
|
84
|
+
console.log("onSiteCreateEnd", inputPath, outputPath, wasSuccessful)
|
|
86
85
|
}
|
|
87
|
-
export function onFileCreateStart(inputFilePath, outputFilePath){
|
|
86
|
+
export function onFileCreateStart(inputFilePath, outputFilePath) {
|
|
88
87
|
console.log("onFileCreateStart", inputFilePath, outputFilePath)
|
|
89
88
|
}
|
|
90
|
-
export function onFileCreateEnd(inputFilePath, outputFilePath){
|
|
89
|
+
export function onFileCreateEnd(inputFilePath, outputFilePath) {
|
|
91
90
|
console.log("onFileCreateEnd", inputFilePath, outputFilePath)
|
|
92
91
|
}
|
|
93
|
-
export function onHostStart(port){
|
|
92
|
+
export function onHostStart(port) {
|
|
94
93
|
console.log("onHostStart", port)
|
|
95
94
|
}
|
|
96
|
-
export function onHostEnd(port){
|
|
95
|
+
export function onHostEnd(port) {
|
|
97
96
|
console.log("onHostEnd", port)
|
|
98
97
|
}
|
|
99
|
-
export function modBundleMDXSettings(settings){
|
|
98
|
+
export function modBundleMDXSettings(inputPath, outputPath, settings) {
|
|
100
99
|
// Modify settings ...
|
|
101
100
|
return settings
|
|
102
101
|
}
|
|
102
|
+
export function toTriggerRecreate(event, path) {
|
|
103
|
+
const isGOutputStream = /\.goutputstream-\w+$/.test(path);
|
|
104
|
+
if (isGOutputStream) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
103
110
|
```
|
|
104
111
|
|
|
105
112
|
Output Directory:
|
|
@@ -120,4 +127,6 @@ my-website/
|
|
|
120
127
|
|
|
121
128
|
The site will now be visible in the browser at `localhost:3113`
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
|
|
131
|
+
## 🔑 License
|
|
132
|
+
MIT © [Manas Ravindra Makde](https://manasmakde.github.io/)
|
package/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import os from 'os'
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import net from 'net';
|
|
5
6
|
import path from 'path'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
7
|
+
import sirv from 'sirv';
|
|
8
|
+
import polka from 'polka';
|
|
9
|
+
import ignore from 'ignore';
|
|
8
10
|
import chokidar from 'chokidar';
|
|
9
11
|
import * as readline from 'readline';
|
|
10
|
-
import {
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
13
|
+
import { mdxToHtml } from './mdx-to-html.js';
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
// To-Set Properties
|
|
14
|
-
const APP_NAME = "host-mdx"
|
|
15
|
-
const DEFAULT_PORT = 3000
|
|
16
|
-
const
|
|
17
|
-
const
|
|
17
|
+
const APP_NAME = "host-mdx";
|
|
18
|
+
const DEFAULT_PORT = 3000;
|
|
19
|
+
const MAX_PORT = 3002;
|
|
20
|
+
const IGNORE_FILE_NAME = ".hostmdxignore";
|
|
21
|
+
const CONFIG_FILE_NAME = "host-mdx.js";
|
|
22
|
+
const FILE_404 = "404.html";
|
|
23
|
+
const NOT_FOUND_404_MESSAGE = "404";
|
|
18
24
|
const DEFAULT_IGNORES = `
|
|
19
25
|
${IGNORE_FILE_NAME}
|
|
20
26
|
${CONFIG_FILE_NAME}
|
|
@@ -22,21 +28,23 @@ node_modules
|
|
|
22
28
|
package-lock.json
|
|
23
29
|
package.json
|
|
24
30
|
.git
|
|
25
|
-
|
|
31
|
+
.github
|
|
32
|
+
.gitignore
|
|
33
|
+
`;
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
// Flags
|
|
29
|
-
const CREATE_FLAG = "--create-only"
|
|
30
|
-
const CREATE_SHORT_FLAG = "-c"
|
|
31
|
-
const HELP_FLAG = "--help"
|
|
32
|
-
const HELP_SHORT_FLAG = "-h"
|
|
33
|
-
const INPUT_PATH_FLAG = "--input-path"
|
|
34
|
-
const OUTPUT_PATH_FLAG = "--output-path"
|
|
35
|
-
const PORT_FLAG = "--port"
|
|
36
|
-
const VERBOSE_FLAG = "--
|
|
37
|
-
const VERBOSE_SHORT_FLAG = "-v"
|
|
38
|
-
const TRACK_CHANGES_FLAG = "--track-changes"
|
|
39
|
-
const TRACK_CHANGES_SHORT_FLAG = "-t"
|
|
37
|
+
const CREATE_FLAG = "--create-only";
|
|
38
|
+
const CREATE_SHORT_FLAG = "-c";
|
|
39
|
+
const HELP_FLAG = "--help";
|
|
40
|
+
const HELP_SHORT_FLAG = "-h";
|
|
41
|
+
const INPUT_PATH_FLAG = "--input-path";
|
|
42
|
+
const OUTPUT_PATH_FLAG = "--output-path";
|
|
43
|
+
const PORT_FLAG = "--port";
|
|
44
|
+
const VERBOSE_FLAG = "--verbose";
|
|
45
|
+
const VERBOSE_SHORT_FLAG = "-v";
|
|
46
|
+
const TRACK_CHANGES_FLAG = "--track-changes";
|
|
47
|
+
const TRACK_CHANGES_SHORT_FLAG = "-t";
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
// Messages & Errors
|
|
@@ -50,15 +58,16 @@ ${OUTPUT_PATH_FLAG}=... The path to which all html files will be generated
|
|
|
50
58
|
${PORT_FLAG}=... Localhost port number on which to host
|
|
51
59
|
${TRACK_CHANGES_FLAG}, ${TRACK_CHANGES_SHORT_FLAG} Tracks any changes made & auto reloads
|
|
52
60
|
${VERBOSE_FLAG}, ${VERBOSE_SHORT_FLAG} Shows additional log messages
|
|
53
|
-
|
|
61
|
+
`;
|
|
54
62
|
|
|
55
63
|
|
|
56
64
|
// Private Properties
|
|
57
|
-
let isCreatingSite = false // Prevents site from being recreated if creation is already ongoing
|
|
58
|
-
let
|
|
59
|
-
let
|
|
60
|
-
let
|
|
61
|
-
|
|
65
|
+
let isCreatingSite = false; // Prevents site from being recreated if creation is already ongoing
|
|
66
|
+
let isCreateSitePending = false // Keeps track if files have been modified and site needs to be recreated
|
|
67
|
+
let isVerbose = false;
|
|
68
|
+
let configs;
|
|
69
|
+
let app;
|
|
70
|
+
const TEMP_HTML_DIR = path.join(os.tmpdir(), `${APP_NAME}`);
|
|
62
71
|
const TIME_OPTIONS = {
|
|
63
72
|
year: 'numeric',
|
|
64
73
|
month: 'long',
|
|
@@ -68,23 +77,7 @@ const TIME_OPTIONS = {
|
|
|
68
77
|
second: 'numeric',
|
|
69
78
|
hour12: false,
|
|
70
79
|
fractionalSecondDigits: 3
|
|
71
|
-
}
|
|
72
|
-
const MIME_TYPE = { // Maps extensions to mime protocol
|
|
73
|
-
'.html': 'text/html',
|
|
74
|
-
'.css': 'text/css',
|
|
75
|
-
'.js': 'text/javascript',
|
|
76
|
-
'.json': 'application/json',
|
|
77
|
-
'.png': 'image/png',
|
|
78
|
-
'.jpg': 'image/jpeg',
|
|
79
|
-
'.ico': 'image/x-icon',
|
|
80
|
-
'.wav': 'audio/wav',
|
|
81
|
-
'.webp': 'image/webp',
|
|
82
|
-
'.mp3': 'audio/mpeg',
|
|
83
|
-
'.mp4': 'video/mp4',
|
|
84
|
-
'.svg': 'image/svg+xml',
|
|
85
|
-
'.pdf': 'application/pdf',
|
|
86
|
-
'.zip': 'application/zip',
|
|
87
|
-
}
|
|
80
|
+
};
|
|
88
81
|
|
|
89
82
|
|
|
90
83
|
// Utility Methods
|
|
@@ -97,18 +90,16 @@ function log(msg, checkVerbose = false) {
|
|
|
97
90
|
console.log(`[${APP_NAME} ${timestamp}] ${msg}`)
|
|
98
91
|
}
|
|
99
92
|
function createTempDir() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (fs.existsSync(TEMP_HTML_DIR)) {
|
|
103
|
-
fs.rmSync(TEMP_HTML_DIR, { recursive: true, force: true })
|
|
104
|
-
}
|
|
93
|
+
// Create default temp html dir
|
|
94
|
+
fs.mkdirSync(TEMP_HTML_DIR, { recursive: true });
|
|
105
95
|
|
|
106
96
|
|
|
107
|
-
//
|
|
108
|
-
|
|
97
|
+
// Generate time stamp
|
|
98
|
+
const now = new Date()
|
|
99
|
+
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1)}-${now.getDate()}T${now.getHours()}_${now.getMinutes()}_${now.getSeconds()}`
|
|
109
100
|
|
|
110
101
|
|
|
111
|
-
return fs.mkdtempSync(path.join(TEMP_HTML_DIR,
|
|
102
|
+
return fs.mkdtempSync(path.join(TEMP_HTML_DIR, `html-${timestamp}-`));
|
|
112
103
|
}
|
|
113
104
|
function getIgnore(ignoreFilePath) {
|
|
114
105
|
const ig = ignore();
|
|
@@ -137,20 +128,15 @@ function createFile(filePath, fileContent = "") {
|
|
|
137
128
|
async function createSite(inputPath, outputPath) {
|
|
138
129
|
// Exit if already creating
|
|
139
130
|
if (isCreatingSite) {
|
|
140
|
-
log("
|
|
131
|
+
log("Site creation already ongoing! Added to pending")
|
|
132
|
+
isCreateSitePending = true
|
|
141
133
|
return
|
|
142
134
|
}
|
|
143
135
|
|
|
144
136
|
|
|
145
137
|
// Set creating status to ongoing
|
|
146
138
|
isCreatingSite = true
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Get config properties
|
|
150
|
-
let configFilePath = path.join(inputPath, `./${CONFIG_FILE_NAME}`)
|
|
151
|
-
if (fs.existsSync(configFilePath)) {
|
|
152
|
-
configs = await import(configFilePath);
|
|
153
|
-
}
|
|
139
|
+
isCreateSitePending = false
|
|
154
140
|
|
|
155
141
|
|
|
156
142
|
// Broadcast site creation started
|
|
@@ -171,20 +157,20 @@ async function createSite(inputPath, outputPath) {
|
|
|
171
157
|
|
|
172
158
|
// Iterate through all folders & files
|
|
173
159
|
const stack = [inputPath];
|
|
174
|
-
while (stack.length > 0) {
|
|
160
|
+
while (stack.length > 0 && !isCreateSitePending) {
|
|
175
161
|
// Continue if path does not exist
|
|
176
162
|
const currentPath = stack.pop()
|
|
177
|
-
if(!fs.existsSync(currentPath)){
|
|
163
|
+
if (!fs.existsSync(currentPath)) {
|
|
178
164
|
continue;
|
|
179
165
|
}
|
|
180
166
|
|
|
181
|
-
|
|
167
|
+
|
|
182
168
|
// Get essentials
|
|
183
169
|
const relToInput = path.relative(inputPath, currentPath)
|
|
184
170
|
const toIgnore = inputPath != currentPath && ig.ignores(relToInput)
|
|
185
171
|
const absToOutput = path.join(outputPath, relToInput)
|
|
186
172
|
const isDir = fs.statSync(currentPath).isDirectory()
|
|
187
|
-
const isMdx = !isDir &&
|
|
173
|
+
const isMdx = !isDir && currentPath.endsWith(".mdx")
|
|
188
174
|
|
|
189
175
|
|
|
190
176
|
// Skip if to ignore this path
|
|
@@ -212,7 +198,12 @@ async function createSite(inputPath, outputPath) {
|
|
|
212
198
|
// convert mdx code into html & paste into file
|
|
213
199
|
let mdxCode = fs.readFileSync(currentPath, 'utf8');
|
|
214
200
|
let parentDir = path.dirname(currentPath)
|
|
215
|
-
let
|
|
201
|
+
let globalArgs = {
|
|
202
|
+
hostmdxCwd: parentDir,
|
|
203
|
+
hostmdxInputPath: inputPath,
|
|
204
|
+
hostmdxOutputPath: outputPath
|
|
205
|
+
};
|
|
206
|
+
let htmlCode = await mdxToHtml(mdxCode, parentDir, globalArgs, (settings) => { return configs?.modBundleMDXSettings?.(inputPath, outputPath, settings) ?? settings });
|
|
216
207
|
createFile(absHtmlPath, `<!DOCTYPE html>\n${htmlCode}`);
|
|
217
208
|
|
|
218
209
|
|
|
@@ -247,21 +238,157 @@ async function createSite(inputPath, outputPath) {
|
|
|
247
238
|
|
|
248
239
|
|
|
249
240
|
// Broadcast site creation ended
|
|
250
|
-
|
|
251
|
-
|
|
241
|
+
if (isCreateSitePending) {
|
|
242
|
+
log(`Restarting site creation...`)
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
log(`Created site at ${outputPath}`)
|
|
246
|
+
}
|
|
247
|
+
configs?.onSiteCreateEnd?.(inputPath, outputPath, isCreateSitePending)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
// Reinvoke creation
|
|
251
|
+
if (isCreateSitePending) {
|
|
252
|
+
await createSite(inputPath, outputPath);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function isPortAvailable(port) {
|
|
256
|
+
const server = net.createServer();
|
|
257
|
+
server.unref();
|
|
258
|
+
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
server.once('error', () => {
|
|
261
|
+
server.close();
|
|
262
|
+
resolve(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
server.once('listening', () => {
|
|
266
|
+
server.close(() => resolve(true));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
server.listen(port);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function getAvailablePort(startPort, maxPort) {
|
|
273
|
+
let currentPort = startPort;
|
|
274
|
+
while (currentPort <= maxPort) {
|
|
275
|
+
if (await isPortAvailable(currentPort)) {
|
|
276
|
+
return currentPort;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
currentPort++;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
function stripTrailingSep(thePath) {
|
|
285
|
+
if (thePath[thePath.length - 1] === path.sep) {
|
|
286
|
+
return thePath.slice(0, -1);
|
|
287
|
+
}
|
|
288
|
+
return thePath;
|
|
289
|
+
}
|
|
290
|
+
function isSubPath(potentialParent, thePath) {
|
|
291
|
+
// For inside-directory checking, we want to allow trailing slashes, so normalize.
|
|
292
|
+
thePath = stripTrailingSep(thePath);
|
|
293
|
+
potentialParent = stripTrailingSep(potentialParent);
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// Node treats only Windows as case-insensitive in its path module; we follow those conventions.
|
|
297
|
+
if (process.platform === "win32") {
|
|
298
|
+
thePath = thePath.toLowerCase();
|
|
299
|
+
potentialParent = potentialParent.toLowerCase();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
return thePath.lastIndexOf(potentialParent, 0) === 0 &&
|
|
304
|
+
(
|
|
305
|
+
thePath[potentialParent.length] === path.sep ||
|
|
306
|
+
thePath[potentialParent.length] === undefined
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
async function filterArgs(rawArgs) {
|
|
310
|
+
// Assign to create
|
|
311
|
+
let toCreateOnly = rawArgs.includes(CREATE_FLAG) || rawArgs.includes(CREATE_SHORT_FLAG)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
// Assign input path
|
|
315
|
+
let inputPath = rawArgs.find(val => val.startsWith(INPUT_PATH_FLAG));
|
|
316
|
+
let inputPathProvided = inputPath !== undefined;
|
|
317
|
+
inputPath = inputPathProvided ? inputPath.split('=')[1] : process.cwd();
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
// Check input path
|
|
321
|
+
if (!fs.existsSync(inputPath) || !fs.lstatSync(inputPath).isDirectory()) {
|
|
322
|
+
log(`Invalid input path "${inputPath}"`)
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
inputPath = inputPath !== "" ? path.resolve(inputPath) : inputPath; // To ensure input path is absolute
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// Assign output path
|
|
331
|
+
let outputPath = rawArgs.find(val => val.startsWith(OUTPUT_PATH_FLAG));
|
|
332
|
+
let outputPathProvided = outputPath !== undefined;
|
|
333
|
+
outputPath = outputPathProvided ? outputPath.split('=')[1] : createTempDir();
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
// Check output path
|
|
337
|
+
if (!fs.existsSync(outputPath) || !fs.lstatSync(outputPath).isDirectory()) {
|
|
338
|
+
log(`Invalid output path "${outputPath}"`)
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
outputPath = outputPath !== "" ? path.resolve(outputPath) : outputPath; // To ensure output path is absolute
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
// Check if output path is inside input path (causing infinite loop)
|
|
347
|
+
if (isSubPath(inputPath, outputPath)) {
|
|
348
|
+
log(`Output path "${outputPath}" cannot be inside or same as input path "${inputPath}"`);
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
// Assign port
|
|
354
|
+
let port = rawArgs.find(val => val.startsWith(PORT_FLAG));
|
|
355
|
+
let portProvided = port !== undefined;
|
|
356
|
+
port = portProvided ? Number(port.split('=')[1]) : (await getAvailablePort(DEFAULT_PORT, MAX_PORT));
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// Check port
|
|
360
|
+
if (port === -1) {
|
|
361
|
+
log(`Could not find any available ports between ${DEFAULT_PORT} to ${MAX_PORT}, Try manually passing ${PORT_FLAG}=... flag`);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
else if (!Number.isInteger(port)) {
|
|
365
|
+
log(`Invalid port`)
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
// Assign tracking changes
|
|
371
|
+
let toTrackChanges = rawArgs.includes(TRACK_CHANGES_FLAG) || rawArgs.includes(TRACK_CHANGES_SHORT_FLAG);
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
return { toCreateOnly, inputPath, inputPathProvided, outputPath, outputPathProvided, toTrackChanges, port, portProvided, };
|
|
252
375
|
}
|
|
253
376
|
|
|
254
377
|
|
|
255
378
|
// Main Methods
|
|
256
379
|
async function createSiteSafe(...args) {
|
|
380
|
+
|
|
381
|
+
let success = true;
|
|
257
382
|
try {
|
|
258
383
|
await createSite(...args);
|
|
259
384
|
}
|
|
260
385
|
catch (err) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
log(
|
|
386
|
+
success = false;
|
|
387
|
+
isCreatingSite = false;
|
|
388
|
+
log(`Failed to create site!\n${err.stack}`);
|
|
264
389
|
}
|
|
390
|
+
|
|
391
|
+
return success;
|
|
265
392
|
}
|
|
266
393
|
async function listenForKey(createSiteCallback) {
|
|
267
394
|
|
|
@@ -276,144 +403,128 @@ async function listenForKey(createSiteCallback) {
|
|
|
276
403
|
createSiteCallback();
|
|
277
404
|
}
|
|
278
405
|
else if (key && key.sequence == '\x03') {
|
|
279
|
-
server
|
|
406
|
+
app?.server?.close((e) => { process.exit() })
|
|
280
407
|
}
|
|
281
408
|
});
|
|
282
409
|
}
|
|
410
|
+
async function watchForChanges(pathTowatch, callback) {
|
|
411
|
+
chokidar.watch(pathTowatch, {
|
|
412
|
+
ignoreInitial: true
|
|
413
|
+
}).on('all', callback);
|
|
414
|
+
}
|
|
283
415
|
function startServer(htmlDir, port) { // Starts server at given port
|
|
284
|
-
|
|
416
|
+
|
|
285
417
|
// Broadcast server starting
|
|
286
418
|
configs?.onHostStart?.(port)
|
|
287
419
|
|
|
288
420
|
|
|
289
421
|
// Start Server
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
res.statusCode = 500
|
|
306
|
-
res.end(`Error getting the file: ${err}.`)
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
// Based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
|
310
|
-
const ext = path.parse(absoluteFilePath).ext
|
|
311
|
-
res.setHeader('Content-type', MIME_TYPE[ext] || 'text/plain') // if the file is found, set Content-type and send data
|
|
312
|
-
res.end(data)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
})
|
|
316
|
-
else { // Respondes with 404 if file not found
|
|
317
|
-
res.statusCode = 404
|
|
318
|
-
res.end(`404 Invalid url not found!`)
|
|
422
|
+
const assets = sirv(htmlDir, { dev: true });
|
|
423
|
+
const newApp = polka({
|
|
424
|
+
onNoMatch: (req, res) => {
|
|
425
|
+
// Set status code to 404
|
|
426
|
+
res.statusCode = 404;
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
// Send 404 file if found else not found message
|
|
430
|
+
const errorFile = path.join(htmlDir, FILE_404);
|
|
431
|
+
if (fs.existsSync(errorFile)) {
|
|
432
|
+
res.setHeader('Content-Type', 'text/html');
|
|
433
|
+
res.end(fs.readFileSync(errorFile));
|
|
434
|
+
} else {
|
|
435
|
+
res.end(NOT_FOUND_404_MESSAGE);
|
|
436
|
+
}
|
|
319
437
|
}
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
438
|
+
}).use((req, res, next) => { // Add trailing slash
|
|
439
|
+
if (1 < req.path.length && !req.path.endsWith('/') && !path.extname(req.path)) {
|
|
440
|
+
res.writeHead(301, { Location: req.path + '/' });
|
|
441
|
+
return res.end();
|
|
442
|
+
}
|
|
443
|
+
next();
|
|
444
|
+
}).use(assets)
|
|
445
|
+
|
|
324
446
|
|
|
447
|
+
// Start listening
|
|
448
|
+
newApp.listen(port)
|
|
449
|
+
newApp.server.on("close", () => { configs?.onHostEnd?.(port) });
|
|
450
|
+
newApp.server.on("error", (e) => { log(`Failed to start server: ${e.message}`); throw e; });
|
|
451
|
+
log(`Server listening at ${port} ... (Press 'r' to manually reload, Press 'Ctrl+c' to exit)`)
|
|
325
452
|
|
|
326
|
-
|
|
453
|
+
|
|
454
|
+
return newApp
|
|
327
455
|
}
|
|
328
456
|
async function Main() {
|
|
457
|
+
|
|
329
458
|
// Get all arguments
|
|
330
|
-
const
|
|
459
|
+
const rawArgs = process.argv.slice(2);
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
// Check if verbose
|
|
463
|
+
isVerbose = rawArgs.includes(VERBOSE_FLAG) || rawArgs.includes(VERBOSE_SHORT_FLAG);
|
|
331
464
|
|
|
332
465
|
|
|
333
466
|
// Check if asked for help
|
|
334
|
-
if (
|
|
467
|
+
if (rawArgs.includes(HELP_FLAG) || rawArgs.includes(HELP_SHORT_FLAG)) {
|
|
335
468
|
console.log(HELP_MESSAGE)
|
|
336
469
|
return;
|
|
337
470
|
}
|
|
338
471
|
|
|
339
472
|
|
|
340
|
-
//
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let inputPath = args.find(val => val.startsWith(INPUT_PATH_FLAG))
|
|
345
|
-
inputPath = inputPath !== undefined ? inputPath.split('=')[1] : process.cwd()
|
|
346
|
-
|
|
347
|
-
// Assign output path
|
|
348
|
-
let outputPath = args.find(val => val.startsWith(OUTPUT_PATH_FLAG))
|
|
349
|
-
let outputPathProvided = outputPath !== undefined
|
|
350
|
-
outputPath = outputPathProvided ? outputPath.split('=')[1] : createTempDir()
|
|
351
|
-
|
|
352
|
-
// Assign tracking changes
|
|
353
|
-
let toTrackChanges = args.includes(TRACK_CHANGES_FLAG) || args.includes(TRACK_CHANGES_SHORT_FLAG)
|
|
354
|
-
|
|
355
|
-
// Assign port
|
|
356
|
-
let port = args.find(val => val.startsWith(PORT_FLAG))
|
|
357
|
-
port = port !== undefined ? Number(port.split('=')[1]) : DEFAULT_PORT
|
|
358
|
-
|
|
359
|
-
// Assign verbose
|
|
360
|
-
isVerbose = args.includes(VERBOSE_FLAG) || args.includes(VERBOSE_SHORT_FLAG)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// Check input path
|
|
364
|
-
if (!fs.existsSync(inputPath) || !fs.lstatSync(inputPath).isDirectory()) {
|
|
365
|
-
log(`Invalid input path "${inputPath}"`)
|
|
366
|
-
return
|
|
473
|
+
// Filter arguments
|
|
474
|
+
let args = await filterArgs(rawArgs);
|
|
475
|
+
if (args === null) {
|
|
476
|
+
return;
|
|
367
477
|
}
|
|
368
478
|
|
|
369
|
-
// Check output path
|
|
370
|
-
if (!fs.existsSync(outputPath) || !fs.lstatSync(outputPath).isDirectory()) {
|
|
371
|
-
log(`Invalid output path "${outputPath}"`)
|
|
372
|
-
return
|
|
373
|
-
}
|
|
374
479
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
480
|
+
// Get config
|
|
481
|
+
let configFilePath = path.join(args.inputPath, `./${CONFIG_FILE_NAME}`)
|
|
482
|
+
if (fs.existsSync(configFilePath)) {
|
|
483
|
+
configs = await import(pathToFileURL(configFilePath).href);
|
|
379
484
|
}
|
|
380
485
|
|
|
381
486
|
|
|
382
487
|
// Create site from mdx & return if only needed to create site
|
|
383
|
-
await createSiteSafe(inputPath, outputPath)
|
|
384
|
-
if (toCreateOnly) {
|
|
488
|
+
let wasCreated = await createSiteSafe(args.inputPath, args.outputPath);
|
|
489
|
+
if (args.toCreateOnly) {
|
|
490
|
+
process.exitCode = !wasCreated ? 1 : 0; // Exit with error code if not created successfully
|
|
385
491
|
return;
|
|
386
492
|
}
|
|
387
493
|
|
|
388
494
|
|
|
389
495
|
// Watch for key presses
|
|
390
|
-
listenForKey(() => createSiteSafe(inputPath, outputPath))
|
|
496
|
+
listenForKey(() => createSiteSafe(args.inputPath, args.outputPath));
|
|
391
497
|
|
|
392
498
|
|
|
393
499
|
// Watch for changes
|
|
394
|
-
if (toTrackChanges) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
500
|
+
if (args.toTrackChanges) {
|
|
501
|
+
watchForChanges(args.inputPath, (event, path) => {
|
|
502
|
+
if (typeof configs.toTriggerRecreate === 'function' && !configs?.toTriggerRecreate(event, path)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
log(`Recreating site, Event: ${event}, Path: ${path}`, true)
|
|
507
|
+
createSiteSafe(args.inputPath, args.outputPath)
|
|
400
508
|
});
|
|
401
509
|
}
|
|
402
510
|
|
|
403
511
|
|
|
404
512
|
// Start server
|
|
405
|
-
|
|
513
|
+
app = startServer(args.outputPath, args.port);
|
|
406
514
|
|
|
407
515
|
|
|
408
516
|
// Handle quit
|
|
409
|
-
|
|
517
|
+
const cleanup = () => {
|
|
410
518
|
// Remove html path
|
|
411
|
-
if (!outputPathProvided && fs.existsSync(outputPath)) {
|
|
412
|
-
fs.rmSync(outputPath, { recursive: true, force: true })
|
|
519
|
+
if (!args.outputPathProvided && fs.existsSync(args.outputPath)) {
|
|
520
|
+
fs.rmSync(args.outputPath, { recursive: true, force: true })
|
|
413
521
|
}
|
|
414
522
|
|
|
415
|
-
process.
|
|
416
|
-
}
|
|
523
|
+
process.stdin.setRawMode(false);
|
|
524
|
+
}
|
|
525
|
+
process.on("exit", cleanup);
|
|
526
|
+
process.on("SIGINT", cleanup);
|
|
527
|
+
process.on("SIGTERM", cleanup);
|
|
417
528
|
}
|
|
418
529
|
|
|
419
530
|
Main()
|
package/mdx-to-html.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as _jsx_runtime from '
|
|
1
|
+
import * as Preact from "preact";
|
|
2
|
+
import * as PreactDOM from "preact/compat";
|
|
3
|
+
import * as _jsx_runtime from 'preact/jsx-runtime';
|
|
4
|
+
import { renderToString } from 'preact-render-to-string';
|
|
4
5
|
import { common } from 'lowlight';
|
|
5
6
|
import { bundleMDX } from 'mdx-bundler';
|
|
6
|
-
import { getMDXComponent } from 'mdx-bundler/client/index.js'
|
|
7
|
-
import { renderToString } from 'react-dom/server';
|
|
8
7
|
import { createRequire } from 'module';
|
|
8
|
+
import rehypeHighlight from "rehype-highlight";
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
// Constants
|
|
10
12
|
const nativeRequire = createRequire(import.meta.url);
|
|
13
|
+
const jsxBundlerConfig = {
|
|
14
|
+
jsxLib: {
|
|
15
|
+
varName: 'Preact',
|
|
16
|
+
package: 'preact',
|
|
17
|
+
},
|
|
18
|
+
jsxDom: {
|
|
19
|
+
varName: 'PreactDom',
|
|
20
|
+
package: 'preact/compat',
|
|
21
|
+
},
|
|
22
|
+
jsxRuntime: {
|
|
23
|
+
varName: '_jsx_runtime',
|
|
24
|
+
package: 'preact/jsx-runtime',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// Methods
|
|
30
|
+
function getMDXComponent(code, globals) {
|
|
31
|
+
const fn = new Function(...Object.keys(globals), code);
|
|
32
|
+
const mdxExport = fn(...Object.values(globals));
|
|
33
|
+
return mdxExport.default;
|
|
34
|
+
}
|
|
35
|
+
export async function mdxToHtml(mdxCode, baseUrl, globalArgs = {}, modSettingsCallback = undefined) {
|
|
11
36
|
|
|
12
|
-
export async function mdxToHtml(mdxCode, baseUrl, modSettingsCallback = undefined) {
|
|
13
|
-
|
|
14
37
|
// Assign default settings
|
|
15
38
|
let settings = {
|
|
16
39
|
source: mdxCode,
|
|
40
|
+
jsxConfig: jsxBundlerConfig,
|
|
17
41
|
cwd: baseUrl,
|
|
18
42
|
esbuildOptions: (options) => {
|
|
19
43
|
options.platform = 'node'
|
|
@@ -30,13 +54,15 @@ export async function mdxToHtml(mdxCode, baseUrl, modSettingsCallback = undefine
|
|
|
30
54
|
|
|
31
55
|
|
|
32
56
|
// Modify settings
|
|
33
|
-
if(modSettingsCallback !== undefined){
|
|
57
|
+
if (modSettingsCallback !== undefined) {
|
|
34
58
|
settings = modSettingsCallback(settings)
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
|
|
38
62
|
// Generate html
|
|
39
63
|
const { code } = await bundleMDX(settings);
|
|
40
|
-
const Component = getMDXComponent(code, { require: nativeRequire,
|
|
41
|
-
|
|
64
|
+
const Component = getMDXComponent(code, { Preact, PreactDOM, _jsx_runtime, require: nativeRequire, ...globalArgs })
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
return renderToString(Preact.h(Component, {}));
|
|
42
68
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "host-mdx",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "A cli tool to create and serve a static html website from a given mdx directory",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
@@ -10,20 +10,29 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"host-mdx": "index.js"
|
|
12
12
|
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ManasMakde/host-mdx/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ManasMakde/host-mdx.git"
|
|
19
|
+
},
|
|
13
20
|
"dependencies": {
|
|
14
21
|
"chokidar": "^5.0.0",
|
|
15
22
|
"ignore": "^7.0.5",
|
|
16
23
|
"lowlight": "^3.3.0",
|
|
17
24
|
"mdx-bundler": "^10.1.1",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
25
|
+
"polka": "^0.5.2",
|
|
26
|
+
"preact": "^10.28.2",
|
|
27
|
+
"preact-render-to-string": "^6.6.5",
|
|
28
|
+
"rehype-highlight": "^7.0.2",
|
|
29
|
+
"sirv": "^3.0.2"
|
|
21
30
|
},
|
|
22
31
|
"keywords": [
|
|
23
32
|
"mdx"
|
|
24
33
|
],
|
|
25
34
|
"author": "Manas Makde",
|
|
26
|
-
"license": "
|
|
35
|
+
"license": "MIT",
|
|
27
36
|
"devDependencies": {
|
|
28
37
|
"@babel/preset-react": "^7.28.5",
|
|
29
38
|
"@babel/register": "^7.28.3"
|