host-mdx 2.2.1 → 2.3.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/index.js CHANGED
@@ -1,26 +1,39 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import os from 'os';
5
- import net from 'net';
6
- import path from 'path'
7
- import sirv from 'sirv';
8
- import polka from 'polka';
9
- import ignore from 'ignore';
10
- import chokidar from 'chokidar';
11
- import * as readline from 'readline';
12
- import { pathToFileURL } from 'url';
13
- import { mdxToHtml } from './mdx-to-html.js';
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import net from "net";
4
+ import path from "path";
5
+ import sirv from "sirv";
6
+ import polka from "polka";
7
+ import ignore from "ignore";
8
+ import pLimit from 'p-limit';
9
+ import chokidar from "chokidar";
10
+ import { pathToFileURL } from "url";
11
+ import { promises as fsp } from "fs";
12
+ import { mdxToHtml } from "./mdx-to-html.js";
13
+
14
+
15
+ // Enums
16
+ export const TrackChanges = Object.freeze({
17
+ NONE: 0,
18
+ SOFT: 1,
19
+ HARD: 2
20
+ });
21
+ export const SiteCreationStatus = Object.freeze({
22
+ NONE: 0,
23
+ PENDING_RECREATION: 1,
24
+ ONGOING: 2,
25
+ });
14
26
 
15
27
 
16
28
  // To-Set Properties
17
29
  const APP_NAME = "host-mdx";
18
- const DEFAULT_PORT = 3000;
19
- const MAX_PORT = 3002;
20
30
  const IGNORE_FILE_NAME = ".hostmdxignore";
21
31
  const CONFIG_FILE_NAME = "host-mdx.js";
22
32
  const FILE_404 = "404.html";
23
33
  const NOT_FOUND_404_MESSAGE = "404";
34
+ const DEFAULT_PORT = 3000;
35
+ const MAX_PORT = 4000;
36
+ const TEMP_HTML_DIR = path.join(os.tmpdir(), `${APP_NAME}`);
24
37
  const DEFAULT_IGNORES = `
25
38
  ${IGNORE_FILE_NAME}
26
39
  ${CONFIG_FILE_NAME}
@@ -33,63 +46,138 @@ package.json
33
46
  `;
34
47
 
35
48
 
36
- // Flags
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";
48
-
49
-
50
- // Messages & Errors
51
- const HELP_MESSAGE = `Usage: host-mdx [options]
52
-
53
- Options:
54
- ${CREATE_FLAG}, ${CREATE_SHORT_FLAG} Only create the html website from mdx does not host
55
- ${HELP_FLAG}, ${HELP_SHORT_FLAG} Shows all available options
56
- ${INPUT_PATH_FLAG}=... The path at which all mdx files are stored
57
- ${OUTPUT_PATH_FLAG}=... The path to which all html files will be generated
58
- ${PORT_FLAG}=... Localhost port number on which to host
59
- ${TRACK_CHANGES_FLAG}, ${TRACK_CHANGES_SHORT_FLAG} Tracks any changes made & auto reloads
60
- ${VERBOSE_FLAG}, ${VERBOSE_SHORT_FLAG} Shows additional log messages
61
- `;
62
-
63
-
64
- // Private Properties
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}`);
71
- const TIME_OPTIONS = {
72
- year: 'numeric',
73
- month: 'long',
74
- day: 'numeric',
75
- hour: 'numeric',
76
- minute: 'numeric',
77
- second: 'numeric',
49
+ // Properties
50
+ const LOG_TIME_OPTIONS = {
51
+ year: "numeric",
52
+ month: "long",
53
+ day: "numeric",
54
+ hour: "numeric",
55
+ minute: "numeric",
56
+ second: "numeric",
78
57
  hour12: false,
79
58
  fractionalSecondDigits: 3
80
59
  };
60
+ const DEFAULT_CHOKIDAR_OPTIONS = {
61
+ ignoreInitial: true
62
+ };
81
63
 
82
64
 
83
65
  // Utility Methods
84
- function log(msg, checkVerbose = false) {
85
- if (checkVerbose && !isVerbose) {
66
+ function getIgnore(ignoreFilePath) {
67
+ const ig = ignore();
68
+ let ignoreContent = DEFAULT_IGNORES;
69
+ if (fs.existsSync(ignoreFilePath)) {
70
+ ignoreContent += `\n${fs.readFileSync(ignoreFilePath, "utf8")}`;
71
+ }
72
+
73
+ ig.add(ignoreContent);
74
+
75
+ return ig;
76
+ }
77
+ async function createFile(filePath, fileContent = "") {
78
+ let fileLocation = path.dirname(filePath)
79
+ await fsp.mkdir(fileLocation, { recursive: true });
80
+ await fsp.writeFile(filePath, fileContent);
81
+ }
82
+ function crawlDir(dir) {
83
+ const absDir = path.resolve(dir);
84
+ let entries = fs.readdirSync(absDir, { recursive: true });
85
+ return entries.map(file => path.join(absDir, file));
86
+ }
87
+ async function setupConfigs(configFilePath) {
88
+ if (fs.existsSync(configFilePath)) {
89
+ let cleanConfigFilePath = pathToFileURL(configFilePath).href
90
+ return await import(cleanConfigFilePath);
91
+ }
92
+
93
+ return {};
94
+ }
95
+ async function startServer(hostDir, port, errorCallback) { // Starts server at given port
96
+
97
+ // Make sure host dir path is absolute
98
+ hostDir = path.resolve(hostDir);
99
+
100
+
101
+ // Start Server
102
+ const assets = sirv(hostDir, { dev: true });
103
+ const newApp = polka({
104
+ onNoMatch: (req, res) => { // Send 404 file if found else not found message
105
+ const errorFile = path.join(hostDir, FILE_404);
106
+ if (fs.existsSync(errorFile)) {
107
+ const content = fs.readFileSync(errorFile);
108
+ res.writeHead(404, {
109
+ 'Content-Type': 'text/html',
110
+ 'Content-Length': content.length
111
+ });
112
+ res.end(content);
113
+ } else {
114
+ res.statusCode = 404;
115
+ res.end(NOT_FOUND_404_MESSAGE);
116
+ }
117
+ }
118
+ }).use((req, res, next) => { // Add trailing slash
119
+ if (1 < req.path.length && !req.path.endsWith('/') && !path.extname(req.path)) {
120
+ res.writeHead(301, { Location: req.path + '/' });
121
+ return res.end();
122
+ }
123
+ next();
124
+ }).use(assets)
125
+
126
+
127
+ // Start listening
128
+ newApp.listen(port)
129
+ newApp.server.on("error", errorCallback);
130
+
131
+
132
+ return newApp
133
+ }
134
+ async function isPortAvailable(port) {
135
+ const server = net.createServer();
136
+ server.unref();
137
+
138
+ return new Promise((resolve) => {
139
+ server.once("error", () => {
140
+ server.close();
141
+ resolve(false);
142
+ });
143
+
144
+ server.once("listening", () => {
145
+ server.close(() => resolve(true));
146
+ });
147
+
148
+ server.listen(port);
149
+ });
150
+ }
151
+ export function log(msg, toSkip = false) {
152
+ if (toSkip) { // Useful for verbose check
86
153
  return
87
154
  }
88
155
 
89
- let timestamp = new Date().toLocaleString(undefined, TIME_OPTIONS)
156
+ let timestamp = new Date().toLocaleString(undefined, LOG_TIME_OPTIONS)
90
157
  console.log(`[${APP_NAME} ${timestamp}] ${msg}`)
91
158
  }
92
- function createTempDir() {
159
+ export function isPathInside(parentPath, childPath) {
160
+
161
+ // Make sure both are absolute paths
162
+ parentPath = parentPath !== "" ? path.resolve(parentPath) : "";
163
+ childPath = childPath !== "" ? path.resolve(childPath) : "";
164
+
165
+
166
+ // Check if parent & child are same
167
+ if (parentPath === childPath) {
168
+ return true;
169
+ }
170
+
171
+
172
+ const relation = path.relative(parentPath, childPath);
173
+ return Boolean(
174
+ relation &&
175
+ relation !== '..' &&
176
+ !relation.startsWith(`..${path.sep}`) &&
177
+ relation !== path.resolve(childPath)
178
+ );
179
+ }
180
+ export function createTempDir() {
93
181
  // Create default temp html dir
94
182
  fs.mkdirSync(TEMP_HTML_DIR, { recursive: true });
95
183
 
@@ -101,440 +189,401 @@ function createTempDir() {
101
189
 
102
190
  return fs.mkdtempSync(path.join(TEMP_HTML_DIR, `html-${timestamp}-`));
103
191
  }
104
- function getIgnore(ignoreFilePath) {
105
- const ig = ignore();
106
- let ignoreContent = DEFAULT_IGNORES
107
-
108
- if (fs.existsSync(ignoreFilePath)) {
109
- ignoreContent += `\n${fs.readFileSync(ignoreFilePath, "utf8")}`
192
+ export function emptyDir(dirPath) {
193
+ const files = fs.readdirSync(dirPath);
194
+ for (const file of files) {
195
+ const fullPath = path.join(dirPath, file);
196
+ fs.rmSync(fullPath, { recursive: true, force: true });
110
197
  }
198
+ }
199
+ export async function getAvailablePort(startPort = DEFAULT_PORT, maxPort = MAX_PORT) {
200
+ let currentPort = startPort;
201
+ while (currentPort <= maxPort) {
202
+ if (await isPortAvailable(currentPort)) {
203
+ return currentPort;
204
+ }
111
205
 
112
- ig.add(ignoreContent);
206
+ currentPort++;
207
+ }
113
208
 
114
- return ig
209
+ return -1;
115
210
  }
116
- function createFile(filePath, fileContent = "") {
211
+ export async function createSite(inputPath = "", outputPath = "", pathsToCreate = [], ignores = undefined, configs = undefined, interruptCondition = undefined) {
117
212
 
118
- // Check if path for file exists
119
- let fileLocation = path.dirname(filePath)
120
- if (!fs.existsSync(fileLocation)) {
121
- fs.mkdirSync(fileLocation, { recursive: true });
122
- }
213
+ // Check `inputPath`
214
+ inputPath = inputPath !== "" ? inputPath : process.cwd();
123
215
 
124
216
 
125
- // Create file
126
- fs.writeFileSync(filePath, fileContent);
127
- }
128
- async function createSite(inputPath, outputPath) {
129
- // Exit if already creating
130
- if (isCreatingSite) {
131
- log("Site creation already ongoing! Added to pending")
132
- isCreateSitePending = true
133
- return
134
- }
217
+ // Check `outputPath`
218
+ outputPath = outputPath !== "" ? outputPath : createTempDir();
135
219
 
136
220
 
137
- // Set creating status to ongoing
138
- isCreatingSite = true
139
- isCreateSitePending = false
221
+ // Get input path
222
+ if (!fs.existsSync(inputPath) || !fs.lstatSync(inputPath)?.isDirectory()) {
223
+ throw new Error(`Invalid input path "${inputPath}"`);
224
+ }
140
225
 
141
226
 
142
- // Broadcast site creation started
143
- log("Creating site...")
144
- await configs?.onSiteCreateStart?.(inputPath, outputPath)
227
+ // Get output path exists & is a directory
228
+ if (!fs.existsSync(outputPath) || !fs.lstatSync(outputPath).isDirectory()) {
229
+ throw new Error(`Invalid output path "${outputPath}"`);
230
+ }
145
231
 
146
232
 
147
- // Remove html folder if it already exists
148
- if (fs.existsSync(outputPath)) {
149
- fs.rmSync(outputPath, { recursive: true, force: true });
233
+ // Check if `outputPath` is inside `inputPath` (causing infinite loop)
234
+ if (isPathInside(inputPath, outputPath)) {
235
+ throw new Error(`Output path "${outputPath}" cannot be inside or same as input path "${inputPath}"`);
150
236
  }
151
237
 
152
238
 
153
- // Setup ignore
154
- let ignoreFilePath = path.join(inputPath, IGNORE_FILE_NAME)
155
- let ig = getIgnore(ignoreFilePath)
239
+ // Check if `inputPath` is inside `outputPath` (causing code wipeout)
240
+ if (isPathInside(outputPath, inputPath)) {
241
+ throw `Input path "${inputPath}" cannot be inside or same as output path "${outputPath}"`;
242
+ }
156
243
 
157
244
 
158
- // Iterate through all folders & files
159
- const stack = [inputPath];
160
- while (stack.length > 0 && !isCreateSitePending) {
161
- // Continue if path does not exist
162
- const currentPath = stack.pop()
163
- if (!fs.existsSync(currentPath)) {
164
- continue;
165
- }
245
+ // Check for verbose
246
+ const toBeVerbose = configs?.toBeVerbose === true;
166
247
 
167
248
 
168
- // Get essentials
169
- const relToInput = path.relative(inputPath, currentPath)
170
- const toIgnore = inputPath != currentPath && ig.ignores(relToInput)
171
- const absToOutput = path.join(outputPath, relToInput)
172
- const isDir = fs.statSync(currentPath).isDirectory()
173
- const isMdx = !isDir && currentPath.endsWith(".mdx")
249
+ // Check `interruptCondition` provided
250
+ if (typeof interruptCondition !== 'function') {
251
+ interruptCondition = async () => false;
252
+ }
174
253
 
175
254
 
176
- // Skip if to ignore this path
177
- if (toIgnore) {
178
- continue
179
- }
255
+ // Hard reload, clear output path & Get all paths from `inputPath`
256
+ if (pathsToCreate == null) {
257
+ emptyDir(outputPath)
258
+ pathsToCreate = crawlDir(inputPath);
259
+ }
180
260
 
181
261
 
182
- // Make dir
183
- if (isDir) {
184
- log(`${currentPath} ---> ${absToOutput}`, true)
185
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput)
186
- fs.mkdirSync(absToOutput, { recursive: true });
187
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined)
188
- }
189
- // Make html file from mdx
190
- else if (!isDir && isMdx) {
262
+ // Setup .ignore file
263
+ if (ignores === undefined) {
264
+ let ignoreFilePath = path.join(inputPath, IGNORE_FILE_NAME);
265
+ ignores = getIgnore(ignoreFilePath)
266
+ }
191
267
 
192
- // Broadcast file creation started
193
- let absHtmlPath = path.format({ ...path.parse(absToOutput), base: '', ext: '.html' })
194
- log(`${currentPath} ---> ${absHtmlPath}`, true)
195
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath)
196
268
 
269
+ // Setup configs
270
+ if (configs === undefined) {
271
+ let configFilePath = path.join(inputPath, `./${CONFIG_FILE_NAME}`);
272
+ let doesConfigFileExists = fs.existsSync(configFilePath);
273
+ log(`Importing config file ${configFilePath}`, !doesConfigFileExists);
274
+ configs = await setupConfigs(configFilePath);
275
+ }
197
276
 
198
- // Intercept mdx code
199
- let mdxCode = fs.readFileSync(currentPath, 'utf8');
200
- if (typeof configs?.modMDXCode === 'function') {
201
- log(`Modifying mdx code of ${currentPath}`, true);
202
- mdxCode = await configs?.modMDXCode(inputPath, outputPath, currentPath, absHtmlPath, mdxCode);
203
- }
204
-
205
-
206
- // convert mdx code into html & paste into file
207
- let parentDir = path.dirname(currentPath);
208
- let globalArgs = {
209
- hostmdxCwd: parentDir,
210
- hostmdxInputPath: inputPath,
211
- hostmdxOutputPath: outputPath
212
- };
213
- globalArgs = await configs?.modGlobalArgs?.(inputPath, outputPath, globalArgs) ?? globalArgs;
214
- let result = await mdxToHtml(mdxCode, parentDir, globalArgs, async (settings) => { return await configs?.modBundleMDXSettings?.(inputPath, outputPath, settings) ?? settings });
215
- let htmlCode = result.html;
216
- createFile(absHtmlPath, `<!DOCTYPE html>${htmlCode}`);
217
277
 
278
+ // Setup concurrency limit
279
+ const concurrency = configs?.concurrency ?? 1;
280
+ log(`Setting concurrency to ${concurrency}`, !toBeVerbose);
281
+ const limit = pLimit(concurrency);
218
282
 
219
- // Broadcast file creation ended
220
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result)
221
- }
222
- // Copy paste file
223
- else if (!isDir) {
224
- log(`${currentPath} ---> ${absToOutput}`, true)
225
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput)
226
- fs.copyFileSync(currentPath, absToOutput)
227
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined)
283
+
284
+ // Filter out paths based on ignore
285
+ const filterResults = await Promise.all(pathsToCreate.map(async (currentPath) => limit(async () => {
286
+ // Filter out input path itself if passed
287
+ if (inputPath === currentPath) {
288
+ return false;
228
289
  }
229
290
 
230
291
 
231
- // Skip if current path is a file or a directory to ignore
232
- if (!isDir) {
233
- continue
292
+ // Filter based on .ignore file
293
+ const relToInput = path.relative(inputPath, currentPath);
294
+ if (ignores.ignores(relToInput)) {
295
+ return false;
234
296
  }
235
297
 
236
298
 
237
- // Add to stack if current path is dir
238
- const files = fs.readdirSync(currentPath);
239
- for (const file of files) {
240
- stack.push(path.join(currentPath, file));
299
+ // Filter based on toIgnore() in configs
300
+ const toIgnore = await configs?.toIgnore?.(inputPath, outputPath, currentPath);
301
+ if (toIgnore) {
302
+ return false;
241
303
  }
242
- }
243
-
244
304
 
245
- // Unset creating status & Notify
246
- isCreatingSite = false;
305
+ return true;
306
+ })));
307
+ pathsToCreate = pathsToCreate.filter((_, index) => filterResults[index]);
247
308
 
248
309
 
249
- // Broadcast site creation ended
250
- if (isCreateSitePending) {
251
- log(`Restarting site creation...`)
252
- }
253
- else {
254
- log(`Created site at ${outputPath}`)
310
+ // Return if no paths remaining to create after filtering for ignores
311
+ if (pathsToCreate.length === 0) {
312
+ log(`Skipping site creation since no paths to create`, !toBeVerbose);
313
+ return;
255
314
  }
256
- await configs?.onSiteCreateEnd?.(inputPath, outputPath, isCreateSitePending)
257
315
 
258
316
 
259
- // Reinvoke creation
260
- if (isCreateSitePending) {
261
- await createSite(inputPath, outputPath);
262
- }
263
- }
264
- async function isPortAvailable(port) {
265
- const server = net.createServer();
266
- server.unref();
317
+ // Modify rebuild paths based on configs
318
+ pathsToCreate = await configs?.modRebuildPaths?.(inputPath, outputPath, pathsToCreate) ?? pathsToCreate;
267
319
 
268
- return new Promise((resolve) => {
269
- server.once('error', () => {
270
- server.close();
271
- resolve(false);
272
- });
273
320
 
274
- server.once('listening', () => {
275
- server.close(() => resolve(true));
276
- });
321
+ // Broadcast site creation started
322
+ log("Creating site...");
323
+ await configs?.onSiteCreateStart?.(inputPath, outputPath);
277
324
 
278
- server.listen(port);
279
- });
280
- }
281
- async function getAvailablePort(startPort, maxPort) {
282
- let currentPort = startPort;
283
- while (currentPort <= maxPort) {
284
- if (await isPortAvailable(currentPort)) {
285
- return currentPort;
325
+
326
+ // Iterate through all folders & files
327
+ let wasInterrupted = false;
328
+ await Promise.all(pathsToCreate.map((currentPath) => limit(async () => {
329
+
330
+ // Check for interruption & return
331
+ wasInterrupted = wasInterrupted || await interruptCondition(inputPath, outputPath, currentPath);
332
+ if (wasInterrupted) {
333
+ return;
286
334
  }
287
335
 
288
- currentPort++;
289
- }
290
336
 
291
- return -1;
292
- }
293
- function stripTrailingSep(thePath) {
294
- if (thePath[thePath.length - 1] === path.sep) {
295
- return thePath.slice(0, -1);
296
- }
297
- return thePath;
298
- }
299
- function isSubPath(potentialParent, thePath) {
300
- // For inside-directory checking, we want to allow trailing slashes, so normalize.
301
- thePath = stripTrailingSep(thePath);
302
- potentialParent = stripTrailingSep(potentialParent);
337
+ // Essentials
338
+ const pathExists = fs.existsSync(currentPath);
339
+ const relToInput = path.relative(inputPath, currentPath);
340
+ const absToOutput = path.join(outputPath, relToInput);
341
+ const isDir = pathExists ? fs.statSync(currentPath).isDirectory() : false;
342
+ const isMdx = currentPath.endsWith(".mdx");
343
+ const absHtmlPath = isMdx ? path.format({ ...path.parse(absToOutput), base: "", ext: ".html" }) : "";
303
344
 
304
345
 
305
- // Node treats only Windows as case-insensitive in its path module; we follow those conventions.
306
- if (process.platform === "win32") {
307
- thePath = thePath.toLowerCase();
308
- potentialParent = potentialParent.toLowerCase();
309
- }
346
+ // Delete if path does not exist
347
+ if (!pathExists) {
348
+ let pathToDelete = isMdx ? absHtmlPath : absToOutput;
349
+ log(`Deleting ${pathToDelete}`, !toBeVerbose);
350
+ await fsp.rm(pathToDelete, { recursive: true, force: true });
351
+ }
352
+ // Make corresponding directory
353
+ else if (isDir) {
354
+ log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
355
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
356
+ await fsp.mkdir(absToOutput, { recursive: true });
357
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
358
+ }
359
+ // Make html file from mdx
360
+ else if (isMdx) {
310
361
 
362
+ // Broadcast file creation started
363
+ log(`Creating ${currentPath} ---> ${absHtmlPath}`, !toBeVerbose);
364
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath);
311
365
 
312
- return thePath.lastIndexOf(potentialParent, 0) === 0 &&
313
- (
314
- thePath[potentialParent.length] === path.sep ||
315
- thePath[potentialParent.length] === undefined
316
- );
317
- }
318
- async function filterArgs(rawArgs) {
319
- // Assign to create
320
- let toCreateOnly = rawArgs.includes(CREATE_FLAG) || rawArgs.includes(CREATE_SHORT_FLAG)
321
366
 
367
+ // Intercept mdx code
368
+ let mdxCode = await fsp.readFile(currentPath, "utf8");
369
+ log(`Modifying mdx code of ${currentPath}`, !toBeVerbose || !configs?.modMDXCode);
370
+ mdxCode = await configs?.modMDXCode?.(inputPath, outputPath, currentPath, absHtmlPath, mdxCode) ?? mdxCode;
322
371
 
323
- // Assign input path
324
- let inputPath = rawArgs.find(val => val.startsWith(INPUT_PATH_FLAG));
325
- let inputPathProvided = inputPath !== undefined;
326
- inputPath = inputPathProvided ? inputPath.split('=')[1] : process.cwd();
327
372
 
373
+ // convert mdx code into html & paste into file
374
+ let parentDir = path.dirname(currentPath);
375
+ let globalArgs = { hostmdxCwd: parentDir, hostmdxInputPath: inputPath, hostmdxOutputPath: outputPath };
376
+ globalArgs = await configs?.modGlobalArgs?.(inputPath, outputPath, globalArgs) ?? globalArgs;
377
+ let result = await mdxToHtml(mdxCode, parentDir, globalArgs, async (settings) => { return await configs?.modBundleMDXSettings?.(inputPath, outputPath, settings) ?? settings });
378
+ let htmlCode = result.html;
379
+ await createFile(absHtmlPath, `<!DOCTYPE html>${htmlCode}`);
328
380
 
329
- // Check input path
330
- if (!fs.existsSync(inputPath) || !fs.lstatSync(inputPath).isDirectory()) {
331
- log(`Invalid input path "${inputPath}"`)
332
- return null;
333
- }
334
- else {
335
- inputPath = inputPath !== "" ? path.resolve(inputPath) : inputPath; // To ensure input path is absolute
336
- }
337
381
 
382
+ // Broadcast file creation ended
383
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result);
384
+ }
385
+ // Copy paste file
386
+ else {
387
+ log(`Creating ${currentPath} ---> ${absToOutput}`, !configs.toBeVerbose);
388
+ await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
389
+ await fsp.mkdir(path.dirname(absToOutput), { recursive: true });
390
+ await fsp.copyFile(currentPath, absToOutput);
391
+ await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
392
+ }
393
+ })));
338
394
 
339
- // Assign output path
340
- let outputPath = rawArgs.find(val => val.startsWith(OUTPUT_PATH_FLAG));
341
- let outputPathProvided = outputPath !== undefined;
342
- outputPath = outputPathProvided ? outputPath.split('=')[1] : createTempDir();
343
395
 
396
+ // Broadcast site creation ended
397
+ log(wasInterrupted ? `Site creation was interrupted!` : `Created site at ${outputPath}`);
398
+ await configs?.onSiteCreateEnd?.(inputPath, outputPath, wasInterrupted);
399
+ }
344
400
 
345
- // Check output path
346
- if (!fs.existsSync(outputPath) || !fs.lstatSync(outputPath).isDirectory()) {
347
- log(`Invalid output path "${outputPath}"`)
348
- return null;
349
- }
350
- else {
351
- outputPath = outputPath !== "" ? path.resolve(outputPath) : outputPath; // To ensure output path is absolute
352
- }
353
401
 
402
+ // Classes
403
+ export class HostMdx {
354
404
 
355
- // Check if output path is inside input path (causing infinite loop)
356
- if (isSubPath(inputPath, outputPath)) {
357
- log(`Output path "${outputPath}" cannot be inside or same as input path "${inputPath}"`);
358
- return null;
405
+ // Private Properties
406
+ #inputPathProvided = true;
407
+ #outputPathProvided = true;
408
+ #siteCreationStatus = SiteCreationStatus.NONE;
409
+ #pendingHardSiteCreation = false;
410
+ #alteredPaths = [];
411
+ #app = null;
412
+ #watcher = null;
413
+ #ignores = null;
414
+
415
+
416
+ // Constructors
417
+ constructor(inputPath = "", outputPath = "", configs = {}) {
418
+ this.inputPath = inputPath;
419
+ this.outputPath = outputPath;
420
+ this.configs = configs;
359
421
  }
360
422
 
361
423
 
362
- // Assign port
363
- let port = rawArgs.find(val => val.startsWith(PORT_FLAG));
364
- let portProvided = port !== undefined;
365
- port = portProvided ? Number(port.split('=')[1]) : (await getAvailablePort(DEFAULT_PORT, MAX_PORT));
424
+ // Private Methods
425
+ async #watchForChanges(event, somePath) {
366
426
 
427
+ // Return if out input path itself if passed
428
+ if (this.inputPath === somePath) {
429
+ return;
430
+ }
367
431
 
368
- // Check port
369
- if (port === -1) {
370
- log(`Could not find any available ports between ${DEFAULT_PORT} to ${MAX_PORT}, Try manually passing ${PORT_FLAG}=... flag`);
371
- return null;
372
- }
373
- else if (!Number.isInteger(port)) {
374
- log(`Invalid port`)
375
- return null;
376
- }
377
432
 
433
+ // Return if matches .ignore file
434
+ const relToInput = path.relative(this.inputPath, somePath);
435
+ if (this.#ignores.ignores(relToInput)) {
436
+ return;
437
+ }
378
438
 
379
- // Assign tracking changes
380
- let toTrackChanges = rawArgs.includes(TRACK_CHANGES_FLAG) || rawArgs.includes(TRACK_CHANGES_SHORT_FLAG);
381
439
 
440
+ // Return if toIgnore() from configs
441
+ const toIgnore = await this.configs?.toIgnore?.(this.inputPath, this.outputPath, somePath);
442
+ if (toIgnore) {
443
+ return;
444
+ }
382
445
 
383
- return { toCreateOnly, inputPath, inputPathProvided, outputPath, outputPathProvided, toTrackChanges, port, portProvided, };
384
- }
385
446
 
447
+ // Add changed path
448
+ this.#alteredPaths.push(somePath);
386
449
 
387
- // Main Methods
388
- async function createSiteSafe(...args) {
389
450
 
390
- let success = true;
391
- try {
392
- await createSite(...args);
451
+ // Reflect changes immediately
452
+ if (this.configs?.trackChanges !== undefined && this.configs?.trackChanges != TrackChanges.NONE) {
453
+ let toHardReload = this.configs?.trackChanges == TrackChanges.HARD;
454
+ log(`${toHardReload ? "Hard recreating" : "Recreating"} site, Event: ${event}, Path: ${somePath}`, !this.configs?.toBeVerbose);
455
+ await this.recreateSite(toHardReload);
456
+ }
393
457
  }
394
- catch (err) {
395
- success = false;
396
- isCreatingSite = false;
397
- log(`Failed to create site!\n${err.stack}`);
458
+
459
+
460
+ // Getter Methods
461
+ getSiteCreationStatus() {
462
+ return this.#siteCreationStatus;
398
463
  }
399
464
 
400
- return success;
401
- }
402
- async function listenForKey(createSiteCallback) {
403
465
 
404
- readline.emitKeypressEvents(process.stdin);
466
+ // Public Methods
467
+ async start() {
405
468
 
406
- if (process.stdin.isTTY) {
407
- process.stdin.setRawMode(true);
408
- }
469
+ // Make sure hosting has stopped before starting again
470
+ await this.stop();
409
471
 
410
- process.stdin.on('keypress', (chunk, key) => {
411
- if (key && key.name == 'r') {
412
- createSiteCallback();
413
- }
414
- else if (key && key.sequence == '\x03') {
415
- app?.server?.close((e) => { process.exit() })
416
- }
417
- });
418
- }
419
- async function watchForChanges(pathTowatch, callback) {
420
- chokidar.watch(pathTowatch, {
421
- ignoreInitial: true
422
- }).on('all', callback);
423
- }
424
- async function startServer(htmlDir, port) { // Starts server at given port
425
472
 
426
- // Broadcast server starting
427
- await configs?.onHostStart?.(port)
473
+ // Asssign all
474
+ this.#inputPathProvided = this.inputPath !== "";
475
+ this.#outputPathProvided = this.outputPath !== "";
476
+ this.inputPath = this.#inputPathProvided ? this.inputPath : process.cwd();
477
+ this.outputPath = this.#outputPathProvided ? this.outputPath : createTempDir();
428
478
 
429
479
 
430
- // Start Server
431
- const assets = sirv(htmlDir, { dev: true });
432
- const newApp = polka({
433
- onNoMatch: (req, res) => {
434
- // Set status code to 404
435
- res.statusCode = 404;
480
+ // Get configs
481
+ let configFilePath = path.join(this.inputPath, `./${CONFIG_FILE_NAME}`);
482
+ let doesConfigFileExists = fs.existsSync(configFilePath);
483
+ log(`Importing config file ${configFilePath}`, !doesConfigFileExists);
484
+ this.configs = { ...(await setupConfigs(configFilePath)), ...this.configs };
436
485
 
437
486
 
438
- // Send 404 file if found else not found message
439
- const errorFile = path.join(htmlDir, FILE_404);
440
- if (fs.existsSync(errorFile)) {
441
- res.setHeader('Content-Type', 'text/html');
442
- res.end(fs.readFileSync(errorFile));
443
- } else {
444
- res.end(NOT_FOUND_404_MESSAGE);
445
- }
487
+ // Get port
488
+ let port = this.configs?.port ?? await getAvailablePort();
489
+ if (port === -1) {
490
+ log(`Could not find any available ports`);
491
+ return false;
446
492
  }
447
- }).use((req, res, next) => { // Add trailing slash
448
- if (1 < req.path.length && !req.path.endsWith('/') && !path.extname(req.path)) {
449
- res.writeHead(301, { Location: req.path + '/' });
450
- return res.end();
493
+ else if (!Number.isInteger(port)) {
494
+ log(`Invalid port`)
495
+ return false;
451
496
  }
452
- next();
453
- }).use(assets)
454
497
 
455
498
 
456
- // Start listening
457
- newApp.listen(port)
458
- newApp.server.on("close", async () => { await configs?.onHostEnd?.(port) });
459
- newApp.server.on("error", (e) => { log(`Failed to start server: ${e.message}`); throw e; });
460
- log(`Server listening at ${port} ... (Press 'r' to manually reload, Press 'Ctrl+c' to exit)`)
499
+ // Get ignores
500
+ let ignoreFilePath = path.join(this.inputPath, IGNORE_FILE_NAME);
501
+ this.#ignores = getIgnore(ignoreFilePath);
461
502
 
462
503
 
463
- return newApp
464
- }
465
- async function Main() {
504
+ // Broadcast hosting about to start
505
+ await this.configs?.onHostStarting?.(this.inputPath, this.outputPath, port);
466
506
 
467
- // Get all arguments
468
- const rawArgs = process.argv.slice(2);
469
507
 
508
+ // Watch for changes
509
+ let chokidarOptions = { ...DEFAULT_CHOKIDAR_OPTIONS, ...(this.configs?.chokidarOptions ?? {}) };
510
+ this.#watcher = chokidar.watch(this.inputPath, chokidarOptions).on("all", (event, path) => this.#watchForChanges(event, path));
470
511
 
471
- // Check if verbose
472
- isVerbose = rawArgs.includes(VERBOSE_FLAG) || rawArgs.includes(VERBOSE_SHORT_FLAG);
473
512
 
513
+ // Delete old files & Create site
514
+ await this.recreateSite(true);
474
515
 
475
- // Check if asked for help
476
- if (rawArgs.includes(HELP_FLAG) || rawArgs.includes(HELP_SHORT_FLAG)) {
477
- console.log(HELP_MESSAGE)
478
- return;
479
- }
480
516
 
517
+ // Start server to host site
518
+ this.#app = await startServer(this.outputPath, port, (e) => { log(`Failed to start server: ${e.message}`); throw e; });
519
+ this.#app?.server?.on("close", async () => { await this.configs?.onHostEnded?.(this.inputPath, this.outputPath, port); });
481
520
 
482
- // Filter arguments
483
- let args = await filterArgs(rawArgs);
484
- if (args === null) {
485
- return;
486
- }
487
521
 
522
+ // Broadcast hosting started
523
+ await this.configs?.onHostStarted?.(this.inputPath, this.outputPath, port);
488
524
 
489
- // Get config
490
- let configFilePath = path.join(args.inputPath, `./${CONFIG_FILE_NAME}`)
491
- if (fs.existsSync(configFilePath)) {
492
- log(`Importing config file ${CONFIG_FILE_NAME}`);
493
- configs = await import(pathToFileURL(configFilePath).href);
494
- }
495
525
 
526
+ // Load as started
527
+ log(`Server listening at ${port} ...`);
496
528
 
497
- // Create site from mdx & return if only needed to create site
498
- let wasCreated = await createSiteSafe(args.inputPath, args.outputPath);
499
- if (args.toCreateOnly) {
500
- process.exitCode = !wasCreated ? 1 : 0; // Exit with error code if not created successfully
501
- return;
529
+
530
+ return true;
502
531
  }
532
+ async recreateSite(hardReload = false) {
503
533
 
534
+ // Return if no changes made and no requested hard reload
535
+ if (this.#alteredPaths.length == 0 && !hardReload) {
536
+ log(`No changes made which require reloading, Try hard reloading instead`)
537
+ return;
538
+ }
504
539
 
505
- // Watch for key presses
506
- listenForKey(() => createSiteSafe(args.inputPath, args.outputPath));
507
540
 
541
+ // Return & add to pending if already creating
542
+ if (this.#siteCreationStatus == SiteCreationStatus.ONGOING) {
543
+ log("Site creation already ongoing! Added to pending")
544
+ this.#siteCreationStatus = SiteCreationStatus.PENDING_RECREATION;
545
+ this.#pendingHardSiteCreation = hardReload;
546
+ return;
547
+ }
508
548
 
509
- // Watch for changes
510
- if (args.toTrackChanges) {
511
- watchForChanges(args.inputPath, async (event, path) => {
512
- if (typeof configs?.toTriggerRecreate === 'function' && !(await configs?.toTriggerRecreate(event, path))) {
513
- return;
514
- }
515
549
 
516
- log(`Recreating site, Event: ${event}, Path: ${path}`, true)
517
- createSiteSafe(args.inputPath, args.outputPath)
518
- });
519
- }
550
+ // Set creating status to ongoing
551
+ this.#siteCreationStatus = SiteCreationStatus.ONGOING;
520
552
 
521
553
 
522
- // Start server
523
- app = await startServer(args.outputPath, args.port);
554
+ // Actual site creation
555
+ try {
556
+ let pathsToCreate = hardReload ? null : [...this.#alteredPaths];
557
+ this.#alteredPaths = [];
558
+ await createSite(this.inputPath, this.outputPath, pathsToCreate, this.#ignores, this.configs, () => this.#siteCreationStatus != SiteCreationStatus.ONGOING);
559
+ }
560
+ catch (err) {
561
+ log(`Failed to create site!\n${err.stack}`);
562
+ }
524
563
 
525
564
 
526
- // Handle quit
527
- const cleanup = () => {
528
- // Remove html path
529
- if (!args.outputPathProvided && fs.existsSync(args.outputPath)) {
530
- fs.rmSync(args.outputPath, { recursive: true, force: true })
565
+ // If recreate was triggered while site creation was ongoing
566
+ const wasPending = this.#siteCreationStatus === SiteCreationStatus.PENDING_RECREATION;
567
+ this.#siteCreationStatus = SiteCreationStatus.NONE;
568
+ if (wasPending) {
569
+ await this.recreateSite(this.#pendingHardSiteCreation);
531
570
  }
532
-
533
- process.stdin.setRawMode(false);
534
571
  }
535
- process.on("exit", cleanup);
536
- process.on("SIGINT", cleanup);
537
- process.on("SIGTERM", cleanup);
538
- }
572
+ async abortSiteCreation() {
573
+ this.#siteCreationStatus = SiteCreationStatus.NONE;
574
+ }
575
+ async stop() {
576
+ // Remove temp dir html path
577
+ if (!this.#outputPathProvided && fs.existsSync(this.outputPath)) {
578
+ fs.rmSync(this.outputPath, { recursive: true });
579
+ }
580
+
539
581
 
540
- Main()
582
+ // Stop server
583
+ this.#app?.server?.close?.(); //(e) => { process.exit(); });
584
+
585
+
586
+ // Stop watching for changes
587
+ await this.#watcher?.close?.();
588
+ }
589
+ }