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 +34 -15
- package/cli.js +5 -2
- package/dependency-graph.js +340 -0
- package/index.js +54 -42
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# host-mdx
|
|
1
|
+
# 🌐 host-mdx
|
|
2
2
|
|
|
3
3
|
[](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
|
|
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
|
|
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
|
|
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
|
|
134
|
-
console.log("
|
|
133
|
+
export async function onFileChangeStart(inputPath, outputPath, inFilePath, outFilePath, toBeDeleted) {
|
|
134
|
+
console.log("onFileChangeStart");
|
|
135
135
|
}
|
|
136
|
-
export async function
|
|
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("
|
|
140
|
+
console.log("onFileChangeEnd");
|
|
141
141
|
}
|
|
142
|
-
export async function toIgnore(inputPath, outputPath,
|
|
143
|
-
const isGOutputStream = /\.goutputstream-\w+$/.test(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 () =>
|
|
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
|
-
|
|
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(
|
|
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?.
|
|
370
|
+
await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
|
|
358
371
|
await fsp.mkdir(absToOutput, { recursive: true });
|
|
359
|
-
await configs?.
|
|
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?.
|
|
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?.
|
|
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}`, !
|
|
390
|
-
await configs?.
|
|
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?.
|
|
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!` : `
|
|
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
|
-
//
|
|
430
|
-
if (
|
|
431
|
-
|
|
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.#
|
|
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
|
-
//
|
|
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,
|
|
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
|
+
"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
|
},
|