host-mdx 2.1.0 → 2.2.0

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
@@ -1,7 +1,6 @@
1
1
  # host-mdx
2
2
 
3
- [![Version](https://img.shields.io/npm/v/host-mdx.svg)](https://www.npmjs.com/package/host-mdx )
4
-
3
+ [![Version](https://img.shields.io/npm/v/host-mdx.svg)](https://www.npmjs.com/package/host-mdx)\
5
4
  A cli tool to create and serve a static html website from a given mdx directory
6
5
 
7
6
  ## 🛠️ Usage
@@ -18,38 +17,45 @@ Options:
18
17
  --track-changes, -t Tracks any changes made & auto reloads
19
18
  --verobse, -v Shows additional log messages
20
19
  ```
21
-
22
- > If `--input-path` is not provided it will default to `./` i.e. current working directory
23
- > If `--output-path` is not provided a temp folder will be created automatically & deleted upon exit
24
20
 
25
- Add a file by the name `.hostmdxignore` at the root of your project to filter out which files/folders to skip while generating html
26
- (similar to [.gitignore](https://git-scm.com/docs/gitignore))
21
+ > If `--input-path` is not provided it will default to `./` i.e. current working directory\
22
+ > If `--output-path` is not provided a temp folder will be created automatically & deleted upon exit
27
23
 
24
+ You can add a file by the name `.hostmdxignore` at the root of your project to filter out which files/folders to skip while generating html
25
+ (similar to [.gitignore](https://git-scm.com/docs/gitignore))
28
26
 
29
- Add a file by the name `host-mdx.js` at the root of your input folder as a config file with the following:
27
+ You can also add a file by the name `host-mdx.js` at the root of your input folder as a config file with access to the following:
30
28
 
31
29
  ```js
32
- // Modify
33
- modBundleMDXSettings(settings)
34
-
35
-
36
- // Hooks
37
30
  onSiteCreateStart(inputPath, outputPath)
38
31
  onSiteCreateEnd(inputPath, outputPath, wasInterrupted)
39
- onFileCreateStart(inputFilePath, outputFilePath)
40
- onFileCreateEnd(inputFilePath, outputFilePath)
32
+ onFileCreateStart(inputPath, outputPath, inFilePath, outFilePath)
33
+ onFileCreateEnd(inputPath, outputPath, inFilePath, outFilePath, result)
34
+ modBundleMDXSettings(inputPath, outputPath, settings)
35
+ modGlobalArgs(inputPath, outputPath, globalArgs)
36
+ toTriggerRecreate(event, path)
37
+ ```
38
+
39
+ > **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)
40
+
41
+ Default global variables you can use inside any .mdx files:
42
+
43
+ ```
44
+ hostmdxCwd
45
+ hostmdxInputPath
46
+ hostmdxOutputPath
41
47
  ```
42
- > Note: Any changes made to `host-mdx.js` require complete restart otherwise changes will not reflect
43
48
 
44
49
  ## 📖 Example
45
50
 
46
51
  Command:
52
+
47
53
  ```bash
48
54
  npx host-mdx --input-path="path/to/my-website-template" --output-path="path/to/my-website" --port=3113 -t
49
55
  ```
50
56
 
51
-
52
57
  Input Directory:
58
+
53
59
  ```
54
60
  my-website-template/
55
61
  ├─ index.mdx
@@ -73,6 +79,7 @@ my-website-template/
73
79
  ```
74
80
 
75
81
  `.hostmdxignore` file content:
82
+
76
83
  ```sh
77
84
  *.jsx
78
85
  blog/page2/
@@ -81,32 +88,42 @@ static/temp.jpg
81
88
  ```
82
89
 
83
90
  `host-mdx.js` file content:
91
+
84
92
  ```js
85
93
  export function onSiteCreateStart(inputPath, outputPath) {
86
94
  console.log("onSiteCreateStart", inputPath, outputPath)
87
95
  }
88
- export function onSiteCreateEnd(inputPath, outputPath, wasSuccessful){
96
+ export function onSiteCreateEnd(inputPath, outputPath, wasSuccessful) {
89
97
  console.log("onSiteCreateEnd", inputPath, outputPath, wasSuccessful)
90
98
  }
91
- export function onFileCreateStart(inputFilePath, outputFilePath){
99
+ export function onFileCreateStart(inputFilePath, outputFilePath) {
92
100
  console.log("onFileCreateStart", inputFilePath, outputFilePath)
93
101
  }
94
- export function onFileCreateEnd(inputFilePath, outputFilePath){
102
+ export function onFileCreateEnd(inputFilePath, outputFilePath) {
95
103
  console.log("onFileCreateEnd", inputFilePath, outputFilePath)
96
104
  }
97
- export function onHostStart(port){
105
+ export function onHostStart(port) {
98
106
  console.log("onHostStart", port)
99
107
  }
100
- export function onHostEnd(port){
108
+ export function onHostEnd(port) {
101
109
  console.log("onHostEnd", port)
102
110
  }
103
- export function modBundleMDXSettings(settings){
111
+ export function modBundleMDXSettings(inputPath, outputPath, settings) {
104
112
  // Modify settings ...
105
113
  return settings
106
114
  }
115
+ export function toTriggerRecreate(event, path) {
116
+ const isGOutputStream = /\.goutputstream-\w+$/.test(path);
117
+ if (isGOutputStream) {
118
+ return false;
119
+ }
120
+
121
+ return true;
122
+ }
107
123
  ```
108
124
 
109
125
  Output Directory:
126
+
110
127
  ```
111
128
  my-website/
112
129
  ├─ index.html
@@ -124,7 +141,6 @@ my-website/
124
141
 
125
142
  The site will now be visible in the browser at `localhost:3113`
126
143
 
127
-
128
144
  ## 🔑 License
129
145
 
130
146
  MIT © [Manas Ravindra Makde](https://manasmakde.github.io/)
package/index.js CHANGED
@@ -1,24 +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 polka from 'polka';
7
7
  import sirv from 'sirv';
8
- import ignore from "ignore";
8
+ import polka from 'polka';
9
+ import ignore from 'ignore';
9
10
  import chokidar from 'chokidar';
10
11
  import * as readline from 'readline';
11
12
  import { pathToFileURL } from 'url';
12
- import { mdxToHtml } from './mdx-to-html.js'
13
+ import { mdxToHtml } from './mdx-to-html.js';
13
14
 
14
15
 
15
16
  // To-Set Properties
16
17
  const APP_NAME = "host-mdx";
17
18
  const DEFAULT_PORT = 3000;
19
+ const MAX_PORT = 3002;
18
20
  const IGNORE_FILE_NAME = ".hostmdxignore";
19
21
  const CONFIG_FILE_NAME = "host-mdx.js";
20
- const NOT_FOUND_404_FILE = "404.html"
21
- const NOT_FOUND_404_MESSAGE = "404"
22
+ const FILE_404 = "404.html";
23
+ const NOT_FOUND_404_MESSAGE = "404";
22
24
  const DEFAULT_IGNORES = `
23
25
  ${IGNORE_FILE_NAME}
24
26
  ${CONFIG_FILE_NAME}
@@ -26,6 +28,8 @@ node_modules
26
28
  package-lock.json
27
29
  package.json
28
30
  .git
31
+ .github
32
+ .gitignore
29
33
  `;
30
34
 
31
35
 
@@ -135,17 +139,9 @@ async function createSite(inputPath, outputPath) {
135
139
  isCreateSitePending = false
136
140
 
137
141
 
138
- // Get config properties
139
- let configFilePath = path.join(inputPath, `./${CONFIG_FILE_NAME}`)
140
- if (fs.existsSync(configFilePath)) {
141
-
142
- configs = await import(pathToFileURL(configFilePath).href);
143
- }
144
-
145
-
146
142
  // Broadcast site creation started
147
143
  log("Creating site...")
148
- configs?.onSiteCreateStart?.(inputPath, outputPath)
144
+ await configs?.onSiteCreateStart?.(inputPath, outputPath)
149
145
 
150
146
 
151
147
  // Remove html folder if it already exists
@@ -186,9 +182,9 @@ async function createSite(inputPath, outputPath) {
186
182
  // Make dir
187
183
  if (isDir) {
188
184
  log(`${currentPath} ---> ${absToOutput}`, true)
189
- configs?.onFileCreateStart?.(currentPath, absToOutput)
185
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput)
190
186
  fs.mkdirSync(absToOutput, { recursive: true });
191
- configs?.onFileCreateEnd?.(currentPath, absToOutput)
187
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined)
192
188
  }
193
189
  // Make html file from mdx
194
190
  else if (!isDir && isMdx) {
@@ -196,25 +192,32 @@ async function createSite(inputPath, outputPath) {
196
192
  // Broadcast file creation started
197
193
  let absHtmlPath = path.format({ ...path.parse(absToOutput), base: '', ext: '.html' })
198
194
  log(`${currentPath} ---> ${absHtmlPath}`, true)
199
- configs?.onFileCreateStart?.(currentPath, absHtmlPath)
195
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath, undefined)
200
196
 
201
197
 
202
198
  // convert mdx code into html & paste into file
203
199
  let mdxCode = fs.readFileSync(currentPath, 'utf8');
204
200
  let parentDir = path.dirname(currentPath)
205
- let htmlCode = await mdxToHtml(mdxCode, parentDir, configs?.modBundleMDXSettings);
206
- createFile(absHtmlPath, `<!DOCTYPE html>\n${htmlCode}`);
201
+ let globalArgs = {
202
+ hostmdxCwd: parentDir,
203
+ hostmdxInputPath: inputPath,
204
+ hostmdxOutputPath: outputPath
205
+ };
206
+ globalArgs = await configs?.modGlobalArgs?.(inputPath, outputPath, globalArgs) ?? globalArgs;
207
+ let result = await mdxToHtml(mdxCode, parentDir, globalArgs, async (settings) => { return await configs?.modBundleMDXSettings?.(inputPath, outputPath, settings) ?? settings });
208
+ let htmlCode = result.html;
209
+ createFile(absHtmlPath, `<!DOCTYPE html>${htmlCode}`);
207
210
 
208
211
 
209
212
  // Broadcast file creation ended
210
- configs?.onFileCreateEnd?.(currentPath, absHtmlPath)
213
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result)
211
214
  }
212
215
  // Copy paste file
213
216
  else if (!isDir) {
214
217
  log(`${currentPath} ---> ${absToOutput}`, true)
215
- configs?.onFileCreateStart?.(currentPath, absToOutput)
218
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput)
216
219
  fs.copyFileSync(currentPath, absToOutput)
217
- configs?.onFileCreateEnd?.(currentPath, absToOutput)
220
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined)
218
221
  }
219
222
 
220
223
 
@@ -237,16 +240,75 @@ async function createSite(inputPath, outputPath) {
237
240
 
238
241
 
239
242
  // Broadcast site creation ended
240
- log(`Created site at ${outputPath}`)
241
- configs?.onSiteCreateEnd?.(inputPath, outputPath, isCreateSitePending)
243
+ if (isCreateSitePending) {
244
+ log(`Restarting site creation...`)
245
+ }
246
+ else {
247
+ log(`Created site at ${outputPath}`)
248
+ }
249
+ await configs?.onSiteCreateEnd?.(inputPath, outputPath, isCreateSitePending)
242
250
 
243
251
 
244
252
  // Reinvoke creation
245
- if(isCreateSitePending){
253
+ if (isCreateSitePending) {
246
254
  await createSite(inputPath, outputPath);
247
255
  }
248
256
  }
249
- function filterArgs(rawArgs) {
257
+ async function isPortAvailable(port) {
258
+ const server = net.createServer();
259
+ server.unref();
260
+
261
+ return new Promise((resolve) => {
262
+ server.once('error', () => {
263
+ server.close();
264
+ resolve(false);
265
+ });
266
+
267
+ server.once('listening', () => {
268
+ server.close(() => resolve(true));
269
+ });
270
+
271
+ server.listen(port);
272
+ });
273
+ }
274
+ async function getAvailablePort(startPort, maxPort) {
275
+ let currentPort = startPort;
276
+ while (currentPort <= maxPort) {
277
+ if (await isPortAvailable(currentPort)) {
278
+ return currentPort;
279
+ }
280
+
281
+ currentPort++;
282
+ }
283
+
284
+ return -1;
285
+ }
286
+ function stripTrailingSep(thePath) {
287
+ if (thePath[thePath.length - 1] === path.sep) {
288
+ return thePath.slice(0, -1);
289
+ }
290
+ return thePath;
291
+ }
292
+ function isSubPath(potentialParent, thePath) {
293
+ // For inside-directory checking, we want to allow trailing slashes, so normalize.
294
+ thePath = stripTrailingSep(thePath);
295
+ potentialParent = stripTrailingSep(potentialParent);
296
+
297
+
298
+ // Node treats only Windows as case-insensitive in its path module; we follow those conventions.
299
+ if (process.platform === "win32") {
300
+ thePath = thePath.toLowerCase();
301
+ potentialParent = potentialParent.toLowerCase();
302
+ }
303
+
304
+
305
+ return thePath.lastIndexOf(potentialParent, 0) === 0 &&
306
+ (
307
+ thePath[potentialParent.length] === path.sep ||
308
+ thePath[potentialParent.length] === undefined
309
+ );
310
+ }
311
+ async function filterArgs(rawArgs) {
250
312
  // Assign to create
251
313
  let toCreateOnly = rawArgs.includes(CREATE_FLAG) || rawArgs.includes(CREATE_SHORT_FLAG)
252
314
 
@@ -283,14 +345,25 @@ function filterArgs(rawArgs) {
283
345
  }
284
346
 
285
347
 
348
+ // Check if output path is inside input path (causing infinite loop)
349
+ if (isSubPath(inputPath, outputPath)) {
350
+ log(`Output path "${outputPath}" cannot be inside or same as input path "${inputPath}"`);
351
+ return null;
352
+ }
353
+
354
+
286
355
  // Assign port
287
356
  let port = rawArgs.find(val => val.startsWith(PORT_FLAG));
288
357
  let portProvided = port !== undefined;
289
- port = portProvided ? Number(port.split('=')[1]) : DEFAULT_PORT;
358
+ port = portProvided ? Number(port.split('=')[1]) : (await getAvailablePort(DEFAULT_PORT, MAX_PORT));
290
359
 
291
360
 
292
361
  // Check port
293
- if (!Number.isInteger(port)) {
362
+ if (port === -1) {
363
+ log(`Could not find any available ports between ${DEFAULT_PORT} to ${MAX_PORT}, Try manually passing ${PORT_FLAG}=... flag`);
364
+ return null;
365
+ }
366
+ else if (!Number.isInteger(port)) {
294
367
  log(`Invalid port`)
295
368
  return null;
296
369
  }
@@ -314,7 +387,7 @@ async function createSiteSafe(...args) {
314
387
  catch (err) {
315
388
  success = false;
316
389
  isCreatingSite = false;
317
- log(`Failed to create site!\n${err}`);
390
+ log(`Failed to create site!\n${err.stack}`);
318
391
  }
319
392
 
320
393
  return success;
@@ -332,27 +405,31 @@ async function listenForKey(createSiteCallback) {
332
405
  createSiteCallback();
333
406
  }
334
407
  else if (key && key.sequence == '\x03') {
335
- app.server.close((e) => { process.exit() })
408
+ app?.server?.close((e) => { process.exit() })
336
409
  }
337
410
  });
338
411
  }
339
- function startServer(htmlDir, port) { // Starts server at given port
412
+ async function watchForChanges(pathTowatch, callback) {
413
+ chokidar.watch(pathTowatch, {
414
+ ignoreInitial: true
415
+ }).on('all', callback);
416
+ }
417
+ async function startServer(htmlDir, port) { // Starts server at given port
340
418
 
341
419
  // Broadcast server starting
342
- configs?.onHostStart?.(port)
420
+ await configs?.onHostStart?.(port)
343
421
 
344
422
 
345
423
  // Start Server
346
424
  const assets = sirv(htmlDir, { dev: true });
347
425
  const newApp = polka({
348
426
  onNoMatch: (req, res) => {
349
-
350
427
  // Set status code to 404
351
428
  res.statusCode = 404;
352
429
 
353
430
 
354
- // Send 404 file if found otherwise default not found message
355
- const errorFile = path.join(htmlDir, NOT_FOUND_404_FILE);
431
+ // Send 404 file if found else not found message
432
+ const errorFile = path.join(htmlDir, FILE_404);
356
433
  if (fs.existsSync(errorFile)) {
357
434
  res.setHeader('Content-Type', 'text/html');
358
435
  res.end(fs.readFileSync(errorFile));
@@ -360,10 +437,18 @@ function startServer(htmlDir, port) { // Starts server at given port
360
437
  res.end(NOT_FOUND_404_MESSAGE);
361
438
  }
362
439
  }
440
+ }).use((req, res, next) => { // Add trailing slash
441
+ if (1 < req.path.length && !req.path.endsWith('/') && !path.extname(req.path)) {
442
+ res.writeHead(301, { Location: req.path + '/' });
443
+ return res.end();
444
+ }
445
+ next();
363
446
  }).use(assets)
364
447
 
448
+
449
+ // Start listening
365
450
  newApp.listen(port)
366
- newApp.server.on("close", () => { configs?.onHostEnd?.(port) });
451
+ newApp.server.on("close", async () => { await configs?.onHostEnd?.(port) });
367
452
  newApp.server.on("error", (e) => { log(`Failed to start server: ${e.message}`); throw e; });
368
453
  log(`Server listening at ${port} ... (Press 'r' to manually reload, Press 'Ctrl+c' to exit)`)
369
454
 
@@ -371,6 +456,7 @@ function startServer(htmlDir, port) { // Starts server at given port
371
456
  return newApp
372
457
  }
373
458
  async function Main() {
459
+
374
460
  // Get all arguments
375
461
  const rawArgs = process.argv.slice(2);
376
462
 
@@ -387,12 +473,20 @@ async function Main() {
387
473
 
388
474
 
389
475
  // Filter arguments
390
- let args = filterArgs(rawArgs);
476
+ let args = await filterArgs(rawArgs);
391
477
  if (args === null) {
392
478
  return;
393
479
  }
394
480
 
395
481
 
482
+ // Get config
483
+ let configFilePath = path.join(args.inputPath, `./${CONFIG_FILE_NAME}`)
484
+ if (fs.existsSync(configFilePath)) {
485
+ log(`Importing config file ${CONFIG_FILE_NAME}`);
486
+ configs = await import(pathToFileURL(configFilePath).href);
487
+ }
488
+
489
+
396
490
  // Create site from mdx & return if only needed to create site
397
491
  let wasCreated = await createSiteSafe(args.inputPath, args.outputPath);
398
492
  if (args.toCreateOnly) {
@@ -407,16 +501,19 @@ async function Main() {
407
501
 
408
502
  // Watch for changes
409
503
  if (args.toTrackChanges) {
410
- chokidar.watch(args.inputPath, {
411
- ignoreInitial: true
412
- }).on('all', (event, path) => {
504
+ watchForChanges(args.inputPath, async (event, path) => {
505
+ if (typeof configs?.toTriggerRecreate === 'function' && !(await configs?.toTriggerRecreate(event, path))) {
506
+ return;
507
+ }
508
+
509
+ log(`Recreating site, Event: ${event}, Path: ${path}`, true)
413
510
  createSiteSafe(args.inputPath, args.outputPath)
414
511
  });
415
512
  }
416
513
 
417
514
 
418
515
  // Start server
419
- app = startServer(args.outputPath, args.port);
516
+ app = await startServer(args.outputPath, args.port);
420
517
 
421
518
 
422
519
  // Handle quit
@@ -425,6 +522,8 @@ async function Main() {
425
522
  if (!args.outputPathProvided && fs.existsSync(args.outputPath)) {
426
523
  fs.rmSync(args.outputPath, { recursive: true, force: true })
427
524
  }
525
+
526
+ process.stdin.setRawMode(false);
428
527
  }
429
528
  process.on("exit", cleanup);
430
529
  process.on("SIGINT", cleanup);
package/mdx-to-html.js CHANGED
@@ -27,12 +27,11 @@ const jsxBundlerConfig = {
27
27
 
28
28
 
29
29
  // Methods
30
- function getMDXComponent(code, globals) {
30
+ function getMDXExport(code, globals) {
31
31
  const fn = new Function(...Object.keys(globals), code);
32
- const mdxExport = fn(...Object.values(globals));
33
- return mdxExport.default;
32
+ return fn(...Object.values(globals));
34
33
  }
35
- export async function mdxToHtml(mdxCode, baseUrl, modSettingsCallback = undefined) {
34
+ export async function mdxToHtml(mdxCode, baseUrl, globalArgs = {}, modSettingsCallback = undefined) {
36
35
 
37
36
  // Assign default settings
38
37
  let settings = {
@@ -55,14 +54,18 @@ export async function mdxToHtml(mdxCode, baseUrl, modSettingsCallback = undefine
55
54
 
56
55
  // Modify settings
57
56
  if (modSettingsCallback !== undefined) {
58
- settings = modSettingsCallback(settings)
57
+ settings = await modSettingsCallback(settings)
59
58
  }
60
59
 
61
60
 
62
61
  // Generate html
63
62
  const { code } = await bundleMDX(settings);
64
- const Component = getMDXComponent(code, { Preact, PreactDOM, _jsx_runtime, require: nativeRequire, cwd: baseUrl })
63
+ const Exports = getMDXExport(code, { Preact, PreactDOM, _jsx_runtime, require: nativeRequire, ...globalArgs });
64
+ const Component = Exports.default;
65
65
 
66
66
 
67
- return renderToString(Preact.h(Component, {}));
67
+ return {
68
+ html: renderToString(Preact.h(Component, {})),
69
+ exports: Exports
70
+ }
68
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "host-mdx",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
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",
@@ -10,6 +10,13 @@
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",
@@ -25,7 +32,7 @@
25
32
  "mdx"
26
33
  ],
27
34
  "author": "Manas Makde",
28
- "license": "ISC",
35
+ "license": "MIT",
29
36
  "devDependencies": {
30
37
  "@babel/preset-react": "^7.28.5",
31
38
  "@babel/register": "^7.28.3"