host-mdx 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # host-mdx
1
+ # 🌐 host-mdx
2
2
 
3
3
  [![Version](https://img.shields.io/npm/v/host-mdx.svg)](https://www.npmjs.com/package/host-mdx)\
4
4
  A cli tool to create and serve a static html website from a given mdx directory
@@ -50,15 +50,16 @@ hostMdx.start();
50
50
 
51
51
  ### Additional:
52
52
 
53
- 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
53
+ You can add `.hostmdxignore` at the root of your project to filter out which files/folders to skip while generating html
54
54
  (similar to [.gitignore](https://git-scm.com/docs/gitignore))
55
55
 
56
- You can also add a file by the name `host-mdx.js` at the root of your input folder as a config file (Look at the example below for all available options)
56
+ > You can add `# [EXCLUDE]` comment on top of every path for behaviour similar to returning null in `toIgnore`
57
+
58
+ You can also add `host-mdx.js` at the root of your project as an optional config file for more complex behaviour (Look at the example below for all available options)
57
59
 
58
60
 
59
- > **Note:**
60
61
  > 1. Any config properties passed from npx or import e.g. `port`, `toBeVerbose`, `trackChanges`, etc will override `host-mdx.js` export values
61
- > 1. 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)
62
+ > 1. Any changes made to `host-mdx.js` or any new packages added require complete restart otherwise changes will not reflect due to [this bug](https://github.com/nodejs/node/issues/49442)
62
63
 
63
64
  <br/>
64
65
 
@@ -82,9 +83,9 @@ Input Directory:
82
83
 
83
84
  ```
84
85
  my-website-template/
86
+ ├─ .hostmdxignore
85
87
  ├─ 404.mdx
86
88
  ├─ index.mdx
87
- ├─ .hostmdxignore
88
89
  ├─ host-mdx.js
89
90
  ├─ about/
90
91
  │ ├─ index.mdx
@@ -107,9 +108,11 @@ my-website-template/
107
108
 
108
109
  ```sh
109
110
  *.jsx
110
- blog/page2/
111
+ # [EXCLUDE]
111
112
  static/temp.jpg
112
113
  !static/sample.jsx
114
+ # [EXCLUDE]
115
+ blog/page2/
113
116
  ```
114
117
 
115
118
  `host-mdx.js` file content:
@@ -124,31 +127,42 @@ export async function onHostStarted(inputPath, outputPath, port) {
124
127
  export async function onHostEnded(inputPath, outputPath, port) {
125
128
  console.log("onHostEnded");
126
129
  }
127
- export async function onSiteCreateStart(inputPath, outputPath) {
130
+ export async function onSiteCreateStart(inputPath, outputPath, isSoftReload) {
128
131
  console.log("onSiteCreateStart");
129
132
  }
130
- export async function onSiteCreateEnd(inputPath, outputPath, wasInterrupted) {
133
+ export async function onSiteCreateEnd(inputPath, outputPath, isSoftReload, wasInterrupted) {
131
134
  console.log("onSiteCreateEnd");
132
135
  }
133
- export async function onFileCreateStart(inputFilePath, outputFilePath, inFilePath, outFilePath) {
134
- console.log("onFileCreateStart");
136
+ export async function onFileChangeStart(inputPath, outputPath, inFilePath, outFilePath, toBeDeleted) {
137
+ console.log("onFileChangeStart");
135
138
  }
136
- export async function onFileCreateEnd(inputFilePath, outputFilePath, inFilePath, outFilePath, result) {
139
+ export async function onFileChangeEnd(inputPath, outputPath, inFilePath, outFilePath, wasDeleted, result) {
137
140
  // `result = undefined` if file is not .mdx
138
141
  // `result.html` contains stringified HTML
139
142
  // `result.exports` contains exports from mdx
140
- console.log("onFileCreateEnd");
143
+ console.log("onFileChangeEnd");
141
144
  }
142
- export async function toIgnore(inputPath, outputPath, path) {
143
- const isGOutputStream = /\.goutputstream-\w+$/.test(path);
145
+ export async function toIgnore(inputPath, outputPath, targetPath) {
146
+
147
+ // Return true = file not generated in output folder,
148
+ // Return null = same as true but also prevents triggering reload
149
+
150
+ const isGOutputStream = /\.goutputstream-\w+$/.test(targetPath);
144
151
  if (isGOutputStream) {
145
- return true;
152
+ return null;
146
153
  }
147
-
154
+
155
+ const ignoredDirs = new Set(['node_modules', '.git', '.github']);
156
+ const segments = targetPath.split(path.sep);
157
+ if (segments.some(segment => ignoredDirs.has(segment))) {
158
+ return null;
159
+ }
160
+
148
161
  return false;
149
162
  }
150
163
  export async function modMDXCode(inputPath, outputPath, inFilePath, outFilePath, code){
151
- // Modify code ...
164
+ // Wrapper example
165
+ code = `import Content from "${inFilePath}"; import { Wrapper } from "utils.jsx";\n\n<Wrapper><Content /></Wrapper>`
152
166
  return code;
153
167
  }
154
168
  export async function modGlobalArgs(inputPath, outputPath, globalArgs){
@@ -156,7 +170,20 @@ export async function modGlobalArgs(inputPath, outputPath, globalArgs){
156
170
  return globalArgs;
157
171
  }
158
172
  export async function modBundleMDXSettings(inputPath, outputPath, settings) {
159
- // Modify settings ...
173
+ // Example for adding '@' root alias
174
+ var oldBuildOptions = settings.esbuildOptions;
175
+ settings.esbuildOptions = (options) => {
176
+ options = oldBuildOptions(options)
177
+ options.logLevel = 'error';
178
+ options.alias = {
179
+ ...options.alias,
180
+ '@': inputPath
181
+ };
182
+
183
+ return options;
184
+ }
185
+
186
+
160
187
  return settings;
161
188
  }
162
189
  export async function modRebuildPaths(inputPath, outputPath, rebuildPaths) {
package/cli.js CHANGED
@@ -41,7 +41,7 @@ ${VERBOSE_FLAG}, ${VERBOSE_SHORT_FLAG} Shows additional log messages
41
41
  function getInputPathFromArgs(rawArgs) {
42
42
  let inputPath = rawArgs.find(val => val.startsWith(INPUT_PATH_FLAG));
43
43
  let inputPathProvided = inputPath !== undefined;
44
- inputPath = inputPathProvided ? inputPath.split('=')?.[1] : "";
44
+ inputPath = inputPathProvided ? inputPath.split('=')?.[1] : process.cwd();
45
45
  return inputPath !== "" ? path.resolve(inputPath) : inputPath; // To ensure input path is absolute
46
46
  }
47
47
  function getOutputPathFromArgs(rawArgs) {
@@ -56,8 +56,14 @@ function getPortFromArgs(rawArgs) {
56
56
  return portProvided ? Number(port.split('=')[1]) : undefined;
57
57
  }
58
58
  function getTrackChangesFromArgs(rawArgs) {
59
+
59
60
  // If flag not passed do not track changes
60
- let trackChanges = rawArgs.find(val => (val.startsWith(TRACK_CHANGES_FLAG) || val.startsWith(TRACK_CHANGES_SHORT_FLAG)));
61
+ let trackChanges = rawArgs.find(val =>
62
+ val === TRACK_CHANGES_SHORT_FLAG ||
63
+ val.startsWith(`${TRACK_CHANGES_SHORT_FLAG}=`) ||
64
+ val === TRACK_CHANGES_FLAG ||
65
+ val.startsWith(`${TRACK_CHANGES_FLAG}=`)
66
+ );
61
67
  if (trackChanges == undefined) {
62
68
  return undefined;
63
69
  }
@@ -79,7 +85,8 @@ function getTrackChangesFromArgs(rawArgs) {
79
85
  function getConcurrencyFromArgs(rawArgs) {
80
86
  let concurrency = rawArgs.find(val => val.startsWith(CONCURRENCY_FLAG));
81
87
  let concurrencyProvided = concurrency !== undefined;
82
- return concurrencyProvided ? Number(concurrency.split('=')[1]) : undefined;
88
+ const val = concurrencyProvided ? Number(concurrency.split('=')[1]) : undefined;
89
+ return Number.isInteger(val) && val >= 1 ? val : undefined;
83
90
  }
84
91
  function listenForKey(reloadCallback, hardReloadCallback, exitCallback) {
85
92
  readline.emitKeypressEvents(process.stdin);
@@ -176,16 +183,22 @@ export async function main() {
176
183
 
177
184
  // Watch for key press
178
185
  listenForKey(
179
- async () => await hostMdx?.recreateSite(),
180
- async () => await hostMdx?.recreateSite(true),
186
+ async () => {
187
+ log("--- RELOAD TRIGGERED ---");
188
+ await hostMdx?.recreateSite();
189
+ },
190
+ async () => {
191
+ log("--- HARD RELOAD TRIGGERED ---");
192
+ await hostMdx?.recreateSite(true);
193
+ },
181
194
  cleanup
182
195
  );
183
196
 
184
197
 
185
198
  // Watch for quit
186
- process.on("exit", cleanup);
187
199
  process.on("SIGINT", cleanup);
188
200
  process.on("SIGTERM", cleanup);
201
+ process.on("exit", () => { process.stdin.setRawMode(false) });
189
202
 
190
203
 
191
204
  // Log key press instructions
@@ -0,0 +1,346 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import resolve from 'resolve';
4
+ import precinct from 'precinct';
5
+ import * as mdx from '@mdx-js/mdx';
6
+
7
+
8
+ // Properties
9
+ const DEPENDENTS_KEY = "dependents";
10
+ const DEPENDENCIES_KEY = "dependencies";
11
+
12
+
13
+ // Methods
14
+ export async function crawlDir(dir, ignoreCheck = async (p) => false) {
15
+
16
+ // Make sure dir is absolute
17
+ dir = path.resolve(dir);
18
+
19
+
20
+ // Iterate through all files in dir
21
+ let results = [];
22
+ const list = fs.readdirSync(dir);
23
+ for (let targetPath of list) {
24
+
25
+ // get absolute path
26
+ const absPath = path.join(dir, targetPath);
27
+
28
+
29
+ // Skip if to ignore
30
+ if (await ignoreCheck(absPath)) {
31
+ continue;
32
+ }
33
+
34
+
35
+ // If directory, Recurse into it
36
+ const stat = fs.statSync(absPath);
37
+ if (stat && stat.isDirectory()) {
38
+ results = results.concat(await crawlDir(absPath, ignoreCheck));
39
+ continue;
40
+ }
41
+
42
+
43
+ // If file, Add to list
44
+ results.push(absPath);
45
+ }
46
+
47
+ return results;
48
+ }
49
+ export function resolveAlias(targetPath, aliases) {
50
+ for (const [alias, aliasPath] of Object.entries(aliases)) {
51
+
52
+ // Check if import is the alias or starts with alias + system separator
53
+ const isExact = targetPath === alias;
54
+ const isSubPath = targetPath.startsWith(`${alias}${path.sep}`) || targetPath.startsWith(`${alias}/`);
55
+
56
+ if (isExact || isSubPath) {
57
+ targetPath = targetPath.replace(alias, aliasPath);
58
+ targetPath = path.normalize(targetPath);
59
+ break;
60
+ }
61
+ }
62
+
63
+ return targetPath;
64
+ }
65
+ export function ensureRelativePath(rootPath, filePath) {
66
+ const absoluteTarget = path.resolve(rootPath, filePath);
67
+ const absoluteRoot = path.resolve(rootPath);
68
+ return path.relative(absoluteRoot, absoluteTarget);
69
+ }
70
+ export async function calcDependencies(filePath, aliases = {}) {
71
+
72
+ // Return if given path is in node_modules
73
+ const absolutePath = path.resolve(filePath);
74
+ if (absolutePath.split(path.sep).includes('node_modules')) {
75
+ return new Set();
76
+ }
77
+
78
+
79
+ // Compile mdx if passed
80
+ let foundImports = [];
81
+ if (absolutePath.endsWith('.mdx')) {
82
+ let content = fs.readFileSync(absolutePath, 'utf8');
83
+ const compiled = await mdx.compile(content);
84
+ content = String(compiled?.value ?? "");
85
+ foundImports = precinct(content);
86
+ }
87
+ else {
88
+ foundImports = precinct.paperwork(absolutePath);
89
+ }
90
+
91
+
92
+ // Get & iterate through all imports
93
+ let filteredImports = [];
94
+ for (let i of foundImports) {
95
+
96
+ // Resolve aliases
97
+ i = resolveAlias(i, aliases);
98
+
99
+
100
+ // Skip if not a local file
101
+ const isLocal = i.startsWith('.') || i.startsWith('/');
102
+ if (!isLocal) {
103
+ continue;
104
+ }
105
+
106
+
107
+ // Resolve the found import
108
+ let resolvedPath = "";
109
+ try {
110
+ resolvedPath = resolve.sync(i, { basedir: path.dirname(absolutePath) }); // extensions: ['.js', '.jsx', '.mdx', '.json', '.tsx', '.ts']
111
+ }
112
+ catch (err) {
113
+ continue;
114
+ }
115
+
116
+
117
+ // Skip if the resolved path is within node_modules
118
+ if (resolvedPath.split(path.sep).includes('node_modules')) {
119
+ continue;
120
+ }
121
+
122
+
123
+ // Add path as a dependency
124
+ filteredImports.push(resolvedPath);
125
+ }
126
+
127
+
128
+ return new Set(filteredImports);
129
+ }
130
+
131
+
132
+ // Classes
133
+ export class DependencyGraph {
134
+
135
+ // Private Properties
136
+ #graph = {}; // Format { "path/to/file" : { dependents: Set(...), dependencies : Set(...) }, ... }
137
+ #aliases = {}; // Format { '@' : "path/to/dir" }
138
+ #rootFolder = "";
139
+ #ignoreCheck = async (checkPath) => false;
140
+
141
+
142
+ // Public Methods
143
+ getGraph() {
144
+ return structuredClone(this.#graph);
145
+ }
146
+ async createGraph(newRootFolder, newIgnoreCheck = async (checkPath) => false) {
147
+ this.#graph = {};
148
+ this.#rootFolder = path.resolve(newRootFolder);
149
+ this.#ignoreCheck = newIgnoreCheck;
150
+
151
+
152
+ // Get all files inside directory
153
+ const allFiles = await crawlDir(this.#rootFolder, this.#ignoreCheck);
154
+
155
+
156
+ // Assign all dependencies
157
+ for (const file of allFiles) {
158
+ await this.addEntry(file);
159
+ }
160
+ }
161
+ async addEntry(filePath) {
162
+
163
+ // Get relative path
164
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
165
+
166
+
167
+ // If did not exist previously create fresh
168
+ if (this.#graph?.[relFilePath] === undefined) {
169
+ this.#graph[relFilePath] = {
170
+ [DEPENDENCIES_KEY]: new Set(),
171
+ [DEPENDENTS_KEY]: new Set()
172
+ }
173
+ }
174
+
175
+
176
+ // Remove previous dependencies
177
+ const oldDeps = this.#graph[relFilePath][DEPENDENCIES_KEY];
178
+ oldDeps.forEach(dep => {
179
+ this.#graph?.[dep]?.[DEPENDENTS_KEY]?.delete(relFilePath);
180
+ });
181
+
182
+
183
+ // Intentionally not removing dependents since no way of knowing which files depend on `relFilePath` DO NOT CHANGE
184
+
185
+
186
+ // Get all dependencies
187
+ const relDependencies = new Set();
188
+ const absFilePath = path.resolve(this.#rootFolder, relFilePath);
189
+ let dependencies;
190
+ try {
191
+ dependencies = await calcDependencies(absFilePath, this.#aliases);
192
+ }
193
+ catch (err) {
194
+ dependencies = new Set();
195
+ }
196
+ dependencies.forEach(p => {
197
+ relDependencies.add(path.relative(this.#rootFolder, p));
198
+ });
199
+
200
+
201
+ // Add dependencies
202
+ this.#graph[relFilePath][DEPENDENCIES_KEY] = relDependencies;
203
+
204
+
205
+ // Add to dependents
206
+ const depList = this.#graph[relFilePath][DEPENDENCIES_KEY];
207
+ depList.forEach(dep => {
208
+ if (this.#graph?.[dep] === undefined) {
209
+ this.#graph[dep] = { [DEPENDENCIES_KEY]: new Set(), [DEPENDENTS_KEY]: new Set() };
210
+ }
211
+
212
+ this.#graph[dep][DEPENDENTS_KEY].add(relFilePath);
213
+ });
214
+ }
215
+ removeEntry(filePath) {
216
+
217
+ // Get relative path
218
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
219
+
220
+
221
+ // Return if entry does not exist
222
+ if (this.#graph[relFilePath] === undefined) {
223
+ return;
224
+ }
225
+
226
+
227
+ // Remove from dependents
228
+ const depList = this.#graph[relFilePath][DEPENDENCIES_KEY];
229
+ depList.forEach(dep => {
230
+ this.#graph?.[dep]?.[DEPENDENTS_KEY]?.delete(relFilePath);
231
+ });
232
+
233
+
234
+ // Remove from dependencies
235
+ const depOf = this.#graph[relFilePath][DEPENDENTS_KEY];
236
+ depOf.forEach(dependent => {
237
+ this.#graph[dependent]?.[DEPENDENCIES_KEY]?.delete(relFilePath);
238
+ });
239
+
240
+
241
+ // Remove entry
242
+ delete this.#graph[relFilePath];
243
+ }
244
+ hasEntry(filePath) {
245
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
246
+ return this.#graph[relFilePath] !== undefined;
247
+ }
248
+ getEntry(filePath) {
249
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
250
+ return this.#graph[relFilePath];
251
+ }
252
+ getDependencies(filePath) {
253
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
254
+ let absDeps = new Set();
255
+ let deps = this.#graph?.[relFilePath]?.[DEPENDENCIES_KEY] ?? new Set();
256
+ for (const dep of deps) {
257
+ absDeps.add(path.resolve(this.#rootFolder, dep));
258
+ }
259
+
260
+ return absDeps;
261
+ }
262
+ getDeepDependencies(filePath) {
263
+
264
+ // Get relative path
265
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
266
+
267
+
268
+ // Return empty set if entry does not exist
269
+ if (!this.hasEntry(relFilePath)) {
270
+ return new Set();
271
+ }
272
+
273
+
274
+ // Recursively get dependencies
275
+ const deepDeps = new Set();
276
+ const walk = (currentPath) => {
277
+
278
+ // Iterate over all dependencies
279
+ const deps = this.getDependencies(currentPath);
280
+ deps.forEach(dep => {
281
+ if (deepDeps.has(dep)) {
282
+ return;
283
+ }
284
+
285
+ // Add to list and continue walking
286
+ deepDeps.add(dep);
287
+ walk(dep);
288
+ });
289
+ };
290
+ walk(relFilePath);
291
+
292
+ return deepDeps;
293
+ }
294
+ getDependents(filePath) {
295
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
296
+ let absDeps = new Set();
297
+ let deps = this.#graph?.[relFilePath]?.[DEPENDENTS_KEY] ?? new Set();
298
+ for (const dep of deps) {
299
+ absDeps.add(path.resolve(this.#rootFolder, dep));
300
+ }
301
+
302
+ return absDeps;
303
+ }
304
+ getDeepDependents(filePath) {
305
+
306
+ // Get relative path
307
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
308
+
309
+
310
+ // Return empty set if entry does not exist
311
+ if (!this.hasEntry(relFilePath)) {
312
+ return new Set();
313
+ }
314
+
315
+
316
+ // Recursively get dependents
317
+ const deepDependents = new Set();
318
+ const walk = (currentPath) => {
319
+
320
+ // Iterate over all dependents
321
+ const dependents = this.getDependents(currentPath);
322
+ dependents.forEach(dependent => {
323
+ if (deepDependents.has(dependent)) {
324
+ return;
325
+ }
326
+
327
+ // Add to list and continue walking
328
+ deepDependents.add(dependent);
329
+ walk(dependent);
330
+ });
331
+ };
332
+ walk(relFilePath);
333
+
334
+
335
+ return deepDependents;
336
+ }
337
+ addAlias(symbol, toPath) {
338
+ this.#aliases[symbol] = toPath;
339
+ }
340
+ removeAlias(symbol) {
341
+ delete this.#aliases[symbol];
342
+ }
343
+ setAlias(newAliases) {
344
+ this.#aliases = newAliases;
345
+ }
346
+ }
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import os from "os";
3
3
  import net from "net";
4
+ import _ from 'lodash';
4
5
  import path from "path";
5
6
  import sirv from "sirv";
6
7
  import polka from "polka";
@@ -10,6 +11,7 @@ import chokidar from "chokidar";
10
11
  import { pathToFileURL } from "url";
11
12
  import { promises as fsp } from "fs";
12
13
  import { mdxToHtml } from "./mdx-to-html.js";
14
+ import { DependencyGraph, crawlDir } from "./dependency-graph.js";
13
15
 
14
16
 
15
17
  // Enums
@@ -33,15 +35,24 @@ const FILE_404 = "404.html";
33
35
  const NOT_FOUND_404_MESSAGE = "404";
34
36
  const DEFAULT_PORT = 3000;
35
37
  const MAX_PORT = 4000;
38
+ const EXCLUDE_HEADER = "# [EXCLUDE]"; // Case insensitive
36
39
  const TEMP_HTML_DIR = path.join(os.tmpdir(), `${APP_NAME}`);
37
40
  const DEFAULT_IGNORES = `
41
+ ${EXCLUDE_HEADER}
38
42
  ${IGNORE_FILE_NAME}
43
+ ${EXCLUDE_HEADER}
39
44
  ${CONFIG_FILE_NAME}
45
+ ${EXCLUDE_HEADER}
40
46
  node_modules
47
+ ${EXCLUDE_HEADER}
41
48
  package-lock.json
49
+ ${EXCLUDE_HEADER}
42
50
  package.json
51
+ ${EXCLUDE_HEADER}
43
52
  .git
53
+ ${EXCLUDE_HEADER}
44
54
  .github
55
+ ${EXCLUDE_HEADER}
45
56
  .gitignore
46
57
  `;
47
58
 
@@ -60,6 +71,27 @@ const LOG_TIME_OPTIONS = {
60
71
  const DEFAULT_CHOKIDAR_OPTIONS = {
61
72
  ignoreInitial: true
62
73
  };
74
+ const DEFAULT_CONFIGS = {
75
+ // port: 3000, // Intentionally kept commented out, otherwise interferes with auto port assigning DO NOT CHANGE
76
+ trackChanges: 0,
77
+ toBeVerbose: false,
78
+ concurrency: 1,
79
+ chokidarOptions: DEFAULT_CHOKIDAR_OPTIONS,
80
+ toIgnore: (inputPath, outputPath, targetPath) => {
81
+ const isGOutputStream = /\.goutputstream-\w+$/.test(targetPath);
82
+ if (isGOutputStream) {
83
+ return null;
84
+ }
85
+
86
+ const ignoredDirs = new Set(['node_modules', '.git', '.github']);
87
+ const segments = targetPath.split(path.sep);
88
+ if (segments.some(segment => ignoredDirs.has(segment))) {
89
+ return null;
90
+ }
91
+
92
+ return false;
93
+ }
94
+ };
63
95
 
64
96
 
65
97
  // Utility Methods
@@ -69,9 +101,48 @@ function getIgnore(ignoreFilePath) {
69
101
  if (fs.existsSync(ignoreFilePath)) {
70
102
  ignoreContent += `\n${fs.readFileSync(ignoreFilePath, "utf8")}`;
71
103
  }
72
-
73
104
  ig.add(ignoreContent);
105
+ return ig;
106
+ }
107
+ function getExclude(ignoreFilePath) {
108
+
109
+ // Read .ignore file
110
+ const ig = ignore();
111
+ let rawContent = DEFAULT_IGNORES;
112
+ if (fs.existsSync(ignoreFilePath)) {
113
+ rawContent += "\n" + fs.readFileSync(ignoreFilePath, "utf8");
114
+ }
115
+
116
+
117
+ // Only get lines which have "# [EXCLUDE]" comment on top
118
+ let filteredLines = [];
119
+ let hasExclude = false;
120
+ const lines = rawContent.split(/\r?\n/);
121
+ const excludeComment = EXCLUDE_HEADER.toLowerCase();
122
+ for (const line of lines) {
123
+ const trimmed = line.trim();
124
+
125
+ // Check for the header tag
126
+ if (trimmed.toLowerCase() === excludeComment) {
127
+ hasExclude = true;
128
+ continue;
129
+ }
74
130
 
131
+ // Reset if empty line found
132
+ if (trimmed === "") {
133
+ hasExclude = false;
134
+ continue;
135
+ }
136
+
137
+ // Add line if has exclude otherwise continue
138
+ if (hasExclude) {
139
+ filteredLines.push(trimmed);
140
+ }
141
+ }
142
+
143
+
144
+ // Add to ignore
145
+ ig.add(filteredLines.join("\n"));
75
146
  return ig;
76
147
  }
77
148
  async function createFile(filePath, fileContent = "") {
@@ -79,11 +150,6 @@ async function createFile(filePath, fileContent = "") {
79
150
  await fsp.mkdir(fileLocation, { recursive: true });
80
151
  await fsp.writeFile(filePath, fileContent);
81
152
  }
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
153
  async function startServer(hostDir, port, errorCallback) { // Starts server at given port
88
154
 
89
155
  // Make sure host dir path is absolute
@@ -93,10 +159,10 @@ async function startServer(hostDir, port, errorCallback) { // Starts server at
93
159
  // Start Server
94
160
  const assets = sirv(hostDir, { dev: true });
95
161
  const newApp = polka({
96
- onNoMatch: (req, res) => { // Send 404 file if found else not found message
97
- const errorFile = path.join(hostDir, FILE_404);
98
- if (fs.existsSync(errorFile)) {
99
- const content = fs.readFileSync(errorFile);
162
+ onNoMatch: async (req, res) => { // Send 404 file if found else not found message
163
+ const file404 = path.join(hostDir, FILE_404);
164
+ if (fs.existsSync(file404)) {
165
+ const content = await fsp.readFile(file404);
100
166
  res.writeHead(404, {
101
167
  'Content-Type': 'text/html',
102
168
  'Content-Length': content.length
@@ -140,6 +206,18 @@ async function isPortAvailable(port) {
140
206
  server.listen(port);
141
207
  });
142
208
  }
209
+ async function getAvailablePort(startPort = DEFAULT_PORT, maxPort = MAX_PORT) {
210
+ let currentPort = startPort;
211
+ while (currentPort <= maxPort) {
212
+ if (await isPortAvailable(currentPort)) {
213
+ return currentPort;
214
+ }
215
+
216
+ currentPort++;
217
+ }
218
+
219
+ return -1;
220
+ }
143
221
  export function log(msg, toSkip = false) {
144
222
  if (toSkip) { // Useful for verbose check
145
223
  return
@@ -174,10 +252,10 @@ export async function setupConfigs(inputPath) {
174
252
  let configFilePath = path.join(inputPath, CONFIG_FILE_NAME);
175
253
  if (fs.existsSync(configFilePath)) {
176
254
  let cleanConfigFilePath = pathToFileURL(configFilePath).href
177
- return await import(cleanConfigFilePath);
255
+ return { ...DEFAULT_CONFIGS, ...(await import(cleanConfigFilePath)) };
178
256
  }
179
257
 
180
- return {};
258
+ return _.cloneDeep(DEFAULT_CONFIGS);
181
259
  }
182
260
  export function createTempDir() {
183
261
  // Create default temp html dir
@@ -191,24 +269,12 @@ export function createTempDir() {
191
269
 
192
270
  return fs.mkdtempSync(path.join(TEMP_HTML_DIR, `html-${timestamp}-`));
193
271
  }
194
- export function emptyDir(dirPath) {
195
- const files = fs.readdirSync(dirPath);
272
+ export async function emptyDir(dirPath) {
273
+ const files = await fsp.readdir(dirPath);
196
274
  for (const file of files) {
197
275
  const fullPath = path.join(dirPath, file);
198
- fs.rmSync(fullPath, { recursive: true, force: true });
199
- }
200
- }
201
- export async function getAvailablePort(startPort = DEFAULT_PORT, maxPort = MAX_PORT) {
202
- let currentPort = startPort;
203
- while (currentPort <= maxPort) {
204
- if (await isPortAvailable(currentPort)) {
205
- return currentPort;
206
- }
207
-
208
- currentPort++;
276
+ await fsp.rm(fullPath, { recursive: true, force: true });
209
277
  }
210
-
211
- return -1;
212
278
  }
213
279
  export async function createSite(inputPath = "", outputPath = "", pathsToCreate = [], ignores = undefined, configs = undefined, interruptCondition = undefined) {
214
280
 
@@ -240,7 +306,7 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
240
306
 
241
307
  // Check if `inputPath` is inside `outputPath` (causing code wipeout)
242
308
  if (isPathInside(outputPath, inputPath)) {
243
- throw `Input path "${inputPath}" cannot be inside or same as output path "${outputPath}"`;
309
+ throw new Error(`Input path "${inputPath}" cannot be inside or same as output path "${outputPath}"`);
244
310
  }
245
311
 
246
312
 
@@ -255,9 +321,10 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
255
321
 
256
322
 
257
323
  // Hard reload, clear output path & Get all paths from `inputPath`
258
- if (pathsToCreate == null) {
259
- emptyDir(outputPath)
260
- pathsToCreate = crawlDir(inputPath);
324
+ let isHardReloading = pathsToCreate == null;
325
+ if (isHardReloading) {
326
+ await emptyDir(outputPath)
327
+ pathsToCreate = await crawlDir(inputPath);
261
328
  }
262
329
 
263
330
 
@@ -299,8 +366,8 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
299
366
 
300
367
 
301
368
  // Filter based on toIgnore() in configs
302
- const toIgnore = await configs?.toIgnore?.(inputPath, outputPath, currentPath);
303
- if (toIgnore) {
369
+ const toBeIgnored = await configs?.toIgnore?.(inputPath, outputPath, currentPath);
370
+ if (toBeIgnored === true || toBeIgnored === null) {
304
371
  return false;
305
372
  }
306
373
 
@@ -321,11 +388,11 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
321
388
 
322
389
 
323
390
  // Broadcast site creation started
324
- log("Creating site...");
325
- await configs?.onSiteCreateStart?.(inputPath, outputPath);
391
+ log(`Starting site creation at ${outputPath} ...`);
392
+ await configs?.onSiteCreateStart?.(inputPath, outputPath, !isHardReloading);
326
393
 
327
394
 
328
- // Iterate through all folders & files
395
+ // Iterate & build all files
329
396
  let wasInterrupted = false;
330
397
  await Promise.all(pathsToCreate.map((currentPath) => limit(async () => {
331
398
 
@@ -349,21 +416,23 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
349
416
  if (!pathExists) {
350
417
  let pathToDelete = isMdx ? absHtmlPath : absToOutput;
351
418
  log(`Deleting ${pathToDelete}`, !toBeVerbose);
419
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, pathToDelete, true);
352
420
  await fsp.rm(pathToDelete, { recursive: true, force: true });
421
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, pathToDelete, true, undefined);
353
422
  }
354
423
  // Make corresponding directory
355
424
  else if (isDir) {
356
425
  log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
357
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
426
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
358
427
  await fsp.mkdir(absToOutput, { recursive: true });
359
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
428
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
360
429
  }
361
430
  // Make html file from mdx
362
431
  else if (isMdx) {
363
432
 
364
433
  // Broadcast file creation started
365
434
  log(`Creating ${currentPath} ---> ${absHtmlPath}`, !toBeVerbose);
366
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath);
435
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absHtmlPath, false);
367
436
 
368
437
 
369
438
  // Intercept mdx code
@@ -382,22 +451,22 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
382
451
 
383
452
 
384
453
  // Broadcast file creation ended
385
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result);
454
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absHtmlPath, false, result);
386
455
  }
387
456
  // Copy paste file
388
457
  else {
389
- log(`Creating ${currentPath} ---> ${absToOutput}`, !configs.toBeVerbose);
390
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
458
+ log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
459
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
391
460
  await fsp.mkdir(path.dirname(absToOutput), { recursive: true });
392
461
  await fsp.copyFile(currentPath, absToOutput);
393
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
462
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
394
463
  }
395
464
  })));
396
465
 
397
466
 
398
467
  // Broadcast site creation ended
399
- log(wasInterrupted ? `Site creation was interrupted!` : `Created site at ${outputPath}`);
400
- await configs?.onSiteCreateEnd?.(inputPath, outputPath, wasInterrupted);
468
+ log(wasInterrupted ? `Site creation was interrupted!` : `Completed site creation at ${outputPath}`);
469
+ await configs?.onSiteCreateEnd?.(inputPath, outputPath, !isHardReloading, wasInterrupted);
401
470
  }
402
471
 
403
472
 
@@ -413,6 +482,8 @@ export class HostMdx {
413
482
  #app = null;
414
483
  #watcher = null;
415
484
  #ignores = null;
485
+ #excludes = null;
486
+ #depGraph = new DependencyGraph();
416
487
 
417
488
 
418
489
  // Constructors
@@ -424,36 +495,41 @@ export class HostMdx {
424
495
 
425
496
 
426
497
  // Private Methods
427
- async #watchForChanges(event, somePath) {
498
+ async #watchForChanges(event, targetPath) {
428
499
 
429
- // Return if out input path itself if passed
430
- if (this.inputPath === somePath) {
500
+ // Skip reload if `toIgnore` gives null
501
+ let ignoreStat = await this.configs?.toIgnore?.(this.inputPath, this.outputPath, targetPath);
502
+ if (ignoreStat === null) {
431
503
  return;
432
504
  }
433
505
 
434
506
 
435
- // Return if matches .ignore file
436
- const relToInput = path.relative(this.inputPath, somePath);
437
- if (this.#ignores.ignores(relToInput)) {
507
+ // Skip reload if has # [EXCLUDE] header in .ignore file
508
+ let relTargetPath = path.relative(this.inputPath, targetPath);
509
+ let excludeStat = this.#excludes?.ignores(relTargetPath);
510
+ if (excludeStat) {
438
511
  return;
439
512
  }
440
513
 
441
514
 
442
- // Return if toIgnore() from configs
443
- const toIgnore = await this.configs?.toIgnore?.(this.inputPath, this.outputPath, somePath);
444
- if (toIgnore) {
445
- return;
515
+ // Update dependency graph
516
+ if (event === "unlink") {
517
+ this.#depGraph.removeEntry(targetPath);
518
+ }
519
+ else {
520
+ this.#depGraph.addEntry(targetPath);
446
521
  }
447
522
 
448
523
 
449
524
  // Add changed path
450
- this.#alteredPaths.push(somePath);
525
+ let dependencies = this.#depGraph.getDeepDependents(targetPath);
526
+ this.#alteredPaths = this.#alteredPaths.concat([...dependencies, targetPath]);
451
527
 
452
528
 
453
529
  // Reflect changes immediately
454
530
  if (this.configs?.trackChanges !== undefined && this.configs?.trackChanges != TrackChanges.NONE) {
455
531
  let toHardReload = this.configs?.trackChanges == TrackChanges.HARD;
456
- log(`${toHardReload ? "Hard recreating" : "Recreating"} site, Event: ${event}, Path: ${somePath}`, !this.configs?.toBeVerbose);
532
+ log(`${toHardReload ? "Hard recreating" : "Recreating"} site, Event: ${event}, Path: ${targetPath}`, !this.configs?.toBeVerbose);
457
533
  await this.recreateSite(toHardReload);
458
534
  }
459
535
  }
@@ -472,7 +548,7 @@ export class HostMdx {
472
548
  await this.stop();
473
549
 
474
550
 
475
- // Asssign all
551
+ // Assign all
476
552
  this.#inputPathProvided = this.inputPath !== "";
477
553
  this.#outputPathProvided = this.outputPath !== "";
478
554
  this.inputPath = this.#inputPathProvided ? this.inputPath : process.cwd();
@@ -503,24 +579,36 @@ export class HostMdx {
503
579
  this.#ignores = getIgnore(ignoreFilePath);
504
580
 
505
581
 
506
- // Broadcast hosting about to start
507
- await this.configs?.onHostStarting?.(this.inputPath, this.outputPath, port);
582
+ // Get excludes
583
+ this.#excludes = getExclude(ignoreFilePath);
508
584
 
509
585
 
510
- // Watch for changes
511
- let chokidarOptions = { ...DEFAULT_CHOKIDAR_OPTIONS, ...(this.configs?.chokidarOptions ?? {}) };
512
- this.#watcher = chokidar.watch(this.inputPath, chokidarOptions).on("all", (event, path) => this.#watchForChanges(event, path));
586
+ // Broadcast hosting about to start
587
+ await this.configs?.onHostStarting?.(this.inputPath, this.outputPath, port);
513
588
 
514
589
 
515
590
  // Delete old files & Create site
516
591
  await this.recreateSite(true);
517
592
 
518
593
 
594
+ // Create dependency graph
595
+ let defaultMdxSettings = { esbuildOptions: () => ({}) };
596
+ let modMdxSettings = await this.configs?.modBundleMDXSettings?.(this.inputPath, this.outputPath, defaultMdxSettings);
597
+ let aliases = modMdxSettings?.esbuildOptions?.({})?.alias ?? {};
598
+ this.#depGraph.setAlias(aliases);
599
+ await this.#depGraph.createGraph(this.inputPath, async (targetPath) => (await this.configs?.toIgnore?.(this.inputPath, this.outputPath, targetPath)) === null || this.#excludes?.ignores(path.relative(this.inputPath, targetPath)));
600
+
601
+
519
602
  // Start server to host site
520
- this.#app = await startServer(this.outputPath, port, (e) => { log(`Failed to start server: ${e.message}`); throw e; });
603
+ this.#app = await startServer(this.outputPath, port, (e) => { log(`Failed to start server: ${e.message}`); });
521
604
  this.#app?.server?.on("close", async () => { await this.configs?.onHostEnded?.(this.inputPath, this.outputPath, port); });
522
605
 
523
606
 
607
+ // Watch for changes
608
+ let chokidarOptions = { ...DEFAULT_CHOKIDAR_OPTIONS, ...(this.configs?.chokidarOptions ?? {}) };
609
+ this.#watcher = chokidar.watch(this.inputPath, chokidarOptions).on("all", async (event, targetPath) => { await this.#watchForChanges(event, targetPath) });
610
+
611
+
524
612
  // Broadcast hosting started
525
613
  await this.configs?.onHostStarted?.(this.inputPath, this.outputPath, port);
526
614
 
@@ -544,7 +632,7 @@ export class HostMdx {
544
632
  if (this.#siteCreationStatus == SiteCreationStatus.ONGOING) {
545
633
  log("Site creation already ongoing! Added to pending")
546
634
  this.#siteCreationStatus = SiteCreationStatus.PENDING_RECREATION;
547
- this.#pendingHardSiteCreation = hardReload;
635
+ this.#pendingHardSiteCreation = this.#pendingHardSiteCreation || hardReload;
548
636
  return;
549
637
  }
550
638
 
@@ -554,12 +642,13 @@ export class HostMdx {
554
642
 
555
643
 
556
644
  // Actual site creation
645
+ let pathsToCreate = hardReload ? null : [...new Set(this.#alteredPaths)];
557
646
  try {
558
- let pathsToCreate = hardReload ? null : [...this.#alteredPaths];
559
647
  this.#alteredPaths = [];
560
648
  await createSite(this.inputPath, this.outputPath, pathsToCreate, this.#ignores, this.configs, () => this.#siteCreationStatus != SiteCreationStatus.ONGOING);
561
649
  }
562
650
  catch (err) {
651
+ this.#alteredPaths = hardReload ? this.#alteredPaths : [...new Set([...pathsToCreate, ...this.#alteredPaths])]; // Readd incase of failure
563
652
  log(`Failed to create site!\n${err.stack}`);
564
653
  }
565
654
 
@@ -568,13 +657,22 @@ export class HostMdx {
568
657
  const wasPending = this.#siteCreationStatus === SiteCreationStatus.PENDING_RECREATION;
569
658
  this.#siteCreationStatus = SiteCreationStatus.NONE;
570
659
  if (wasPending) {
571
- await this.recreateSite(this.#pendingHardSiteCreation);
660
+ log("Recreating previously pending")
661
+ const wasHard = this.#pendingHardSiteCreation;
662
+ this.#pendingHardSiteCreation = false;
663
+ await this.recreateSite(wasHard);
572
664
  }
573
665
  }
574
666
  async abortSiteCreation() {
575
667
  this.#siteCreationStatus = SiteCreationStatus.NONE;
668
+ this.#pendingHardSiteCreation = false;
576
669
  }
577
670
  async stop() {
671
+
672
+ // Abort site creation if ongoing
673
+ await this.abortSiteCreation()
674
+
675
+
578
676
  // Remove temp dir html path
579
677
  if (!this.#outputPathProvided && fs.existsSync(this.outputPath)) {
580
678
  fs.rmSync(this.outputPath, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "host-mdx",
3
- "version": "2.3.1",
3
+ "version": "2.4.1",
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",
@@ -20,12 +20,14 @@
20
20
  "dependencies": {
21
21
  "chokidar": "^5.0.0",
22
22
  "ignore": "^7.0.5",
23
+ "lodash": "^4.17.23",
23
24
  "lowlight": "^3.3.0",
24
25
  "mdx-bundler": "^10.1.1",
25
26
  "p-limit": "^7.3.0",
26
27
  "polka": "^0.5.2",
27
28
  "preact": "^10.28.2",
28
29
  "preact-render-to-string": "^6.6.5",
30
+ "precinct": "^12.2.0",
29
31
  "rehype-highlight": "^7.0.2",
30
32
  "sirv": "^3.0.2"
31
33
  },