mr-md 2.1.1-beta → 2.2.0-beta
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/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +23 -25
- package/dist/cli/dev.d.ts +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +77 -62
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +3 -1
- package/dist/cli.js +0 -0
- package/dist/renderer/utils.d.ts.map +1 -1
- package/dist/renderer/utils.js +5 -3
- package/package.json +2 -2
- package/src/builder.ts +25 -27
- package/src/cli/dev.ts +81 -62
- package/src/cli/init.ts +3 -1
- package/src/renderer/utils.ts +5 -3
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEX,gBAAgB,EAEhB,YAAY,EACZ,YAAY,EACZ,OAAO,EAGP,UAAU,EAEV,cAAc,EAId,YAAY,EACZ,MAAM,EACN,UAAU,EAGV,YAAY,EACZ,SAAS,EAIT,iBAAiB,EAEjB,cAAc,EACd,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"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"}
|
package/dist/builder.js
CHANGED
|
@@ -5,31 +5,29 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
function getCallerDir() {
|
|
8
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
9
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
8
10
|
const err = new Error();
|
|
9
|
-
const stack = err.stack
|
|
10
|
-
|
|
11
|
+
const stack = err.stack;
|
|
12
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
13
|
+
if (!stack || !Array.isArray(stack))
|
|
11
14
|
return undefined;
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (p === builderFilePath) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
return path.dirname(p);
|
|
15
|
+
for (let i = 0; i < stack.length; i++) {
|
|
16
|
+
const callSite = stack[i];
|
|
17
|
+
let p = callSite.getFileName();
|
|
18
|
+
if (!p)
|
|
19
|
+
continue;
|
|
20
|
+
if (p.startsWith("file://")) {
|
|
21
|
+
p = fileURLToPath(p);
|
|
22
|
+
}
|
|
23
|
+
else if (p.startsWith("/") && p[2] === ":") {
|
|
24
|
+
p = p.substring(1);
|
|
25
|
+
}
|
|
26
|
+
const basename = path.basename(p);
|
|
27
|
+
if (basename === "builder.ts" || basename === "builder.js" || basename === "index.ts" || basename === "index.js") {
|
|
28
|
+
continue;
|
|
32
29
|
}
|
|
30
|
+
return path.dirname(p);
|
|
33
31
|
}
|
|
34
32
|
return undefined;
|
|
35
33
|
}
|
|
@@ -307,7 +305,7 @@ export class LessonBuilder {
|
|
|
307
305
|
const base = this.options.contentBase ?? process.cwd();
|
|
308
306
|
const resolved = path.resolve(base, src);
|
|
309
307
|
const ext = path.extname(resolved);
|
|
310
|
-
const configPath = `${resolved.slice(0, -ext.length)}.config.json`;
|
|
308
|
+
const configPath = `${ext.length > 0 ? resolved.slice(0, -ext.length) : resolved}.config.json`;
|
|
311
309
|
if (fs.existsSync(configPath)) {
|
|
312
310
|
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
313
311
|
}
|
|
@@ -417,7 +415,7 @@ export class LessonBuilder {
|
|
|
417
415
|
const lesson = { meta: this.meta, blocks: this.blocks };
|
|
418
416
|
const html = render(lesson, this.options);
|
|
419
417
|
const outDir = path.resolve(this.options.outDir);
|
|
420
|
-
const outPath = path.join(outDir,
|
|
418
|
+
const outPath = path.join(outDir, "index.html");
|
|
421
419
|
const outPathDir = path.dirname(outPath);
|
|
422
420
|
if (!fs.existsSync(outPathDir))
|
|
423
421
|
fs.mkdirSync(outPathDir, { recursive: true });
|
|
@@ -596,7 +594,7 @@ export class ChapterBuilder {
|
|
|
596
594
|
const chapterData = { meta: this.meta, lessons };
|
|
597
595
|
const html = renderChapter(chapterData, this.options);
|
|
598
596
|
const outDir = path.resolve(this.options.outDir);
|
|
599
|
-
const outPath = path.join(outDir,
|
|
597
|
+
const outPath = path.join(outDir, "index.html");
|
|
600
598
|
const outPathDir = path.dirname(outPath);
|
|
601
599
|
if (!fs.existsSync(outPathDir))
|
|
602
600
|
fs.mkdirSync(outPathDir, { recursive: true });
|
package/dist/cli/dev.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function runDev(args: string[]):
|
|
1
|
+
export declare function runDev(args: string[]): void;
|
|
2
2
|
//# sourceMappingURL=dev.d.ts.map
|
package/dist/cli/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAKA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QA4HpC"}
|
package/dist/cli/dev.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
|
|
4
|
-
export async function runDev(args) {
|
|
3
|
+
export function runDev(args) {
|
|
5
4
|
const dir = args[0] || ".";
|
|
6
|
-
|
|
5
|
+
let outDir = path.resolve(process.cwd(), dir, "out");
|
|
7
6
|
console.log(`Starting dev server for directory: ${dir}`);
|
|
8
7
|
let server;
|
|
8
|
+
let currentBuild = null;
|
|
9
9
|
const rebuild = () => {
|
|
10
|
+
if (currentBuild) {
|
|
11
|
+
currentBuild.kill();
|
|
12
|
+
}
|
|
10
13
|
console.log("Rebuilding...");
|
|
11
|
-
const entryPoints = ["chapter.ts", "index.ts", "lesson.ts"];
|
|
14
|
+
const entryPoints = ["chapter.ts", "index.ts", "lesson.ts", "chapters/01-chapter/chapter.ts"];
|
|
12
15
|
for (const entry of entryPoints) {
|
|
13
16
|
const entryPath = path.join(dir, entry);
|
|
14
17
|
if (fs.existsSync(entryPath)) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
outDir = path.resolve(process.cwd(), path.dirname(entryPath), "out");
|
|
19
|
+
currentBuild = Bun.spawn([process.execPath, entryPath], {
|
|
20
|
+
env: { ...process.env, NODE_ENV: "development" },
|
|
21
|
+
onExit(proc, exitCode, signalCode, error) {
|
|
22
|
+
if (exitCode === 0) {
|
|
23
|
+
console.log("Build successful.");
|
|
24
|
+
server?.publish("livereload", "reload");
|
|
25
|
+
}
|
|
26
|
+
else if (exitCode !== null) {
|
|
27
|
+
console.error(`Build failed with exit code ${exitCode}`);
|
|
28
|
+
}
|
|
21
29
|
}
|
|
22
30
|
});
|
|
23
31
|
return;
|
|
@@ -29,7 +37,7 @@ export async function runDev(args) {
|
|
|
29
37
|
if (fs.existsSync(dir)) {
|
|
30
38
|
let timeout;
|
|
31
39
|
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
32
|
-
if (!filename || filename.includes("out/") || filename.includes(".git/"))
|
|
40
|
+
if (!filename || filename.includes("out/") || filename.includes("out\\") || filename.includes(".git/") || filename.includes(".git\\"))
|
|
33
41
|
return;
|
|
34
42
|
clearTimeout(timeout);
|
|
35
43
|
timeout = setTimeout(() => {
|
|
@@ -39,62 +47,69 @@ export async function runDev(args) {
|
|
|
39
47
|
});
|
|
40
48
|
console.log(`Watching ${dir} for changes...`);
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const basePort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
51
|
+
let port = basePort;
|
|
52
|
+
const maxPort = basePort + 10;
|
|
53
|
+
const fetchHandler = async (req, srv) => {
|
|
54
|
+
if (srv.upgrade(req))
|
|
55
|
+
return;
|
|
56
|
+
const url = new URL(req.url);
|
|
57
|
+
const decodedPath = decodeURIComponent(url.pathname);
|
|
58
|
+
let filePath = path.resolve(outDir, "." + decodedPath);
|
|
59
|
+
if (decodedPath.endsWith("/")) {
|
|
60
|
+
filePath = path.join(outDir, "index.html");
|
|
61
|
+
}
|
|
62
|
+
if (!filePath.startsWith(outDir + path.sep) && filePath !== outDir) {
|
|
63
|
+
return new Response("Forbidden", { status: 403 });
|
|
64
|
+
}
|
|
65
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
66
|
+
if (filePath.endsWith(".html")) {
|
|
67
|
+
const file = Bun.file(filePath);
|
|
68
|
+
let text = await file.text();
|
|
69
|
+
const script = `<script>
|
|
70
|
+
const ws = new WebSocket(\`ws://\${location.host}/\`);
|
|
71
|
+
ws.onmessage = (e) => { if (e.data === "reload") location.reload(); };
|
|
72
|
+
</script>`;
|
|
73
|
+
const bodyRegex = /<\/body>/i;
|
|
74
|
+
const match = text.match(bodyRegex);
|
|
75
|
+
if (match && match.index !== undefined) {
|
|
76
|
+
text = text.slice(0, match.index) + script + text.slice(match.index);
|
|
55
77
|
}
|
|
56
78
|
else {
|
|
57
|
-
|
|
58
|
-
if (chapterFile) {
|
|
59
|
-
filePath = path.join(outDir, chapterFile);
|
|
60
|
-
}
|
|
61
|
-
else if (htmlFiles.length > 0) {
|
|
62
|
-
filePath = path.join(outDir, htmlFiles[0]);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
filePath = path.join(outDir, "index.html");
|
|
66
|
-
}
|
|
79
|
+
text += script;
|
|
67
80
|
}
|
|
81
|
+
return new Response(text, { headers: { "Content-Type": "text/html" } });
|
|
68
82
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
return new Response(Bun.file(filePath));
|
|
84
|
+
}
|
|
85
|
+
return new Response("Not found", { status: 404 });
|
|
86
|
+
};
|
|
87
|
+
const wsHandler = {
|
|
88
|
+
message() { },
|
|
89
|
+
open(ws) { ws.subscribe("livereload"); }
|
|
90
|
+
};
|
|
91
|
+
while (port <= maxPort) {
|
|
92
|
+
try {
|
|
93
|
+
server = Bun.serve({
|
|
94
|
+
port,
|
|
95
|
+
fetch: fetchHandler,
|
|
96
|
+
websocket: wsHandler
|
|
97
|
+
});
|
|
98
|
+
console.log(`Dev server listening on http://localhost:${server.port}`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code === 'EADDRINUSE') {
|
|
103
|
+
console.warn(`Port ${port} is in use, trying ${port + 1}...`);
|
|
104
|
+
port++;
|
|
86
105
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (srcPath.startsWith(baseDir + path.sep) && fs.existsSync(srcPath) && fs.statSync(srcPath).isFile()) {
|
|
90
|
-
return new Response(Bun.file(srcPath));
|
|
106
|
+
else {
|
|
107
|
+
throw err;
|
|
91
108
|
}
|
|
92
|
-
return new Response("Not found", { status: 404 });
|
|
93
|
-
},
|
|
94
|
-
websocket: {
|
|
95
|
-
message() { },
|
|
96
|
-
open(ws) { ws.subscribe("livereload"); }
|
|
97
109
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
110
|
+
}
|
|
111
|
+
if (!server) {
|
|
112
|
+
console.error(`Could not find an open port between ${basePort} and ${maxPort}.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
100
115
|
}
|
package/dist/cli/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAIA,wBAAsB,OAAO,kBA8F5B"}
|
package/dist/cli/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
3
4
|
export async function runInit() {
|
|
4
5
|
console.log("Initializing md project structure...");
|
|
5
6
|
const dirs = [
|
|
@@ -71,7 +72,8 @@ export const firstLesson = lesson("First Lesson", { contentBase: import.meta.dir
|
|
|
71
72
|
let mrMdVersion = "latest";
|
|
72
73
|
try {
|
|
73
74
|
// Find mr-md's own package.json to get its version
|
|
74
|
-
const
|
|
75
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
76
|
+
const __dirname = path.dirname(__filename);
|
|
75
77
|
const ownPkgPath = path.resolve(__dirname, "../../package.json");
|
|
76
78
|
const ownPkg = JSON.parse(fs.readFileSync(ownPkgPath, "utf-8"));
|
|
77
79
|
mrMdVersion = ownPkg.version;
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;CACpD;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CA0DR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;CACpD;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CA0DR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAyDnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/renderer/utils.js
CHANGED
|
@@ -28,14 +28,14 @@ function resolveContent(src, options, expectedType = "text") {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
const baseDir = path.resolve(options.contentBase ?? ".");
|
|
31
|
-
if (!filePath.startsWith(baseDir) && options.strict !== false) {
|
|
31
|
+
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir && options.strict !== false) {
|
|
32
32
|
throw new Error(`Security Error: Path traversal attempt outside contentBase: ${filePath}`);
|
|
33
33
|
}
|
|
34
34
|
if (fs.existsSync(filePath)) {
|
|
35
35
|
const stat = fs.statSync(filePath);
|
|
36
36
|
if (stat.isFile()) {
|
|
37
37
|
if (expectedType === "js" && (filePath.endsWith(".js") || filePath.endsWith(".ts") || filePath.endsWith(".jsx") || filePath.endsWith(".tsx"))) {
|
|
38
|
-
const out = spawnSync(
|
|
38
|
+
const out = spawnSync(process.execPath, ["build", "--target=browser", filePath]);
|
|
39
39
|
if (out.status === 0) {
|
|
40
40
|
return out.stdout.toString("utf-8");
|
|
41
41
|
}
|
|
@@ -92,7 +92,9 @@ function resolveAssetSrc(src, options) {
|
|
|
92
92
|
fs.mkdirSync(assetsDir, { recursive: true });
|
|
93
93
|
}
|
|
94
94
|
// Create a safe filename with hash to avoid collisions
|
|
95
|
-
|
|
95
|
+
// Use relative path for hashing to ensure deterministic builds across different machines
|
|
96
|
+
const relPathForHash = path.relative(options.contentBase ?? process.cwd(), filePath);
|
|
97
|
+
const hash = crypto.createHash("md5").update(relPathForHash).digest("hex").substring(0, 8);
|
|
96
98
|
const ext = path.extname(filePath);
|
|
97
99
|
const filename = `${path.basename(filePath, ext)}-${hash}${ext}`;
|
|
98
100
|
const outPath = path.join(assetsDir, filename);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mr-md",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-beta",
|
|
4
4
|
"description": "Mr Markdown is an opinionated TypeScript SDK for building interactive learning pages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "tsc -p tsconfig.build.json &&
|
|
23
|
+
"build": "tsc -p tsconfig.build.json && bun -e \"import {mkdirSync, cpSync} from 'fs'; mkdirSync('dist/styles', {recursive:true}); mkdirSync('dist/client', {recursive:true}); cpSync('src/styles', 'dist/styles', {recursive:true}); cpSync('src/client', 'dist/client', {recursive:true})\"",
|
|
24
24
|
"prepublishOnly": "bun run build",
|
|
25
25
|
"build:docs": "bun docs-site/build.ts",
|
|
26
26
|
"serve:docs": "bunx serve docs-site/out",
|
package/src/builder.ts
CHANGED
|
@@ -36,32 +36,30 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
36
36
|
const __dirname = path.dirname(__filename);
|
|
37
37
|
|
|
38
38
|
function getCallerDir(): string | undefined {
|
|
39
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
40
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
39
41
|
const err = new Error();
|
|
40
|
-
const stack = err.stack
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (let i =
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (p === builderFilePath) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
return path.dirname(p);
|
|
42
|
+
const stack = err.stack as any as NodeJS.CallSite[];
|
|
43
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
44
|
+
|
|
45
|
+
if (!stack || !Array.isArray(stack)) return undefined;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < stack.length; i++) {
|
|
48
|
+
const callSite = stack[i];
|
|
49
|
+
let p = callSite.getFileName();
|
|
50
|
+
if (!p) continue;
|
|
51
|
+
|
|
52
|
+
if (p.startsWith("file://")) {
|
|
53
|
+
p = fileURLToPath(p);
|
|
54
|
+
} else if (p.startsWith("/") && p[2] === ":") {
|
|
55
|
+
p = p.substring(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const basename = path.basename(p);
|
|
59
|
+
if (basename === "builder.ts" || basename === "builder.js" || basename === "index.ts" || basename === "index.js") {
|
|
60
|
+
continue;
|
|
64
61
|
}
|
|
62
|
+
return path.dirname(p);
|
|
65
63
|
}
|
|
66
64
|
return undefined;
|
|
67
65
|
}
|
|
@@ -405,7 +403,7 @@ export class LessonBuilder {
|
|
|
405
403
|
const base = this.options.contentBase ?? process.cwd();
|
|
406
404
|
const resolved = path.resolve(base, src);
|
|
407
405
|
const ext = path.extname(resolved);
|
|
408
|
-
const configPath = `${resolved.slice(0, -ext.length)}.config.json`;
|
|
406
|
+
const configPath = `${ext.length > 0 ? resolved.slice(0, -ext.length) : resolved}.config.json`;
|
|
409
407
|
if (fs.existsSync(configPath)) {
|
|
410
408
|
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
411
409
|
}
|
|
@@ -532,7 +530,7 @@ export class LessonBuilder {
|
|
|
532
530
|
const html = render(lesson, this.options);
|
|
533
531
|
|
|
534
532
|
const outDir = path.resolve(this.options.outDir as string);
|
|
535
|
-
const outPath = path.join(outDir,
|
|
533
|
+
const outPath = path.join(outDir, "index.html");
|
|
536
534
|
const outPathDir = path.dirname(outPath);
|
|
537
535
|
|
|
538
536
|
if (!fs.existsSync(outPathDir)) fs.mkdirSync(outPathDir, { recursive: true });
|
|
@@ -759,7 +757,7 @@ export class ChapterBuilder {
|
|
|
759
757
|
const html = renderChapter(chapterData, this.options);
|
|
760
758
|
|
|
761
759
|
const outDir = path.resolve(this.options.outDir as string);
|
|
762
|
-
const outPath = path.join(outDir,
|
|
760
|
+
const outPath = path.join(outDir, "index.html");
|
|
763
761
|
const outPathDir = path.dirname(outPath);
|
|
764
762
|
|
|
765
763
|
if (!fs.existsSync(outPathDir)) fs.mkdirSync(outPathDir, { recursive: true });
|
package/src/cli/dev.ts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { exec } from "child_process";
|
|
4
3
|
|
|
5
4
|
declare const Bun: any;
|
|
6
5
|
|
|
7
|
-
export
|
|
6
|
+
export function runDev(args: string[]) {
|
|
8
7
|
const dir = args[0] || ".";
|
|
9
|
-
|
|
8
|
+
let outDir = path.resolve(process.cwd(), dir, "out");
|
|
10
9
|
|
|
11
10
|
console.log(`Starting dev server for directory: ${dir}`);
|
|
12
11
|
|
|
13
12
|
let server: any;
|
|
13
|
+
let currentBuild: any = null;
|
|
14
14
|
|
|
15
15
|
const rebuild = () => {
|
|
16
|
+
if (currentBuild) {
|
|
17
|
+
currentBuild.kill();
|
|
18
|
+
}
|
|
16
19
|
console.log("Rebuilding...");
|
|
17
|
-
const entryPoints = ["chapter.ts", "index.ts", "lesson.ts"];
|
|
20
|
+
const entryPoints = ["chapter.ts", "index.ts", "lesson.ts", "chapters/01-chapter/chapter.ts"];
|
|
18
21
|
for (const entry of entryPoints) {
|
|
19
22
|
const entryPath = path.join(dir, entry);
|
|
20
23
|
if (fs.existsSync(entryPath)) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
outDir = path.resolve(process.cwd(), path.dirname(entryPath), "out");
|
|
25
|
+
currentBuild = Bun.spawn([process.execPath, entryPath], {
|
|
26
|
+
env: { ...process.env, NODE_ENV: "development" },
|
|
27
|
+
onExit(proc: any, exitCode: number, signalCode: number, error: string) {
|
|
28
|
+
if (exitCode === 0) {
|
|
29
|
+
console.log("Build successful.");
|
|
30
|
+
server?.publish("livereload", "reload");
|
|
31
|
+
} else if (exitCode !== null) {
|
|
32
|
+
console.error(`Build failed with exit code ${exitCode}`);
|
|
33
|
+
}
|
|
26
34
|
}
|
|
27
35
|
});
|
|
28
36
|
return;
|
|
@@ -36,7 +44,7 @@ export async function runDev(args: string[]) {
|
|
|
36
44
|
if (fs.existsSync(dir)) {
|
|
37
45
|
let timeout: NodeJS.Timeout;
|
|
38
46
|
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
39
|
-
if (!filename || filename.includes("out/") || filename.includes(".git/")) return;
|
|
47
|
+
if (!filename || filename.includes("out/") || filename.includes("out\\") || filename.includes(".git/") || filename.includes(".git\\")) return;
|
|
40
48
|
|
|
41
49
|
clearTimeout(timeout);
|
|
42
50
|
timeout = setTimeout(() => {
|
|
@@ -47,65 +55,76 @@ export async function runDev(args: string[]) {
|
|
|
47
55
|
console.log(`Watching ${dir} for changes...`);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const basePort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
59
|
+
let port = basePort;
|
|
60
|
+
const maxPort = basePort + 10;
|
|
61
|
+
|
|
62
|
+
const fetchHandler = async (req: any, srv: any) => {
|
|
63
|
+
if (srv.upgrade(req)) return;
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const url = new URL(req.url);
|
|
66
|
+
const decodedPath = decodeURIComponent(url.pathname);
|
|
67
|
+
let filePath = path.resolve(outDir, "." + decodedPath);
|
|
68
|
+
|
|
69
|
+
if (decodedPath.endsWith("/")) {
|
|
70
|
+
filePath = path.join(outDir, "index.html");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!filePath.startsWith(outDir + path.sep) && filePath !== outDir) {
|
|
74
|
+
return new Response("Forbidden", { status: 403 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
78
|
+
if (filePath.endsWith(".html")) {
|
|
79
|
+
const file = Bun.file(filePath);
|
|
80
|
+
let text = await file.text();
|
|
81
|
+
|
|
82
|
+
const script = `<script>
|
|
83
|
+
const ws = new WebSocket(\`ws://\${location.host}/\`);
|
|
84
|
+
ws.onmessage = (e) => { if (e.data === "reload") location.reload(); };
|
|
85
|
+
</script>`;
|
|
86
|
+
|
|
87
|
+
const bodyRegex = /<\/body>/i;
|
|
88
|
+
const match = text.match(bodyRegex);
|
|
89
|
+
if (match && match.index !== undefined) {
|
|
90
|
+
text = text.slice(0, match.index) + script + text.slice(match.index);
|
|
64
91
|
} else {
|
|
65
|
-
|
|
66
|
-
if (chapterFile) {
|
|
67
|
-
filePath = path.join(outDir, chapterFile);
|
|
68
|
-
} else if (htmlFiles.length > 0) {
|
|
69
|
-
filePath = path.join(outDir, htmlFiles[0]);
|
|
70
|
-
} else {
|
|
71
|
-
filePath = path.join(outDir, "index.html");
|
|
72
|
-
}
|
|
92
|
+
text += script;
|
|
73
93
|
}
|
|
94
|
+
return new Response(text, { headers: { "Content-Type": "text/html" } });
|
|
74
95
|
}
|
|
96
|
+
return new Response(Bun.file(filePath));
|
|
97
|
+
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
99
|
+
return new Response("Not found", { status: 404 });
|
|
100
|
+
};
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const lastBodyIndex = text.lastIndexOf("</body>");
|
|
85
|
-
if (lastBodyIndex !== -1) {
|
|
86
|
-
text = text.slice(0, lastBodyIndex) + `<script>
|
|
87
|
-
const ws = new WebSocket("ws://localhost:3000/");
|
|
88
|
-
ws.onmessage = (e) => { if (e.data === "reload") location.reload(); };
|
|
89
|
-
</script></body>` + text.slice(lastBodyIndex + 7);
|
|
90
|
-
}
|
|
91
|
-
return new Response(text, { headers: { "Content-Type": "text/html" } });
|
|
92
|
-
}
|
|
93
|
-
return new Response(Bun.file(filePath));
|
|
94
|
-
}
|
|
102
|
+
const wsHandler = {
|
|
103
|
+
message() {},
|
|
104
|
+
open(ws: any) { ws.subscribe("livereload"); }
|
|
105
|
+
};
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
while (port <= maxPort) {
|
|
108
|
+
try {
|
|
109
|
+
server = Bun.serve({
|
|
110
|
+
port,
|
|
111
|
+
fetch: fetchHandler,
|
|
112
|
+
websocket: wsHandler
|
|
113
|
+
});
|
|
114
|
+
console.log(`Dev server listening on http://localhost:${server.port}`);
|
|
115
|
+
break;
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
if (err.code === 'EADDRINUSE') {
|
|
118
|
+
console.warn(`Port ${port} is in use, trying ${port + 1}...`);
|
|
119
|
+
port++;
|
|
120
|
+
} else {
|
|
121
|
+
throw err;
|
|
100
122
|
}
|
|
101
|
-
|
|
102
|
-
return new Response("Not found", { status: 404 });
|
|
103
|
-
},
|
|
104
|
-
websocket: {
|
|
105
|
-
message() {},
|
|
106
|
-
open(ws: any) { ws.subscribe("livereload"); }
|
|
107
123
|
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!server) {
|
|
127
|
+
console.error(`Could not find an open port between ${basePort} and ${maxPort}.`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
111
130
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
3
4
|
|
|
4
5
|
export async function runInit() {
|
|
5
6
|
console.log("Initializing md project structure...");
|
|
@@ -77,7 +78,8 @@ export const firstLesson = lesson("First Lesson", { contentBase: import.meta.dir
|
|
|
77
78
|
let mrMdVersion = "latest";
|
|
78
79
|
try {
|
|
79
80
|
// Find mr-md's own package.json to get its version
|
|
80
|
-
const
|
|
81
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
82
|
+
const __dirname = path.dirname(__filename);
|
|
81
83
|
const ownPkgPath = path.resolve(__dirname, "../../package.json");
|
|
82
84
|
const ownPkg = JSON.parse(fs.readFileSync(ownPkgPath, "utf-8"));
|
|
83
85
|
mrMdVersion = ownPkg.version;
|
package/src/renderer/utils.ts
CHANGED
|
@@ -46,7 +46,7 @@ function resolveContent(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const baseDir = path.resolve(options.contentBase ?? ".");
|
|
49
|
-
if (!filePath.startsWith(baseDir) && options.strict !== false) {
|
|
49
|
+
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir && options.strict !== false) {
|
|
50
50
|
throw new Error(`Security Error: Path traversal attempt outside contentBase: ${filePath}`);
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ function resolveContent(
|
|
|
54
54
|
const stat = fs.statSync(filePath);
|
|
55
55
|
if (stat.isFile()) {
|
|
56
56
|
if (expectedType === "js" && (filePath.endsWith(".js") || filePath.endsWith(".ts") || filePath.endsWith(".jsx") || filePath.endsWith(".tsx"))) {
|
|
57
|
-
const out = spawnSync(
|
|
57
|
+
const out = spawnSync(process.execPath, ["build", "--target=browser", filePath]);
|
|
58
58
|
if (out.status === 0) {
|
|
59
59
|
return out.stdout.toString("utf-8");
|
|
60
60
|
} else {
|
|
@@ -121,7 +121,9 @@ function resolveAssetSrc(src: string, options: BuildOptions): string {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Create a safe filename with hash to avoid collisions
|
|
124
|
-
|
|
124
|
+
// Use relative path for hashing to ensure deterministic builds across different machines
|
|
125
|
+
const relPathForHash = path.relative(options.contentBase ?? process.cwd(), filePath);
|
|
126
|
+
const hash = crypto.createHash("md5").update(relPathForHash).digest("hex").substring(0, 8);
|
|
125
127
|
const ext = path.extname(filePath);
|
|
126
128
|
const filename = `${path.basename(filePath, ext)}-${hash}${ext}`;
|
|
127
129
|
const outPath = path.join(assetsDir, filename);
|