@pajh/buldng 0.0.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/dist/buldng-lib.js +261 -0
- package/package.json +10 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// tools/build-lib.js
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import esbuild from "esbuild";
|
|
6
|
+
|
|
7
|
+
// --------------------------------------------------
|
|
8
|
+
// GLOBAL SOURCE & DEST ROOTS
|
|
9
|
+
// --------------------------------------------------
|
|
10
|
+
|
|
11
|
+
let SRC = null;
|
|
12
|
+
let DEST = null;
|
|
13
|
+
|
|
14
|
+
// --------------------------------------------------
|
|
15
|
+
// SETTERS WITH SAFETY CHECKS
|
|
16
|
+
// --------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function setSource(dir) {
|
|
19
|
+
const abs = path.resolve(dir);
|
|
20
|
+
SRC = abs;
|
|
21
|
+
validateRoots();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setDest(dir) {
|
|
25
|
+
const abs = path.resolve(dir);
|
|
26
|
+
DEST = abs;
|
|
27
|
+
validateRoots();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const INPUTS = new Set();
|
|
31
|
+
|
|
32
|
+
function trackInput(absPath) {
|
|
33
|
+
if (!SRC || !DEST) return;
|
|
34
|
+
if (absPath.startsWith(SRC) && !absPath.startsWith(DEST)) {
|
|
35
|
+
INPUTS.add(absPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateRoots() {
|
|
40
|
+
if (!SRC || !DEST) return;
|
|
41
|
+
|
|
42
|
+
// src cannot be inside dest, dest cannot be inside src
|
|
43
|
+
if (SRC.startsWith(DEST) || DEST.startsWith(SRC)) {
|
|
44
|
+
throw new Error(`SECURITY: src and dest cannot be nested:
|
|
45
|
+
src: ${SRC}
|
|
46
|
+
dest: ${DEST}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --------------------------------------------------
|
|
51
|
+
// PATH SAFETY HELPERS
|
|
52
|
+
// --------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function assertSourceRead(relPath) {
|
|
55
|
+
if (!SRC) throw new Error("SECURITY: Source root not set");
|
|
56
|
+
if (relPath.includes("..")) throw new Error("SECURITY: Illegal path traversal in read()");
|
|
57
|
+
const abs = path.resolve(SRC, relPath);
|
|
58
|
+
if (!abs.startsWith(SRC)) throw new Error("SECURITY: Read outside src/");
|
|
59
|
+
trackInput(abs); // ← NEW
|
|
60
|
+
return abs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertDestWrite(relPath) {
|
|
64
|
+
if (!DEST) throw new Error("SECURITY: Dest root not set");
|
|
65
|
+
if (relPath.includes("..")) throw new Error("SECURITY: Illegal path traversal in save()");
|
|
66
|
+
const abs = path.resolve(DEST, relPath);
|
|
67
|
+
if (!abs.startsWith(DEST)) throw new Error("SECURITY: Write outside dest/");
|
|
68
|
+
return abs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --------------------------------------------------
|
|
72
|
+
// File object — fluent API
|
|
73
|
+
// --------------------------------------------------
|
|
74
|
+
|
|
75
|
+
class File {
|
|
76
|
+
constructor(content) {
|
|
77
|
+
this.content = content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fluent append: read another file from src and append its contents
|
|
81
|
+
append(relPath) {
|
|
82
|
+
const abs = assertSourceRead(relPath);
|
|
83
|
+
const extra = fs.readFileSync(abs, "utf8");
|
|
84
|
+
return new File(this.content + "\n" + extra);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Save relative to dest
|
|
88
|
+
save(relTarget) {
|
|
89
|
+
const out = assertDestWrite(relTarget);
|
|
90
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
91
|
+
fs.writeFileSync(out, this.content, "utf8");
|
|
92
|
+
console.log(`Saved: ${out}`);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --------------------------------------------------
|
|
98
|
+
// read("file") → File
|
|
99
|
+
// --------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export function read(relPath) {
|
|
102
|
+
const abs = assertSourceRead(relPath);
|
|
103
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
104
|
+
return new File(content);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --------------------------------------------------
|
|
108
|
+
// append(["a.css","b.css"]) → File
|
|
109
|
+
// --------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export function append(relPaths) {
|
|
112
|
+
const parts = relPaths.map(p => {
|
|
113
|
+
const abs = assertSourceRead(p);
|
|
114
|
+
return fs.readFileSync(abs, "utf8");
|
|
115
|
+
});
|
|
116
|
+
return new File(parts.join("\n"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --------------------------------------------------
|
|
120
|
+
// stitch({ shell, app, css, js }) → File
|
|
121
|
+
// --------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export function stitch({ shell, app, css = [], js = [] }) {
|
|
124
|
+
const shellAbs = assertSourceRead(shell);
|
|
125
|
+
const appAbs = assertSourceRead(app);
|
|
126
|
+
|
|
127
|
+
let html = fs.readFileSync(shellAbs, "utf8");
|
|
128
|
+
const appHtml = fs.readFileSync(appAbs, "utf8");
|
|
129
|
+
|
|
130
|
+
html = html.replace("<!--APP-->", appHtml);
|
|
131
|
+
|
|
132
|
+
const cssLinks = css
|
|
133
|
+
.map(c => `<link rel="stylesheet" href="/${c}">`)
|
|
134
|
+
.join("\n");
|
|
135
|
+
html = html.replace("<!--CSS-->", cssLinks);
|
|
136
|
+
|
|
137
|
+
const jsScripts = js
|
|
138
|
+
.map(j => `<script type="module" src="/${j}"></script>`)
|
|
139
|
+
.join("\n");
|
|
140
|
+
html = html.replace("<!--SCRIPTS-->", jsScripts);
|
|
141
|
+
|
|
142
|
+
return new File(html);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------
|
|
146
|
+
// compile("pages/puzzle.ts") → File
|
|
147
|
+
// --------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export function compile(relTsPath) {
|
|
150
|
+
const abs = assertSourceRead(relTsPath);
|
|
151
|
+
|
|
152
|
+
const result = esbuild.buildSync({
|
|
153
|
+
entryPoints: [abs],
|
|
154
|
+
bundle: true,
|
|
155
|
+
minify: true,
|
|
156
|
+
sourcemap: true,
|
|
157
|
+
format: "esm",
|
|
158
|
+
target: "esnext",
|
|
159
|
+
write: false
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (result.errors.length > 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Build failed:\n" +
|
|
165
|
+
result.errors.map(e => e.text).join("\n")
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return new File(result.outputFiles[0].text);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --------------------------------------------------
|
|
173
|
+
// mkdir("puzzle")
|
|
174
|
+
// --------------------------------------------------
|
|
175
|
+
|
|
176
|
+
export function mkdir(relDir) {
|
|
177
|
+
const abs = assertDestWrite(relDir);
|
|
178
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function deepCopy(from, to) {
|
|
182
|
+
if (!DEST) throw new Error("SECURITY: Dest root not set");
|
|
183
|
+
console.log(`deepCopy: ${from} → ${to}`);
|
|
184
|
+
// from is raw filesystem path, resolve it normally
|
|
185
|
+
const absFrom = path.resolve(from);
|
|
186
|
+
|
|
187
|
+
// to is relative to DEST
|
|
188
|
+
if (to.includes("..")) {
|
|
189
|
+
throw new Error("SECURITY: Illegal path traversal in deepCopy()");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const absTo = to === "/" ? DEST : path.resolve(DEST, to);
|
|
193
|
+
|
|
194
|
+
// Ensure target is inside DEST
|
|
195
|
+
if (!absTo.startsWith(DEST)) {
|
|
196
|
+
throw new Error(`SECURITY: deepCopy target escapes dest:
|
|
197
|
+
dest: ${DEST}
|
|
198
|
+
target: ${absTo}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Ensure source exists
|
|
202
|
+
if (!fs.existsSync(absFrom)) {
|
|
203
|
+
throw new Error(`deepCopy: source does not exist: ${absFrom}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Recursively copy
|
|
207
|
+
function copyRecursive(src, dst) {
|
|
208
|
+
const stat = fs.statSync(src);
|
|
209
|
+
|
|
210
|
+
if (stat.isDirectory()) {
|
|
211
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
212
|
+
const entries = fs.readdirSync(src);
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const srcEntry = path.join(src, entry);
|
|
215
|
+
const dstEntry = path.join(dst, entry);
|
|
216
|
+
copyRecursive(srcEntry, dstEntry);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// File
|
|
220
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
221
|
+
fs.copyFileSync(src, dst);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
copyRecursive(absFrom, absTo);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --------------------------------------------------
|
|
229
|
+
// rm_dest("dir") — remove a file or directory inside DEST
|
|
230
|
+
// --------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export function rmDest(relPath) {
|
|
233
|
+
if (!DEST) throw new Error("SECURITY: Dest root not set");
|
|
234
|
+
if (relPath.includes("..")) {
|
|
235
|
+
throw new Error("SECURITY: Illegal path traversal in rmDest()");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const abs = path.resolve(DEST, relPath);
|
|
239
|
+
|
|
240
|
+
if (!abs.startsWith(DEST)) {
|
|
241
|
+
throw new Error(`SECURITY: rmDest target escapes dest:
|
|
242
|
+
dest: ${DEST}
|
|
243
|
+
target: ${abs}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(abs)) {
|
|
247
|
+
console.log(`rmDest: nothing to remove: ${abs}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
252
|
+
console.log(`rmDest: removed ${abs}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
process.on("exit", () => {
|
|
257
|
+
if (!DEST) return;
|
|
258
|
+
const manifest = Array.from(INPUTS);
|
|
259
|
+
const out = path.join(DEST, "build-inputs.json");
|
|
260
|
+
fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
|
|
261
|
+
});
|