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/README.md +83 -42
- package/cli.js +195 -0
- package/index.js +432 -383
- package/mdx-to-html.js +15 -15
- package/package.json +4 -3
- package/tests/cli.test.js +49 -0
package/index.js
CHANGED
|
@@ -1,26 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
|
|
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
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
85
|
-
|
|
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,
|
|
156
|
+
let timestamp = new Date().toLocaleString(undefined, LOG_TIME_OPTIONS)
|
|
90
157
|
console.log(`[${APP_NAME} ${timestamp}] ${msg}`)
|
|
91
158
|
}
|
|
92
|
-
function
|
|
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
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
206
|
+
currentPort++;
|
|
207
|
+
}
|
|
113
208
|
|
|
114
|
-
return
|
|
209
|
+
return -1;
|
|
115
210
|
}
|
|
116
|
-
function
|
|
211
|
+
export async function createSite(inputPath = "", outputPath = "", pathsToCreate = [], ignores = undefined, configs = undefined, interruptCondition = undefined) {
|
|
117
212
|
|
|
118
|
-
// Check
|
|
119
|
-
|
|
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
|
-
//
|
|
126
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
//
|
|
148
|
-
if (
|
|
149
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
//
|
|
159
|
-
const
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
246
|
-
|
|
305
|
+
return true;
|
|
306
|
+
})));
|
|
307
|
+
pathsToCreate = pathsToCreate.filter((_, index) => filterResults[index]);
|
|
247
308
|
|
|
248
309
|
|
|
249
|
-
//
|
|
250
|
-
if (
|
|
251
|
-
log(`
|
|
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
|
-
//
|
|
260
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
321
|
+
// Broadcast site creation started
|
|
322
|
+
log("Creating site...");
|
|
323
|
+
await configs?.onSiteCreateStart?.(inputPath, outputPath);
|
|
277
324
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
//
|
|
363
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
466
|
+
// Public Methods
|
|
467
|
+
async start() {
|
|
405
468
|
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
});
|
|
519
|
-
}
|
|
550
|
+
// Set creating status to ongoing
|
|
551
|
+
this.#siteCreationStatus = SiteCreationStatus.ONGOING;
|
|
520
552
|
|
|
521
553
|
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
+
}
|