@pajh/buldng 0.0.1 → 0.0.2
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 +118 -185
- package/dist/buldng-validate.js +102 -0
- package/dist/init.js +2 -0
- package/dist/workfile.js +162 -0
- package/package.json +13 -4
package/dist/buldng-lib.js
CHANGED
|
@@ -1,209 +1,68 @@
|
|
|
1
1
|
// tools/build-lib.js
|
|
2
|
-
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import esbuild from "esbuild";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let SRC = null;
|
|
12
|
-
let DEST = null;
|
|
13
|
-
|
|
14
|
-
// --------------------------------------------------
|
|
15
|
-
// SETTERS WITH SAFETY CHECKS
|
|
16
|
-
// --------------------------------------------------
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
}
|
|
10
|
+
import { WorkFile } from "./workfile.js";
|
|
11
|
+
import { getDest, assertDestWrite, assertSourceRead, getInputs } from "./buldng-validate.js";
|
|
12
|
+
export { assertSourceRead, assertDestWrite, setSource, setDest } from "./buldng-validate.js";
|
|
38
13
|
|
|
39
|
-
|
|
40
|
-
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
|
|
41
16
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
dest: ${DEST}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
17
|
+
let headerPrinted = false;
|
|
18
|
+
export function printHeader(importMeta) {
|
|
19
|
+
if (headerPrinted) return;
|
|
20
|
+
headerPrinted = true;
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// --------------------------------------------------
|
|
22
|
+
const version = pkg.version;
|
|
23
|
+
const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
|
|
53
24
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
}
|
|
25
|
+
const callerFile = fileURLToPath(importMeta.url);
|
|
26
|
+
const callerDir = dirname(callerFile);
|
|
62
27
|
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
28
|
+
console.log(`Xx buldng build system v${version} — ${timestamp}`);
|
|
29
|
+
console.log(`${callerDir}/${callerFile.split("/").pop()}`);
|
|
170
30
|
}
|
|
171
31
|
|
|
172
32
|
// --------------------------------------------------
|
|
173
33
|
// mkdir("puzzle")
|
|
174
34
|
// --------------------------------------------------
|
|
175
|
-
|
|
176
35
|
export function mkdir(relDir) {
|
|
177
36
|
const abs = assertDestWrite(relDir);
|
|
178
37
|
fs.mkdirSync(abs, { recursive: true });
|
|
179
38
|
}
|
|
180
39
|
|
|
40
|
+
// --------------------------------------------------
|
|
41
|
+
// deepCopy — raw filesystem path
|
|
42
|
+
// --------------------------------------------------
|
|
181
43
|
export function deepCopy(from, to) {
|
|
182
|
-
|
|
44
|
+
let DEST = getDest();
|
|
45
|
+
|
|
183
46
|
console.log(`deepCopy: ${from} → ${to}`);
|
|
184
|
-
|
|
47
|
+
|
|
185
48
|
const absFrom = path.resolve(from);
|
|
186
49
|
|
|
187
|
-
// to is relative to DEST
|
|
188
50
|
if (to.includes("..")) {
|
|
189
51
|
throw new Error("SECURITY: Illegal path traversal in deepCopy()");
|
|
190
52
|
}
|
|
191
53
|
|
|
192
54
|
const absTo = to === "/" ? DEST : path.resolve(DEST, to);
|
|
193
55
|
|
|
194
|
-
// Ensure target is inside DEST
|
|
195
56
|
if (!absTo.startsWith(DEST)) {
|
|
196
57
|
throw new Error(`SECURITY: deepCopy target escapes dest:
|
|
197
58
|
dest: ${DEST}
|
|
198
59
|
target: ${absTo}`);
|
|
199
60
|
}
|
|
200
61
|
|
|
201
|
-
// Ensure source exists
|
|
202
62
|
if (!fs.existsSync(absFrom)) {
|
|
203
63
|
throw new Error(`deepCopy: source does not exist: ${absFrom}`);
|
|
204
64
|
}
|
|
205
65
|
|
|
206
|
-
// Recursively copy
|
|
207
66
|
function copyRecursive(src, dst) {
|
|
208
67
|
const stat = fs.statSync(src);
|
|
209
68
|
|
|
@@ -211,12 +70,9 @@ export function deepCopy(from, to) {
|
|
|
211
70
|
fs.mkdirSync(dst, { recursive: true });
|
|
212
71
|
const entries = fs.readdirSync(src);
|
|
213
72
|
for (const entry of entries) {
|
|
214
|
-
|
|
215
|
-
const dstEntry = path.join(dst, entry);
|
|
216
|
-
copyRecursive(srcEntry, dstEntry);
|
|
73
|
+
copyRecursive(path.join(src, entry), path.join(dst, entry));
|
|
217
74
|
}
|
|
218
75
|
} else {
|
|
219
|
-
// File
|
|
220
76
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
221
77
|
fs.copyFileSync(src, dst);
|
|
222
78
|
}
|
|
@@ -225,36 +81,113 @@ export function deepCopy(from, to) {
|
|
|
225
81
|
copyRecursive(absFrom, absTo);
|
|
226
82
|
}
|
|
227
83
|
|
|
84
|
+
export function header() {
|
|
85
|
+
printHeader(import.meta);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --------------------------------------------------
|
|
89
|
+
// file`path.ext` — tagged template literal
|
|
90
|
+
// --------------------------------------------------
|
|
91
|
+
export function file(strings, ...values) {
|
|
92
|
+
if (values.length > 0) {
|
|
93
|
+
throw new Error("file`` does not support interpolation");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const relPath = strings.join("");
|
|
97
|
+
const absPath = assertSourceRead(relPath);
|
|
98
|
+
|
|
99
|
+
return new WorkFile("file", { relPath, absPath }, []);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --------------------------------------------------
|
|
103
|
+
// literal(text)
|
|
104
|
+
// --------------------------------------------------
|
|
105
|
+
export function literal(value) {
|
|
106
|
+
return new WorkFile("literal", { value }, []);
|
|
107
|
+
}
|
|
108
|
+
|
|
228
109
|
// --------------------------------------------------
|
|
229
|
-
//
|
|
110
|
+
// ensureWorkFile(x)
|
|
230
111
|
// --------------------------------------------------
|
|
112
|
+
function ensureWorkFile(x, fieldName = "value") {
|
|
113
|
+
if (x instanceof WorkFile) return x;
|
|
231
114
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (relPath.includes("..")) {
|
|
235
|
-
throw new Error("SECURITY: Illegal path traversal in rmDest()");
|
|
115
|
+
if (typeof x === "string") {
|
|
116
|
+
return literal(x);
|
|
236
117
|
}
|
|
237
118
|
|
|
238
|
-
|
|
119
|
+
throw new Error(`${fieldName} must be WorkFile or string`);
|
|
120
|
+
}
|
|
239
121
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
122
|
+
// --------------------------------------------------
|
|
123
|
+
// csslink(filename)
|
|
124
|
+
// --------------------------------------------------
|
|
125
|
+
export function csslink(filename) {
|
|
126
|
+
return new WorkFile(
|
|
127
|
+
"literal",
|
|
128
|
+
{ value: `<link rel="stylesheet" href="/${filename}">` },
|
|
129
|
+
[]
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --------------------------------------------------
|
|
134
|
+
// scriptlink(filename)
|
|
135
|
+
// --------------------------------------------------
|
|
136
|
+
export function scriptlink(filename) {
|
|
137
|
+
return new WorkFile(
|
|
138
|
+
"literal",
|
|
139
|
+
{ value: `<script type="module" src="/${filename}"></script>` },
|
|
140
|
+
[]
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --------------------------------------------------
|
|
145
|
+
// append(...items)
|
|
146
|
+
// --------------------------------------------------
|
|
147
|
+
export function append(...items) {
|
|
148
|
+
const children = items.map(item => {
|
|
149
|
+
if (item instanceof WorkFile) return item;
|
|
150
|
+
if (typeof item === "string") return literal(item);
|
|
151
|
+
throw new Error("append(): each item must be WorkFile or string");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (children.length < 2) {
|
|
155
|
+
throw new Error("append(): requires at least 2 items");
|
|
244
156
|
}
|
|
245
157
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
158
|
+
return new WorkFile("append", {}, children);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --------------------------------------------------
|
|
162
|
+
// replace(source, tag, replacement)
|
|
163
|
+
// --------------------------------------------------
|
|
164
|
+
export function replace(source, tag, replacement) {
|
|
165
|
+
const srcWF = ensureWorkFile(source, "source");
|
|
166
|
+
const tagWF = ensureWorkFile(tag, "tag");
|
|
167
|
+
const repWF = ensureWorkFile(replacement, "replacement");
|
|
168
|
+
|
|
169
|
+
if (tagWF.op === "literal" && tagWF.config.value.trim() === "") {
|
|
170
|
+
throw new Error(`replace(): tag cannot be blank`);
|
|
249
171
|
}
|
|
250
172
|
|
|
251
|
-
|
|
252
|
-
console.log(`rmDest: removed ${abs}`);
|
|
173
|
+
return new WorkFile("replace", {}, [srcWF, tagWF, repWF]);
|
|
253
174
|
}
|
|
254
175
|
|
|
176
|
+
// --------------------------------------------------
|
|
177
|
+
// compile(source)
|
|
178
|
+
// --------------------------------------------------
|
|
179
|
+
export function compile(source, flags = {}) {
|
|
180
|
+
const srcWF = ensureWorkFile(source, "source");
|
|
181
|
+
return new WorkFile("compile", { flags }, [srcWF]);
|
|
182
|
+
}
|
|
255
183
|
|
|
184
|
+
// --------------------------------------------------
|
|
185
|
+
// build-inputs.json manifest
|
|
186
|
+
// --------------------------------------------------
|
|
256
187
|
process.on("exit", () => {
|
|
257
|
-
if (
|
|
188
|
+
if (process.env.BULDNG_HOT !== "1") return;
|
|
189
|
+
let DEST = getDest();
|
|
190
|
+
let INPUTS = getInputs();
|
|
258
191
|
const manifest = Array.from(INPUTS);
|
|
259
192
|
const out = path.join(DEST, "build-inputs.json");
|
|
260
193
|
fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// --------------------------------------------------
|
|
5
|
+
// GLOBAL SOURCE & DEST ROOTS
|
|
6
|
+
// --------------------------------------------------
|
|
7
|
+
|
|
8
|
+
let SRC = null;
|
|
9
|
+
let DEST = null;
|
|
10
|
+
|
|
11
|
+
// --------------------------------------------------
|
|
12
|
+
// SETTERS WITH SAFETY CHECKS
|
|
13
|
+
// --------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export function setSource(dir) {
|
|
16
|
+
const abs = path.resolve(dir);
|
|
17
|
+
const sentinel = path.join(abs, "BULDNG_SRC");
|
|
18
|
+
if (!fs.existsSync(sentinel)) {
|
|
19
|
+
throw new Error(`SECURITY: Source directory "${abs}" does not contain BLDNG_SRC sentinel`);
|
|
20
|
+
}
|
|
21
|
+
SRC = abs;
|
|
22
|
+
validateRoots();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setDest(dir) {
|
|
26
|
+
const abs = path.resolve(dir);
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(abs)) {
|
|
29
|
+
const sentinel = path.join(abs, "BULDNG_SRC");
|
|
30
|
+
if (fs.existsSync(sentinel)) {
|
|
31
|
+
throw new Error(`SECURITY: Dest directory "${abs}" contains BLDNG_SRC sentinel`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
fs.rmSync(path.join(abs, entry.name), { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DEST = abs;
|
|
43
|
+
validateRoots();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDest() {
|
|
47
|
+
if (!DEST) throw new Error("SECURITY: Dest root not set");
|
|
48
|
+
return DEST;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --------------------------------------------------
|
|
52
|
+
// PATH SAFETY HELPERS
|
|
53
|
+
// --------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export function assertSourceRead(relPath) {
|
|
56
|
+
if (!SRC) throw new Error("SECURITY: Source root not set");
|
|
57
|
+
if (relPath.includes("..")) throw new Error("SECURITY: Illegal path traversal in read()");
|
|
58
|
+
const abs = path.resolve(SRC, relPath);
|
|
59
|
+
if (!abs.startsWith(SRC)) throw new Error("SECURITY: Read outside src/");
|
|
60
|
+
trackInput(abs);
|
|
61
|
+
return abs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assertDestWrite(relPath) {
|
|
65
|
+
if (!DEST) throw new Error("SECURITY: Dest root not set");
|
|
66
|
+
if (relPath.includes("..")) throw new Error("SECURITY: Illegal path traversal in save()");
|
|
67
|
+
const abs = path.resolve(DEST, relPath);
|
|
68
|
+
if (!abs.startsWith(DEST)) throw new Error("SECURITY: Write outside dest/");
|
|
69
|
+
return abs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateRoots() {
|
|
73
|
+
if (!SRC || !DEST) return;
|
|
74
|
+
|
|
75
|
+
const srcReal = fs.realpathSync(SRC);
|
|
76
|
+
const destReal = fs.realpathSync(DEST);
|
|
77
|
+
|
|
78
|
+
if (srcReal === destReal) {
|
|
79
|
+
throw new Error(`SECURITY: src and dest resolve to the same directory:
|
|
80
|
+
src: ${SRC}
|
|
81
|
+
dest: ${DEST}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (srcReal.startsWith(destReal) || destReal.startsWith(srcReal)) {
|
|
85
|
+
throw new Error(`SECURITY: src and dest cannot be nested:
|
|
86
|
+
src: ${SRC}
|
|
87
|
+
dest: ${DEST}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const INPUTS = new Set();
|
|
92
|
+
|
|
93
|
+
export function getInputs() {
|
|
94
|
+
return INPUTS;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function trackInput(absPath) {
|
|
98
|
+
if (!SRC || !DEST) return;
|
|
99
|
+
if (absPath.startsWith(SRC) && !absPath.startsWith(DEST)) {
|
|
100
|
+
INPUTS.add(absPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/init.js
ADDED
package/dist/workfile.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// --------------------------------------------------
|
|
2
|
+
// WorkFile — immutable, lazy, tree-structured build node
|
|
3
|
+
// --------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import esbuild from "esbuild";
|
|
8
|
+
import { assertDestWrite } from "./buldng-lib.js";
|
|
9
|
+
|
|
10
|
+
export class WorkFile {
|
|
11
|
+
constructor(op, config = {}, children = []) {
|
|
12
|
+
this.op = op; // "file", "literal", "append", "replace", "compile"
|
|
13
|
+
this.config = Object.freeze(config);
|
|
14
|
+
this.children = Object.freeze(children);
|
|
15
|
+
|
|
16
|
+
Object.freeze(this); // full immutability
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --------------------------------------------------
|
|
20
|
+
// execute(): recursively produce output string
|
|
21
|
+
// --------------------------------------------------
|
|
22
|
+
execute() {
|
|
23
|
+
switch (this.op) {
|
|
24
|
+
|
|
25
|
+
// ----------------------------------------------
|
|
26
|
+
// FILE — lazy read from disk
|
|
27
|
+
// ----------------------------------------------
|
|
28
|
+
case "file": {
|
|
29
|
+
const abs = this.config.absPath;
|
|
30
|
+
return fs.readFileSync(abs, "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ----------------------------------------------
|
|
34
|
+
// LITERAL — inline text
|
|
35
|
+
// ----------------------------------------------
|
|
36
|
+
case "literal": {
|
|
37
|
+
return this.config.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ----------------------------------------------
|
|
41
|
+
// APPEND — concat children in order
|
|
42
|
+
// ----------------------------------------------
|
|
43
|
+
case "append": {
|
|
44
|
+
let out = "";
|
|
45
|
+
|
|
46
|
+
for (const child of this.children) {
|
|
47
|
+
const chunk = child.execute();
|
|
48
|
+
|
|
49
|
+
if (out.length > 0 &&
|
|
50
|
+
!out.endsWith("\n") &&
|
|
51
|
+
!chunk.startsWith("\n")) {
|
|
52
|
+
out += "\n";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
out += chunk;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ----------------------------------------------
|
|
62
|
+
// REPLACE — replace tag with replacement
|
|
63
|
+
// ----------------------------------------------
|
|
64
|
+
case "replace": {
|
|
65
|
+
if (this.children.length !== 3) {
|
|
66
|
+
throw new Error(`replace(): expected 3 children, got ${this.children.length}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const [srcWF, tagWF, repWF] = this.children;
|
|
70
|
+
|
|
71
|
+
const src = srcWF.execute();
|
|
72
|
+
const tag = tagWF.execute();
|
|
73
|
+
const rep = repWF.execute();
|
|
74
|
+
|
|
75
|
+
if (!src.includes(tag)) {
|
|
76
|
+
throw new Error(`replace(): tag "${tag}" not found in source`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return src.replace(tag, rep);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ----------------------------------------------
|
|
83
|
+
// COMPILE — esbuild wrapper
|
|
84
|
+
// ----------------------------------------------
|
|
85
|
+
case "compile": {
|
|
86
|
+
if (this.children.length !== 1) {
|
|
87
|
+
throw new Error(`compile(): expected 1 child, got ${this.children.length}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const child = this.children[0];
|
|
91
|
+
|
|
92
|
+
// If child is a file WorkFile, use its absolute path directly
|
|
93
|
+
if (child.op === "file" && child.config.absPath) {
|
|
94
|
+
const entry = child.config.absPath;
|
|
95
|
+
|
|
96
|
+
const result = esbuild.buildSync({
|
|
97
|
+
entryPoints: [entry],
|
|
98
|
+
bundle: true,
|
|
99
|
+
minify: true,
|
|
100
|
+
sourcemap: true,
|
|
101
|
+
format: "esm",
|
|
102
|
+
target: "esnext",
|
|
103
|
+
write: false
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (result.errors?.length) {
|
|
107
|
+
throw new Error("Build failed:\n" + result.errors.map(e => e.text).join("\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result.outputFiles[0].text;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Otherwise compile from stdin
|
|
114
|
+
const srcText = child.execute();
|
|
115
|
+
|
|
116
|
+
const result = esbuild.buildSync({
|
|
117
|
+
stdin: {
|
|
118
|
+
contents: srcText,
|
|
119
|
+
resolveDir: process.cwd(),
|
|
120
|
+
sourcefile: "input.ts"
|
|
121
|
+
},
|
|
122
|
+
bundle: true,
|
|
123
|
+
minify: true,
|
|
124
|
+
sourcemap: true,
|
|
125
|
+
format: "esm",
|
|
126
|
+
target: "esnext",
|
|
127
|
+
write: false
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (result.errors?.length) {
|
|
131
|
+
throw new Error("Build failed:\n" + result.errors.map(e => e.text).join("\n"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result.outputFiles[0].text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ----------------------------------------------
|
|
138
|
+
// Unknown op
|
|
139
|
+
// ----------------------------------------------
|
|
140
|
+
default:
|
|
141
|
+
throw new Error(`WorkFile.execute(): unknown op "${this.op}"`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------
|
|
146
|
+
// save(): write output to disk
|
|
147
|
+
// --------------------------------------------------
|
|
148
|
+
save(target) {
|
|
149
|
+
if (!target) {
|
|
150
|
+
throw new Error("WorkFile.save(): target filename required");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const abs = assertDestWrite(target);
|
|
154
|
+
const out = this.execute();
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
157
|
+
fs.writeFileSync(abs, out, "utf8");
|
|
158
|
+
|
|
159
|
+
console.log(`Saved: ${abs}`);
|
|
160
|
+
return abs;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pajh/buldng",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "dist/
|
|
5
|
+
"main": "dist/init.js",
|
|
6
6
|
"files": ["dist"],
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "
|
|
9
|
-
}
|
|
8
|
+
"build": "node build.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"esbuild": "^0.28.1",
|
|
12
|
+
"typescript": "^7.0.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"stylelint": "^17.14.1",
|
|
16
|
+
"stylelint-config-standard": "^40.0.0"
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
}
|