@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.
@@ -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
- // GLOBAL SOURCE & DEST ROOTS
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
- 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
- }
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
- function validateRoots() {
40
- if (!SRC || !DEST) return;
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
41
16
 
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
- }
17
+ let headerPrinted = false;
18
+ export function printHeader(importMeta) {
19
+ if (headerPrinted) return;
20
+ headerPrinted = true;
49
21
 
50
- // --------------------------------------------------
51
- // PATH SAFETY HELPERS
52
- // --------------------------------------------------
22
+ const version = pkg.version;
23
+ const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
53
24
 
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
- }
25
+ const callerFile = fileURLToPath(importMeta.url);
26
+ const callerDir = dirname(callerFile);
62
27
 
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);
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
- if (!DEST) throw new Error("SECURITY: Dest root not set");
44
+ let DEST = getDest();
45
+
183
46
  console.log(`deepCopy: ${from} → ${to}`);
184
- // from is raw filesystem path, resolve it normally
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
- const srcEntry = path.join(src, entry);
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
- // rm_dest("dir") — remove a file or directory inside DEST
110
+ // ensureWorkFile(x)
230
111
  // --------------------------------------------------
112
+ function ensureWorkFile(x, fieldName = "value") {
113
+ if (x instanceof WorkFile) return x;
231
114
 
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()");
115
+ if (typeof x === "string") {
116
+ return literal(x);
236
117
  }
237
118
 
238
- const abs = path.resolve(DEST, relPath);
119
+ throw new Error(`${fieldName} must be WorkFile or string`);
120
+ }
239
121
 
240
- if (!abs.startsWith(DEST)) {
241
- throw new Error(`SECURITY: rmDest target escapes dest:
242
- dest: ${DEST}
243
- target: ${abs}`);
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
- if (!fs.existsSync(abs)) {
247
- console.log(`rmDest: nothing to remove: ${abs}`);
248
- return;
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
- fs.rmSync(abs, { recursive: true, force: true });
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 (!DEST) return;
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
@@ -0,0 +1,2 @@
1
+ export * from "./buldng-lib.js";
2
+
@@ -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.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
- "main": "dist/buldng-lib.js",
5
+ "main": "dist/init.js",
6
6
  "files": ["dist"],
7
7
  "scripts": {
8
- "build": "cp src/buldng-lib.js dist/buldng-lib.js"
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
  }