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