host-mdx 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,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,15 @@ 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 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
57
 
58
58
 
59
59
  > **Note:**
60
60
  > 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)
61
+ > 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
62
 
63
63
  <br/>
64
64
 
@@ -82,9 +82,9 @@ Input Directory:
82
82
 
83
83
  ```
84
84
  my-website-template/
85
+ ├─ .hostmdxignore
85
86
  ├─ 404.mdx
86
87
  ├─ index.mdx
87
- ├─ .hostmdxignore
88
88
  ├─ host-mdx.js
89
89
  ├─ about/
90
90
  │ ├─ index.mdx
@@ -124,23 +124,23 @@ export async function onHostStarted(inputPath, outputPath, port) {
124
124
  export async function onHostEnded(inputPath, outputPath, port) {
125
125
  console.log("onHostEnded");
126
126
  }
127
- export async function onSiteCreateStart(inputPath, outputPath) {
127
+ export async function onSiteCreateStart(inputPath, outputPath, isSoftReload) {
128
128
  console.log("onSiteCreateStart");
129
129
  }
130
- export async function onSiteCreateEnd(inputPath, outputPath, wasInterrupted) {
130
+ export async function onSiteCreateEnd(inputPath, outputPath, isSoftReload, wasInterrupted) {
131
131
  console.log("onSiteCreateEnd");
132
132
  }
133
- export async function onFileCreateStart(inputFilePath, outputFilePath, inFilePath, outFilePath) {
134
- console.log("onFileCreateStart");
133
+ export async function onFileChangeStart(inputPath, outputPath, inFilePath, outFilePath, toBeDeleted) {
134
+ console.log("onFileChangeStart");
135
135
  }
136
- export async function onFileCreateEnd(inputFilePath, outputFilePath, inFilePath, outFilePath, result) {
136
+ export async function onFileChangeEnd(inputPath, outputPath, inFilePath, outFilePath, wasDeleted, result) {
137
137
  // `result = undefined` if file is not .mdx
138
138
  // `result.html` contains stringified HTML
139
139
  // `result.exports` contains exports from mdx
140
- console.log("onFileCreateEnd");
140
+ console.log("onFileChangeEnd");
141
141
  }
142
- export async function toIgnore(inputPath, outputPath, path) {
143
- const isGOutputStream = /\.goutputstream-\w+$/.test(path);
142
+ export async function toIgnore(inputPath, outputPath, targetPath) {
143
+ const isGOutputStream = /\.goutputstream-\w+$/.test(targetPath);
144
144
  if (isGOutputStream) {
145
145
  return true;
146
146
  }
@@ -148,7 +148,8 @@ export async function toIgnore(inputPath, outputPath, path) {
148
148
  return false;
149
149
  }
150
150
  export async function modMDXCode(inputPath, outputPath, inFilePath, outFilePath, code){
151
- // Modify code ...
151
+ // Wrapper example
152
+ code = `import Content from "${inFilePath}"; import { Wrapper } from "utils.jsx";\n\n<Wrapper><Content /></Wrapper>`
152
153
  return code;
153
154
  }
154
155
  export async function modGlobalArgs(inputPath, outputPath, globalArgs){
@@ -156,13 +157,31 @@ export async function modGlobalArgs(inputPath, outputPath, globalArgs){
156
157
  return globalArgs;
157
158
  }
158
159
  export async function modBundleMDXSettings(inputPath, outputPath, settings) {
159
- // Modify settings ...
160
+ // Example for adding '@' root alias
161
+ var oldBuildOptions = settings.esbuildOptions;
162
+ settings.esbuildOptions = (options) => {
163
+ options = oldBuildOptions(options)
164
+ options.logLevel = 'error';
165
+ options.alias = {
166
+ ...options.alias,
167
+ '@': inputPath
168
+ };
169
+
170
+ return options;
171
+ }
172
+
173
+
160
174
  return settings;
161
175
  }
162
176
  export async function modRebuildPaths(inputPath, outputPath, rebuildPaths) {
163
177
  // Modify rebuildPaths ...
164
178
  return rebuildPaths;
165
179
  }
180
+ export async function canTriggerReload(inputPath, outputPath, targetPath) {
181
+ const ignoredDirs = new Set(['node_modules', '.git', '.github']);
182
+ const segments = targetPath.split(/[\\/]/); // or targetPath.split(path.sep);
183
+ return !segments.some(segment => ignoredDirs.has(segment));
184
+ }
166
185
  export const chokidarOptions = {
167
186
  awaitWriteFinish: true
168
187
  }
package/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import path from "path";
4
4
  import * as readline from "readline";
5
- import { HostMdx, createSite, TrackChanges, log } from "./index.js";
5
+ import { HostMdx, createSite, TrackChanges, setupConfigs, log } from "./index.js";
6
6
 
7
7
 
8
8
  // Flags
@@ -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) {
@@ -132,7 +132,8 @@ export async function main() {
132
132
  let toCreateOnly = rawArgs.includes(CREATE_FLAG) || rawArgs.includes(CREATE_SHORT_FLAG);
133
133
  if (toCreateOnly) {
134
134
  try {
135
- await createSite(inputPath, outputPath, null, undefined, { toBeVerbose });
135
+ let configs = await setupConfigs(inputPath);
136
+ await createSite(inputPath, outputPath, null, undefined, { ...configs, toBeVerbose });
136
137
  }
137
138
  catch (err) {
138
139
  process.exitCode = 1; // Exit with error code if not created successfully
@@ -176,7 +177,10 @@ export async function main() {
176
177
  // Watch for key press
177
178
  listenForKey(
178
179
  async () => await hostMdx?.recreateSite(),
179
- async () => await hostMdx?.recreateSite(true),
180
+ async () => {
181
+ log("--- HARD RELOADING ---")
182
+ await hostMdx?.recreateSite(true)
183
+ },
180
184
  cleanup
181
185
  );
182
186
 
@@ -0,0 +1,340 @@
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 somePath of list) {
24
+
25
+ // get absolute path
26
+ const absPath = path.join(dir, somePath);
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(somePath, 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 = somePath === alias;
54
+ const isSubPath = somePath.startsWith(`${alias}${path.sep}`) || somePath.startsWith(`${alias}/`);
55
+
56
+ if (isExact || isSubPath) {
57
+ somePath = somePath.replace(alias, aliasPath);
58
+ somePath = path.normalize(somePath);
59
+ break;
60
+ }
61
+ }
62
+
63
+ return somePath;
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 has already been traversed or is a node_modules
73
+ const absolutePath = path.resolve(filePath);
74
+ if (absolutePath.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.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
+ // Get all dependencies
168
+ const absFilePath = path.resolve(this.#rootFolder, relFilePath);
169
+ const dependencies = await calcDependencies(absFilePath, this.#aliases);
170
+ const relDependencies = new Set();
171
+ dependencies.forEach(p => {
172
+ relDependencies.add(path.relative(this.#rootFolder, p));
173
+ })
174
+
175
+
176
+ // Skip if no dependencies
177
+ if (dependencies.size === 0) {
178
+ return;
179
+ }
180
+
181
+
182
+ // Add dependencies
183
+ this.#graph[relFilePath] = {
184
+ [DEPENDENCIES_KEY]: relDependencies,
185
+ [DEPENDENTS_KEY]: new Set()
186
+ };
187
+
188
+
189
+ // Add dependents
190
+ const depList = this.#graph[relFilePath][DEPENDENCIES_KEY];
191
+ depList.forEach(dep => {
192
+ if (this.#graph[dep] === undefined) {
193
+ this.#graph[dep] = {
194
+ [DEPENDENCIES_KEY]: new Set(),
195
+ [DEPENDENTS_KEY]: new Set()
196
+ }
197
+ }
198
+
199
+ this.#graph[dep][DEPENDENTS_KEY].add(relFilePath);
200
+ });
201
+ }
202
+ removeEntry(filePath) {
203
+
204
+ // Get relative path
205
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
206
+
207
+
208
+ // Return if entry does not exist
209
+ if (this.#graph[relFilePath] === undefined) {
210
+ return;
211
+ }
212
+
213
+
214
+ // Remove from dependents
215
+ const depList = this.#graph[relFilePath][DEPENDENCIES_KEY];
216
+ depList.forEach(dep => {
217
+ if (this.#graph[dep] === undefined) {
218
+ return;
219
+ }
220
+
221
+ this.#graph[dep][DEPENDENTS_KEY].delete(relFilePath);
222
+ });
223
+
224
+
225
+ // Remove entry
226
+ delete this.#graph[relFilePath];
227
+ }
228
+ hasEntry(filePath) {
229
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
230
+ return this.#graph[relFilePath] !== undefined;
231
+ }
232
+ getEntry(filePath) {
233
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
234
+ return this.#graph[relFilePath];
235
+ }
236
+ getDependencies(filePath) {
237
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
238
+ let absDeps = new Set();
239
+ let deps = this.#graph?.[relFilePath]?.[DEPENDENCIES_KEY] ?? new Set();
240
+ for (const dep of deps) {
241
+ absDeps.add(path.resolve(this.#rootFolder, dep));
242
+ }
243
+
244
+ return absDeps;
245
+ }
246
+ getDeepDependencies(filePath) {
247
+
248
+ // Get relative path
249
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
250
+
251
+
252
+ // Return empty set if entry does not exist
253
+ if (!this.hasEntry(relFilePath)) {
254
+ return new Set();
255
+ }
256
+
257
+
258
+ // Recursively get dependencies
259
+ const deepDeps = new Set();
260
+ const walk = (currentPath) => {
261
+
262
+ // Skip if not in graph
263
+ const deps = this.getDependencies(currentPath);
264
+ if (!deps) {
265
+ return;
266
+ }
267
+
268
+ // Iterate over all dependencies
269
+ deps.forEach(dep => {
270
+ if (deepDeps.has(dep)) {
271
+ return;
272
+ }
273
+
274
+ // Add to list and continue walking
275
+ deepDeps.add(dep);
276
+ walk(dep);
277
+ });
278
+ };
279
+ walk(relFilePath);
280
+
281
+ return deepDeps;
282
+ }
283
+ getDependents(filePath) {
284
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
285
+ let absDeps = new Set();
286
+ let deps = this.#graph?.[relFilePath]?.[DEPENDENTS_KEY] ?? new Set();
287
+ for (const dep of deps) {
288
+ absDeps.add(path.resolve(this.#rootFolder, dep));
289
+ }
290
+
291
+ return absDeps;
292
+ }
293
+ getDeepDependents(filePath) {
294
+
295
+ // Get relative path
296
+ let relFilePath = ensureRelativePath(this.#rootFolder, filePath);
297
+
298
+
299
+ // Return empty set if entry does not exist
300
+ if (!this.hasEntry(relFilePath)) {
301
+ return new Set();
302
+ }
303
+
304
+
305
+ // Recursively get dependents
306
+ const deepDependents = new Set();
307
+ const walk = (currentPath) => {
308
+
309
+ // Skip if not in graph
310
+ const dependents = this.getDependents(currentPath);
311
+ if (!dependents) {
312
+ return;
313
+ }
314
+
315
+ // Iterate over all dependencies
316
+ dependents.forEach(dependent => {
317
+ if (deepDependents.has(dependent)) {
318
+ return;
319
+ }
320
+
321
+ // Add to list and continue walking
322
+ deepDependents.add(dependent);
323
+ walk(dependent);
324
+ });
325
+ };
326
+ walk(relFilePath);
327
+
328
+
329
+ return deepDependents;
330
+ }
331
+ addAlias(symbol, toPath) {
332
+ this.#aliases[symbol] = toPath;
333
+ }
334
+ removeAlias(symbol) {
335
+ delete this.#aliases[symbol];
336
+ }
337
+ setAlias(newAliases) {
338
+ this.#aliases = newAliases;
339
+ }
340
+ }
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
@@ -60,6 +62,19 @@ const LOG_TIME_OPTIONS = {
60
62
  const DEFAULT_CHOKIDAR_OPTIONS = {
61
63
  ignoreInitial: true
62
64
  };
65
+ const DEFAULT_CONFIGS = {
66
+ // port: 3000, // Intentionally kept commented out, otherwise interferes with auto port assigning DO NOT CHANGE
67
+ chokidarOptions: DEFAULT_CHOKIDAR_OPTIONS,
68
+ trackChanges: 0,
69
+ toBeVerbose: false,
70
+ concurrency: 1,
71
+ chokidarOptions: DEFAULT_CHOKIDAR_OPTIONS,
72
+ canTriggerReload: (inputPath, outputpath, p) => {
73
+ const ignoredDirs = new Set(['node_modules', '.git', '.github']);
74
+ const segments = p.split(path.sep);
75
+ return !segments.some(segment => ignoredDirs.has(segment));
76
+ }
77
+ };
63
78
 
64
79
 
65
80
  // Utility Methods
@@ -79,19 +94,6 @@ async function createFile(filePath, fileContent = "") {
79
94
  await fsp.mkdir(fileLocation, { recursive: true });
80
95
  await fsp.writeFile(filePath, fileContent);
81
96
  }
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
97
  async function startServer(hostDir, port, errorCallback) { // Starts server at given port
96
98
 
97
99
  // Make sure host dir path is absolute
@@ -177,6 +179,16 @@ export function isPathInside(parentPath, childPath) {
177
179
  relation !== path.resolve(childPath)
178
180
  );
179
181
  }
182
+ export async function setupConfigs(inputPath) {
183
+
184
+ let configFilePath = path.join(inputPath, CONFIG_FILE_NAME);
185
+ if (fs.existsSync(configFilePath)) {
186
+ let cleanConfigFilePath = pathToFileURL(configFilePath).href
187
+ return { ...DEFAULT_CONFIGS, ...(await import(cleanConfigFilePath)) };
188
+ }
189
+
190
+ return _.cloneDeep(DEFAULT_CONFIGS);
191
+ }
180
192
  export function createTempDir() {
181
193
  // Create default temp html dir
182
194
  fs.mkdirSync(TEMP_HTML_DIR, { recursive: true });
@@ -238,7 +250,7 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
238
250
 
239
251
  // Check if `inputPath` is inside `outputPath` (causing code wipeout)
240
252
  if (isPathInside(outputPath, inputPath)) {
241
- throw `Input path "${inputPath}" cannot be inside or same as output path "${outputPath}"`;
253
+ throw new Error(`Input path "${inputPath}" cannot be inside or same as output path "${outputPath}"`);
242
254
  }
243
255
 
244
256
 
@@ -253,9 +265,10 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
253
265
 
254
266
 
255
267
  // Hard reload, clear output path & Get all paths from `inputPath`
256
- if (pathsToCreate == null) {
268
+ let isHardReloading = pathsToCreate == null;
269
+ if (isHardReloading) {
257
270
  emptyDir(outputPath)
258
- pathsToCreate = crawlDir(inputPath);
271
+ pathsToCreate = await crawlDir(inputPath);
259
272
  }
260
273
 
261
274
 
@@ -271,7 +284,7 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
271
284
  let configFilePath = path.join(inputPath, `./${CONFIG_FILE_NAME}`);
272
285
  let doesConfigFileExists = fs.existsSync(configFilePath);
273
286
  log(`Importing config file ${configFilePath}`, !doesConfigFileExists);
274
- configs = await setupConfigs(configFilePath);
287
+ configs = await setupConfigs(inputPath);
275
288
  }
276
289
 
277
290
 
@@ -319,8 +332,8 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
319
332
 
320
333
 
321
334
  // Broadcast site creation started
322
- log("Creating site...");
323
- await configs?.onSiteCreateStart?.(inputPath, outputPath);
335
+ log(`Starting site creation at ${outputPath} ...`);
336
+ await configs?.onSiteCreateStart?.(inputPath, outputPath, !isHardReloading);
324
337
 
325
338
 
326
339
  // Iterate through all folders & files
@@ -347,21 +360,23 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
347
360
  if (!pathExists) {
348
361
  let pathToDelete = isMdx ? absHtmlPath : absToOutput;
349
362
  log(`Deleting ${pathToDelete}`, !toBeVerbose);
363
+ await configs?.onFileChangeStart?.(inputPath, outputPath, pathToDelete, absToOutput, true);
350
364
  await fsp.rm(pathToDelete, { recursive: true, force: true });
365
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, pathToDelete, absToOutput, true, undefined);
351
366
  }
352
367
  // Make corresponding directory
353
368
  else if (isDir) {
354
369
  log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
355
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
370
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
356
371
  await fsp.mkdir(absToOutput, { recursive: true });
357
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
372
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
358
373
  }
359
374
  // Make html file from mdx
360
375
  else if (isMdx) {
361
376
 
362
377
  // Broadcast file creation started
363
378
  log(`Creating ${currentPath} ---> ${absHtmlPath}`, !toBeVerbose);
364
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath);
379
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absHtmlPath, false);
365
380
 
366
381
 
367
382
  // Intercept mdx code
@@ -380,22 +395,22 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
380
395
 
381
396
 
382
397
  // Broadcast file creation ended
383
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result);
398
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absHtmlPath, false, result);
384
399
  }
385
400
  // Copy paste file
386
401
  else {
387
- log(`Creating ${currentPath} ---> ${absToOutput}`, !configs.toBeVerbose);
388
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
402
+ log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
403
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
389
404
  await fsp.mkdir(path.dirname(absToOutput), { recursive: true });
390
405
  await fsp.copyFile(currentPath, absToOutput);
391
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
406
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
392
407
  }
393
408
  })));
394
409
 
395
410
 
396
411
  // Broadcast site creation ended
397
- log(wasInterrupted ? `Site creation was interrupted!` : `Created site at ${outputPath}`);
398
- await configs?.onSiteCreateEnd?.(inputPath, outputPath, wasInterrupted);
412
+ log(wasInterrupted ? `Site creation was interrupted!` : `Completed site creation at ${outputPath}`);
413
+ await configs?.onSiteCreateEnd?.(inputPath, outputPath, !isHardReloading, wasInterrupted);
399
414
  }
400
415
 
401
416
 
@@ -411,6 +426,7 @@ export class HostMdx {
411
426
  #app = null;
412
427
  #watcher = null;
413
428
  #ignores = null;
429
+ #depGraph = new DependencyGraph();
414
430
 
415
431
 
416
432
  // Constructors
@@ -424,28 +440,18 @@ export class HostMdx {
424
440
  // Private Methods
425
441
  async #watchForChanges(event, somePath) {
426
442
 
427
- // Return if out input path itself if passed
428
- if (this.inputPath === somePath) {
429
- return;
443
+ // Update dependency graph
444
+ if (event === "unlink") {
445
+ this.#depGraph.removeEntry(somePath);
430
446
  }
431
-
432
-
433
- // Return if matches .ignore file
434
- const relToInput = path.relative(this.inputPath, somePath);
435
- if (this.#ignores.ignores(relToInput)) {
436
- return;
437
- }
438
-
439
-
440
- // Return if toIgnore() from configs
441
- const toIgnore = await this.configs?.toIgnore?.(this.inputPath, this.outputPath, somePath);
442
- if (toIgnore) {
443
- return;
447
+ else {
448
+ this.#depGraph.addEntry(somePath);
444
449
  }
445
450
 
446
451
 
447
452
  // Add changed path
448
- this.#alteredPaths.push(somePath);
453
+ let dependencies = this.#depGraph.getDeepDependents(somePath);
454
+ this.#alteredPaths = this.#alteredPaths.concat([...dependencies, somePath]);
449
455
 
450
456
 
451
457
  // Reflect changes immediately
@@ -470,7 +476,7 @@ export class HostMdx {
470
476
  await this.stop();
471
477
 
472
478
 
473
- // Asssign all
479
+ // Assign all
474
480
  this.#inputPathProvided = this.inputPath !== "";
475
481
  this.#outputPathProvided = this.outputPath !== "";
476
482
  this.inputPath = this.#inputPathProvided ? this.inputPath : process.cwd();
@@ -481,7 +487,7 @@ export class HostMdx {
481
487
  let configFilePath = path.join(this.inputPath, `./${CONFIG_FILE_NAME}`);
482
488
  let doesConfigFileExists = fs.existsSync(configFilePath);
483
489
  log(`Importing config file ${configFilePath}`, !doesConfigFileExists);
484
- this.configs = { ...(await setupConfigs(configFilePath)), ...this.configs };
490
+ this.configs = { ...(await setupConfigs(this.inputPath)), ...this.configs };
485
491
 
486
492
 
487
493
  // Get port
@@ -507,13 +513,21 @@ export class HostMdx {
507
513
 
508
514
  // Watch for changes
509
515
  let chokidarOptions = { ...DEFAULT_CHOKIDAR_OPTIONS, ...(this.configs?.chokidarOptions ?? {}) };
510
- this.#watcher = chokidar.watch(this.inputPath, chokidarOptions).on("all", (event, path) => this.#watchForChanges(event, path));
516
+ this.#watcher = chokidar.watch(this.inputPath, chokidarOptions).on("all", (event, targetPath) => this.#watchForChanges(event, targetPath));
511
517
 
512
518
 
513
519
  // Delete old files & Create site
514
520
  await this.recreateSite(true);
515
521
 
516
522
 
523
+ // Create dependency graph
524
+ let defaultMdxSettings = { esbuildOptions: () => ({}) };
525
+ let modMdxSettings = await this.configs?.modBundleMDXSettings?.(this.inputPath, this.outputPath, defaultMdxSettings);
526
+ let aliases = modMdxSettings?.esbuildOptions?.({})?.alias ?? {};
527
+ this.#depGraph.setAlias(aliases);
528
+ await this.#depGraph.createGraph(this.inputPath, async (p) => !(await this.configs?.canTriggerReload?.(this.inputPath, this.outputPath, p)));
529
+
530
+
517
531
  // Start server to host site
518
532
  this.#app = await startServer(this.outputPath, port, (e) => { log(`Failed to start server: ${e.message}`); throw e; });
519
533
  this.#app?.server?.on("close", async () => { await this.configs?.onHostEnded?.(this.inputPath, this.outputPath, port); });
@@ -542,7 +556,7 @@ export class HostMdx {
542
556
  if (this.#siteCreationStatus == SiteCreationStatus.ONGOING) {
543
557
  log("Site creation already ongoing! Added to pending")
544
558
  this.#siteCreationStatus = SiteCreationStatus.PENDING_RECREATION;
545
- this.#pendingHardSiteCreation = hardReload;
559
+ this.#pendingHardSiteCreation = this.#pendingHardSiteCreation || hardReload;
546
560
  return;
547
561
  }
548
562
 
@@ -554,8 +568,8 @@ export class HostMdx {
554
568
  // Actual site creation
555
569
  try {
556
570
  let pathsToCreate = hardReload ? null : [...this.#alteredPaths];
557
- this.#alteredPaths = [];
558
571
  await createSite(this.inputPath, this.outputPath, pathsToCreate, this.#ignores, this.configs, () => this.#siteCreationStatus != SiteCreationStatus.ONGOING);
572
+ this.#alteredPaths = [];
559
573
  }
560
574
  catch (err) {
561
575
  log(`Failed to create site!\n${err.stack}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "host-mdx",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "A cli tool to create and serve a static html website from a given mdx directory",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
  },