host-mdx 2.3.1 → 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
@@ -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) {
@@ -177,7 +177,10 @@ export async function main() {
177
177
  // Watch for key press
178
178
  listenForKey(
179
179
  async () => await hostMdx?.recreateSite(),
180
- async () => await hostMdx?.recreateSite(true),
180
+ async () => {
181
+ log("--- HARD RELOADING ---")
182
+ await hostMdx?.recreateSite(true)
183
+ },
181
184
  cleanup
182
185
  );
183
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,11 +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
97
  async function startServer(hostDir, port, errorCallback) { // Starts server at given port
88
98
 
89
99
  // Make sure host dir path is absolute
@@ -174,10 +184,10 @@ export async function setupConfigs(inputPath) {
174
184
  let configFilePath = path.join(inputPath, CONFIG_FILE_NAME);
175
185
  if (fs.existsSync(configFilePath)) {
176
186
  let cleanConfigFilePath = pathToFileURL(configFilePath).href
177
- return await import(cleanConfigFilePath);
187
+ return { ...DEFAULT_CONFIGS, ...(await import(cleanConfigFilePath)) };
178
188
  }
179
189
 
180
- return {};
190
+ return _.cloneDeep(DEFAULT_CONFIGS);
181
191
  }
182
192
  export function createTempDir() {
183
193
  // Create default temp html dir
@@ -240,7 +250,7 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
240
250
 
241
251
  // Check if `inputPath` is inside `outputPath` (causing code wipeout)
242
252
  if (isPathInside(outputPath, inputPath)) {
243
- 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}"`);
244
254
  }
245
255
 
246
256
 
@@ -255,9 +265,10 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
255
265
 
256
266
 
257
267
  // Hard reload, clear output path & Get all paths from `inputPath`
258
- if (pathsToCreate == null) {
268
+ let isHardReloading = pathsToCreate == null;
269
+ if (isHardReloading) {
259
270
  emptyDir(outputPath)
260
- pathsToCreate = crawlDir(inputPath);
271
+ pathsToCreate = await crawlDir(inputPath);
261
272
  }
262
273
 
263
274
 
@@ -321,8 +332,8 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
321
332
 
322
333
 
323
334
  // Broadcast site creation started
324
- log("Creating site...");
325
- await configs?.onSiteCreateStart?.(inputPath, outputPath);
335
+ log(`Starting site creation at ${outputPath} ...`);
336
+ await configs?.onSiteCreateStart?.(inputPath, outputPath, !isHardReloading);
326
337
 
327
338
 
328
339
  // Iterate through all folders & files
@@ -349,21 +360,23 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
349
360
  if (!pathExists) {
350
361
  let pathToDelete = isMdx ? absHtmlPath : absToOutput;
351
362
  log(`Deleting ${pathToDelete}`, !toBeVerbose);
363
+ await configs?.onFileChangeStart?.(inputPath, outputPath, pathToDelete, absToOutput, true);
352
364
  await fsp.rm(pathToDelete, { recursive: true, force: true });
365
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, pathToDelete, absToOutput, true, undefined);
353
366
  }
354
367
  // Make corresponding directory
355
368
  else if (isDir) {
356
369
  log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
357
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
370
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
358
371
  await fsp.mkdir(absToOutput, { recursive: true });
359
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
372
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
360
373
  }
361
374
  // Make html file from mdx
362
375
  else if (isMdx) {
363
376
 
364
377
  // Broadcast file creation started
365
378
  log(`Creating ${currentPath} ---> ${absHtmlPath}`, !toBeVerbose);
366
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absHtmlPath);
379
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absHtmlPath, false);
367
380
 
368
381
 
369
382
  // Intercept mdx code
@@ -382,22 +395,22 @@ export async function createSite(inputPath = "", outputPath = "", pathsToCreate
382
395
 
383
396
 
384
397
  // Broadcast file creation ended
385
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absHtmlPath, result);
398
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absHtmlPath, false, result);
386
399
  }
387
400
  // Copy paste file
388
401
  else {
389
- log(`Creating ${currentPath} ---> ${absToOutput}`, !configs.toBeVerbose);
390
- await configs?.onFileCreateStart?.(inputPath, outputPath, currentPath, absToOutput);
402
+ log(`Creating ${currentPath} ---> ${absToOutput}`, !toBeVerbose);
403
+ await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
391
404
  await fsp.mkdir(path.dirname(absToOutput), { recursive: true });
392
405
  await fsp.copyFile(currentPath, absToOutput);
393
- await configs?.onFileCreateEnd?.(inputPath, outputPath, currentPath, absToOutput, undefined);
406
+ await configs?.onFileChangeEnd?.(inputPath, outputPath, currentPath, absToOutput, false, undefined);
394
407
  }
395
408
  })));
396
409
 
397
410
 
398
411
  // Broadcast site creation ended
399
- log(wasInterrupted ? `Site creation was interrupted!` : `Created site at ${outputPath}`);
400
- 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);
401
414
  }
402
415
 
403
416
 
@@ -413,6 +426,7 @@ export class HostMdx {
413
426
  #app = null;
414
427
  #watcher = null;
415
428
  #ignores = null;
429
+ #depGraph = new DependencyGraph();
416
430
 
417
431
 
418
432
  // Constructors
@@ -426,28 +440,18 @@ export class HostMdx {
426
440
  // Private Methods
427
441
  async #watchForChanges(event, somePath) {
428
442
 
429
- // Return if out input path itself if passed
430
- if (this.inputPath === somePath) {
431
- return;
432
- }
433
-
434
-
435
- // Return if matches .ignore file
436
- const relToInput = path.relative(this.inputPath, somePath);
437
- if (this.#ignores.ignores(relToInput)) {
438
- return;
443
+ // Update dependency graph
444
+ if (event === "unlink") {
445
+ this.#depGraph.removeEntry(somePath);
439
446
  }
440
-
441
-
442
- // Return if toIgnore() from configs
443
- const toIgnore = await this.configs?.toIgnore?.(this.inputPath, this.outputPath, somePath);
444
- if (toIgnore) {
445
- return;
447
+ else {
448
+ this.#depGraph.addEntry(somePath);
446
449
  }
447
450
 
448
451
 
449
452
  // Add changed path
450
- this.#alteredPaths.push(somePath);
453
+ let dependencies = this.#depGraph.getDeepDependents(somePath);
454
+ this.#alteredPaths = this.#alteredPaths.concat([...dependencies, somePath]);
451
455
 
452
456
 
453
457
  // Reflect changes immediately
@@ -472,7 +476,7 @@ export class HostMdx {
472
476
  await this.stop();
473
477
 
474
478
 
475
- // Asssign all
479
+ // Assign all
476
480
  this.#inputPathProvided = this.inputPath !== "";
477
481
  this.#outputPathProvided = this.outputPath !== "";
478
482
  this.inputPath = this.#inputPathProvided ? this.inputPath : process.cwd();
@@ -509,13 +513,21 @@ export class HostMdx {
509
513
 
510
514
  // Watch for changes
511
515
  let chokidarOptions = { ...DEFAULT_CHOKIDAR_OPTIONS, ...(this.configs?.chokidarOptions ?? {}) };
512
- 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));
513
517
 
514
518
 
515
519
  // Delete old files & Create site
516
520
  await this.recreateSite(true);
517
521
 
518
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
+
519
531
  // Start server to host site
520
532
  this.#app = await startServer(this.outputPath, port, (e) => { log(`Failed to start server: ${e.message}`); throw e; });
521
533
  this.#app?.server?.on("close", async () => { await this.configs?.onHostEnded?.(this.inputPath, this.outputPath, port); });
@@ -544,7 +556,7 @@ export class HostMdx {
544
556
  if (this.#siteCreationStatus == SiteCreationStatus.ONGOING) {
545
557
  log("Site creation already ongoing! Added to pending")
546
558
  this.#siteCreationStatus = SiteCreationStatus.PENDING_RECREATION;
547
- this.#pendingHardSiteCreation = hardReload;
559
+ this.#pendingHardSiteCreation = this.#pendingHardSiteCreation || hardReload;
548
560
  return;
549
561
  }
550
562
 
@@ -556,8 +568,8 @@ export class HostMdx {
556
568
  // Actual site creation
557
569
  try {
558
570
  let pathsToCreate = hardReload ? null : [...this.#alteredPaths];
559
- this.#alteredPaths = [];
560
571
  await createSite(this.inputPath, this.outputPath, pathsToCreate, this.#ignores, this.configs, () => this.#siteCreationStatus != SiteCreationStatus.ONGOING);
572
+ this.#alteredPaths = [];
561
573
  }
562
574
  catch (err) {
563
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.1",
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
  },