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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -21
  3. package/index.js +270 -159
  4. package/mdx-to-html.js +36 -10
  5. 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
- [![Version](https://img.shields.io/npm/v/host-mdx.svg)](https://www.npmjs.com/package/host-mdx )
4
-
2
+ [![Version](https://img.shields.io/npm/v/host-mdx.svg)](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 project as a config file with the following:
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
- ## Example
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
- > For a live example take a look at [sourcesnippet.github.io](https://sourcesnippet.github.io/)
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 http from 'http'
7
- import ignore from "ignore";
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 { mdxToHtml } from './mdx-to-html.js'
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 IGNORE_FILE_NAME = ".hostmdxignore"
17
- const CONFIG_FILE_NAME = "host-mdx.js"
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 = "--verobse"
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 isVerbose = false
59
- let configs
60
- let server
61
- const TEMP_HTML_DIR = path.join(os.tmpdir(), `${APP_NAME}`)
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
- // Delete existing temp dir
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
- // Create default temp html dir
108
- fs.mkdirSync(TEMP_HTML_DIR, { recursive: true })
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, `/html-`));
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("site creation already ongoing!")
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 && absToOutput.endsWith(".mdx")
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 htmlCode = await mdxToHtml(mdxCode, parentDir, configs?.modBundleMDXSettings);
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
- log("Created site")
251
- configs?.onSiteCreateEnd?.(inputPath, outputPath)
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
- isCreatingSite = false
262
- console.log(err);
263
- log("Failed to create site!");
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.close((e) => { process.exit() })
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 newServer = http.createServer((req, res) => {
291
-
292
- // Parse & Sanitize URL
293
- let parsedUrl = new URL("http://" + req.headers.host + req.url)
294
- let sanitizedUrl = path.normalize(parsedUrl.pathname).replace(/^(\.\.[\/\\])+/, '')
295
- let isDirectory = !Boolean(path.parse(sanitizedUrl).ext)
296
- let relativeFilePath = path.normalize(sanitizedUrl + (isDirectory ? "/index.html" : ""))
297
- let absoluteFilePath = path.join(path.resolve(htmlDir), relativeFilePath)
298
- let pathExists = fs.existsSync(absoluteFilePath)
299
-
300
- // Respondes with content of file
301
- if (pathExists)
302
- // read file from file system
303
- fs.readFile(absoluteFilePath, function (err, data) {
304
- if (err) {
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
- newServer.listen(port, () => { log(`Server listening at ${port} ... (Press 'r' to manually reload, Press 'Ctrl+c' to exit)`) })
322
- newServer.on("close", () => { configs?.onHostEnd?.(port) });
323
- newServer.on("error", (e) => { log(`Error Starting server ${e.message}`); throw e; });
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
- return newServer
453
+
454
+ return newApp
327
455
  }
328
456
  async function Main() {
457
+
329
458
  // Get all arguments
330
- const args = process.argv.slice(2)
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 (args.includes(HELP_FLAG) || args.includes(HELP_SHORT_FLAG)) {
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
- // Assign to create
341
- let toCreateOnly = args.includes(CREATE_FLAG) || args.includes(CREATE_SHORT_FLAG)
342
-
343
- // Assign input path
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
- // Check port
376
- if (!Number.isInteger(port)) {
377
- log(`Invalid port`)
378
- return
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
- chokidar.watch(inputPath, {
396
- ignoreInitial: true,
397
- ignored: (path, stats) => isCreatingSite // Ignore if site creation is ongoing
398
- }).on('all', (event, path) => {
399
- createSiteSafe(inputPath, outputPath)
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
- server = startServer(outputPath, port)
513
+ app = startServer(args.outputPath, args.port);
406
514
 
407
515
 
408
516
  // Handle quit
409
- process.on("exit", () => {
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.exit(0);
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 React from 'react';
2
- import rehypeHighlight from "rehype-highlight";
3
- import * as _jsx_runtime from 'react/jsx-runtime';
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, cwd: baseUrl })
41
- return renderToString(React.createElement(Component));
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.0.1",
4
- "description": "Creates and serves a github pages style html directory from a corresponding mdx directory",
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
- "react": "^19.2.3",
19
- "react-dom": "^19.2.3",
20
- "rehype-highlight": "^7.0.2"
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": "ISC",
35
+ "license": "MIT",
27
36
  "devDependencies": {
28
37
  "@babel/preset-react": "^7.28.5",
29
38
  "@babel/register": "^7.28.3"