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 +34 -15
- package/cli.js +8 -4
- package/dependency-graph.js +340 -0
- package/index.js +64 -50
- 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
|
@@ -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
|
|
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 () =>
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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?.
|
|
370
|
+
await configs?.onFileChangeStart?.(inputPath, outputPath, currentPath, absToOutput, false);
|
|
356
371
|
await fsp.mkdir(absToOutput, { recursive: true });
|
|
357
|
-
await configs?.
|
|
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?.
|
|
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?.
|
|
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}`, !
|
|
388
|
-
await configs?.
|
|
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?.
|
|
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!` : `
|
|
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
|
-
//
|
|
428
|
-
if (
|
|
429
|
-
|
|
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.#
|
|
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
|
-
//
|
|
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(
|
|
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,
|
|
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
|
+
"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
|
},
|