@pajh/buldng 0.0.1 → 0.0.3

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,261 +1,408 @@
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
- // --------------------------------------------------
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
6
 
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);
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+ import { readFileSync } from "node:fs";
10
+
11
+ import { WorkFile } from "./workfile.js";
12
+ import { getDest, setSource, setDest, assertDestWrite, assertSourceRead, getInputs, looksLikeFilename, looksLikeHTML,
13
+ looksLikeCSS, looksLikeJS, ensureLeadingSlash, ensureWorkFile } from "./buldng-validate.js";
14
+ import { file, literal, wrap, append, replace, compile, materialise}
15
+ from "./buldng-ops.js";
16
+ export { file, literal, wrap, append, replace, compile, materialise }
17
+ from "./buldng-ops.js";
18
+ export { assertSourceRead, assertDestWrite, setSource, setDest, looksLikeFilename,
19
+ looksLikeHTML, looksLikeCSS, looksLikeJS, ensureWorkFile, ensureLeadingSlash } from "./buldng-validate.js";
20
+
21
+ export default {
22
+ setSource,
23
+ setDest,
24
+ mkdir,
25
+ deepCopy,
26
+ file,
27
+ literal,
28
+ wrap,
29
+ append,
30
+ replace,
31
+ materialise,
32
+ compile,
33
+ csslink,
34
+ scriptlink,
35
+ pageHTML,
36
+ buldngHTMLWrapper,
37
+ reg,
38
+ printHeader
39
+ };
40
+
41
+ const here = dirname(fileURLToPath(import.meta.url));
42
+ const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
43
+ const registry = new Map();
44
+
45
+ export function reg(key, value) {
46
+ if (typeof key !== "string") {
47
+ throw new Error(`reg(): key must be a string`);
36
48
  }
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}`);
49
+
50
+ const wf = ensureWorkFile(value, "reg");
51
+
52
+ if (registry.has(key)) {
53
+ console.log(`reg(): overwriting existing key '${key}'`);
47
54
  }
55
+
56
+ registry.set(key, wf);
57
+ return wf;
48
58
  }
49
59
 
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;
60
+ let headerPrinted = false;
61
+ export function printHeader(importMeta) {
62
+ if (headerPrinted) return;
63
+ headerPrinted = true;
64
+
65
+ const version = pkg.version;
66
+ const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
67
+
68
+ const callerFile = fileURLToPath(importMeta.url);
69
+ const callerDir = dirname(callerFile);
70
+
71
+ console.log(`*local* buldng build system v${version} — ${timestamp}`);
72
+ console.log(`${callerDir}/${callerFile.split("/").pop()}`);
61
73
  }
62
74
 
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);
75
+ export function csslink(input) {
76
+ const pre = `<link rel="stylesheet" href="`;
77
+ const post = `">`;
78
+
79
+ // --------------------------------------------------
80
+ // Case 1: string
81
+ // --------------------------------------------------
82
+ if (typeof input === "string") {
83
+ if (!input.endsWith(".css")) {
84
+ throw new Error(`csslink(): expected a .css filename, got '${input}'`);
85
+ }
86
+
87
+ return new WorkFile(
88
+ "literal",
89
+ { value: pre + ensureLeadingSlash(input) + post },
90
+ [],
91
+ "csslink"
92
+ );
85
93
  }
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
+ // Case 2: WorkFile
97
+ // --------------------------------------------------
98
+ if (input instanceof WorkFile) {
99
+ return new WorkFile(
100
+ "wrap",
101
+ { pre, post },
102
+ [input],
103
+ "csslink"
104
+ );
94
105
  }
106
+
107
+ throw new Error(`csslink(): expected string or WorkFile, got ${typeof input}`);
95
108
  }
96
109
 
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")
110
+ export function scriptlink(input) {
111
+ const pre = `<script type="module" src="/`;
112
+ const post = `"></script>`;
113
+
114
+ // --------------------------------------------------
115
+ // Case 1: string
116
+ // --------------------------------------------------
117
+ if (typeof input === "string") {
118
+ if (!input.endsWith(".js")) {
119
+ throw new Error(`scriptlink(): expected a .js filename, got '${input}'`);
120
+ }
121
+
122
+ return new WorkFile(
123
+ "literal",
124
+ { value: pre + input + post },
125
+ [],
126
+ "scriptlink"
166
127
  );
167
128
  }
168
-
169
- return new File(result.outputFiles[0].text);
129
+
130
+ // --------------------------------------------------
131
+ // Case 2: WorkFile
132
+ // --------------------------------------------------
133
+ if (input instanceof WorkFile) {
134
+ return new WorkFile(
135
+ "wrap",
136
+ { pre, post },
137
+ [input],
138
+ "scriptlink"
139
+ );
140
+ }
141
+
142
+ throw new Error(`scriptlink(): expected string or WorkFile, got ${typeof input}`);
170
143
  }
171
144
 
145
+
172
146
  // --------------------------------------------------
173
147
  // mkdir("puzzle")
174
148
  // --------------------------------------------------
175
-
176
149
  export function mkdir(relDir) {
177
150
  const abs = assertDestWrite(relDir);
178
151
  fs.mkdirSync(abs, { recursive: true });
179
152
  }
180
153
 
154
+ // --------------------------------------------------
155
+ // deepCopy — raw filesystem path
156
+ // --------------------------------------------------
181
157
  export function deepCopy(from, to) {
182
- if (!DEST) throw new Error("SECURITY: Dest root not set");
158
+ let DEST = getDest();
159
+
183
160
  console.log(`deepCopy: ${from} → ${to}`);
184
- // from is raw filesystem path, resolve it normally
161
+
185
162
  const absFrom = path.resolve(from);
186
-
187
- // to is relative to DEST
163
+
188
164
  if (to.includes("..")) {
189
165
  throw new Error("SECURITY: Illegal path traversal in deepCopy()");
190
166
  }
191
-
167
+
192
168
  const absTo = to === "/" ? DEST : path.resolve(DEST, to);
193
-
194
- // Ensure target is inside DEST
169
+
195
170
  if (!absTo.startsWith(DEST)) {
196
171
  throw new Error(`SECURITY: deepCopy target escapes dest:
197
172
  dest: ${DEST}
198
173
  target: ${absTo}`);
174
+ }
175
+
176
+ if (!fs.existsSync(absFrom)) {
177
+ throw new Error(`deepCopy: source does not exist: ${absFrom}`);
178
+ }
179
+
180
+ function copyRecursive(src, dst) {
181
+ const stat = fs.statSync(src);
182
+
183
+ if (stat.isDirectory()) {
184
+ fs.mkdirSync(dst, { recursive: true });
185
+ const entries = fs.readdirSync(src);
186
+ for (const entry of entries) {
187
+ copyRecursive(path.join(src, entry), path.join(dst, entry));
188
+ }
189
+ } else {
190
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
191
+ fs.copyFileSync(src, dst);
192
+ }
193
+ }
194
+
195
+ copyRecursive(absFrom, absTo);
199
196
  }
200
-
201
- // Ensure source exists
202
- if (!fs.existsSync(absFrom)) {
203
- throw new Error(`deepCopy: source does not exist: ${absFrom}`);
197
+
198
+ export function header() {
199
+ printHeader(import.meta);
204
200
  }
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
- }
201
+
202
+
203
+ function packageFileAbs(filename) {
204
+ const __filename = fileURLToPath(import.meta.url);
205
+ const __dirname = dirname(__filename);
206
+ return join(__dirname, filename);
207
+ }
208
+
209
+ // wraps whatever you provide in the overall buldng html wrapper.
210
+ // also compiles buldng's layout.ts and saves layout.js to the site root.
211
+ export function buldngHTMLWrapper(source) {
212
+ const srcWF = ensureWorkFile(source, "source");
213
+
214
+ // 1. Compile buldng's layout.ts (lazy)
215
+ const layoutTS = new WorkFile("file", { absPath: packageFileAbs("layout.ts") }, []);
216
+ const layoutJS = compile(layoutTS);
217
+
218
+ // 2. Save compiled layout.js to the site root (this triggers execution)
219
+ layoutJS.save("layout.js");
220
+
221
+ // 2.5 Save camera.css to the site root (this triggers execution)
222
+ const buldngCSS = new WorkFile("file", { absPath: packageFileAbs("buldng.css") }, []);
223
+ buldngCSS.save("buldng.css");
224
+
225
+ // 3. Wrap project shell in buldng's wrapper.html
226
+ const wrapperWF = new WorkFile("file", { absPath: packageFileAbs("wrapper.html") }, []);
227
+ return replace(wrapperWF, "<!--CAMERA-STYLE-->", srcWF);
228
+ }
229
+
230
+
231
+
232
+ const _warned = {
233
+ shell: false,
234
+ projectCSS: false
235
+ };
236
+
237
+ export function pageHTML({ app, css = null, js = null, out }) {
238
+ if (!out || typeof out !== "string") {
239
+ throw new Error("pageHTML(): 'out' must be a non-empty string");
240
+ }
241
+
242
+ if (!app) {
243
+ throw new Error("pageHTML(): 'app' is required and cannot be null");
244
+ }
245
+
246
+ const appWF = ensureWorkFile(app, "app");
247
+
248
+ // Look up optional registered assets
249
+ const shellWF = registry.get("shell") ?? null;
250
+ const projectCSS = registry.get("projectCSS") ?? null;
251
+
252
+ // One-time warnings
253
+ if (!shellWF && !_warned.shell) {
254
+ console.warn(
255
+ "pageHTML(): no 'shell' registered — building raw page HTML.\n" +
256
+ "To register a shell use: reg('shell', file`layout/shell.html`)"
257
+ );
258
+ _warned.shell = true;
259
+ }
260
+
261
+ if (!projectCSS && !_warned.projectCSS) {
262
+ console.warn(
263
+ "pageHTML(): no 'projectCSS' registered — page will not include project CSS.\n" +
264
+ "To register project CSS use: reg('projectCSS', file`layout/buldng.css`)"
265
+ );
266
+ _warned.projectCSS = true;
267
+ }
268
+
269
+ // --------------------------------------------------
270
+ // Build HTML WorkFile lazily
271
+ // --------------------------------------------------
272
+
273
+ let htmlWF;
274
+
275
+ if (shellWF) {
276
+ htmlWF = replace(shellWF, "<!--APP-->", appWF);
277
+ } else {
278
+ htmlWF = appWF;
279
+ }
280
+
281
+ // Insert JS lazily
282
+ if (js) {
283
+ const jsWF = resolveAsset(js, {
284
+ ext: ".js",
285
+ tag: "scriptlink",
286
+ wrap: scriptlink,
287
+ autoPrefix: "js"
288
+ });
289
+ htmlWF = replace(htmlWF, "<!--SCRIPTS-->", jsWF);
290
+ }
291
+
292
+ // Insert CSS lazily (project first)
293
+ const cssWFs = [];
294
+
295
+ if (projectCSS) {
296
+ cssWFs.push(resolveAsset(projectCSS, {
297
+ ext: ".css",
298
+ tag: "csslink",
299
+ wrap: csslink,
300
+ autoPrefix: "css"
301
+ }));
302
+ console.log(`${out}: has projectCSS`);
303
+ }
304
+
305
+ if (css) {
306
+ cssWFs.push(resolveAsset(css, {
307
+ ext: ".css",
308
+ tag: "csslink",
309
+ wrap: csslink,
310
+ autoPrefix: "css"
311
+ }));
312
+ console.log(`${out}: has pageCSS`);
313
+ }
314
+
315
+ if (cssWFs.length === 0) {
316
+ // No CSS at all → do nothing
317
+ // (This is allowed)
318
+ } else if (cssWFs.length === 1) {
319
+ // One CSS → inject directly
320
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssWFs[0]);
218
321
  } else {
219
- // File
220
- fs.mkdirSync(path.dirname(dst), { recursive: true });
221
- fs.copyFileSync(src, dst);
322
+ // Two CSS → append them
323
+ const cssListWF = append(...cssWFs);
324
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssListWF);
222
325
  }
326
+
327
+ // The ONLY public collapse operator
328
+ return htmlWF.save(out);
329
+ }
330
+
331
+ let autoCounter = 1;
332
+
333
+ function resolveAsset(input, {
334
+ ext, // ".css" or ".js"
335
+ tag, // "csslink" or "jslink"
336
+ wrap, // csslink() or stylelink()
337
+ autoPrefix // "css" or "js"
338
+ }) {
339
+ if (input == null) return null;
340
+
341
+ // Case 1: string
342
+ if (typeof input === "string") {
343
+ if (!input.endsWith(ext)) {
344
+ throw new Error(`pageHTML(${ext}): expected a ${ext} filename, got '${input}'`);
345
+ }
346
+ return wrap(input);
223
347
  }
224
348
 
225
- copyRecursive(absFrom, absTo);
226
- }
227
-
228
- // --------------------------------------------------
229
- // rm_dest("dir") — remove a file or directory inside DEST
230
- // --------------------------------------------------
349
+ // Case 2: must be WorkFile
350
+ if (!(input instanceof WorkFile)) {
351
+ throw new Error(`pageHTML(${ext}): expected string or WorkFile, got ${typeof input}`);
352
+ }
231
353
 
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()");
354
+ // Case 3: already a link
355
+ if (input.tag === tag) {
356
+ return input;
236
357
  }
237
358
 
238
- const abs = path.resolve(DEST, relPath);
359
+ // Case 4: literal WorkFile
360
+ if (input.op === "literal") {
361
+ const value = input.config.value;
239
362
 
240
- if (!abs.startsWith(DEST)) {
241
- throw new Error(`SECURITY: rmDest target escapes dest:
242
- dest: ${DEST}
243
- target: ${abs}`);
363
+ if (value.endsWith(ext)) {
364
+ return wrap(input);
365
+ }
366
+
367
+ throw new Error(
368
+ `pageHTML(${ext}): literal WorkFile does not end in ${ext} → '${value}'`
369
+ );
244
370
  }
245
371
 
246
- if (!fs.existsSync(abs)) {
247
- console.log(`rmDest: nothing to remove: ${abs}`);
248
- return;
372
+ // Case 5a: materialise WorkFile
373
+ if (input.op === "materialise") {
374
+ const target = input.config.target;
375
+
376
+ if (!target.endsWith(ext)) {
377
+ throw new Error(
378
+ `pageHTML(${ext}): materialise target '${target}' does not end in ${ext}`
379
+ );
380
+ }
381
+
382
+ return wrap(input);
249
383
  }
250
384
 
251
- fs.rmSync(abs, { recursive: true, force: true });
252
- console.log(`rmDest: removed ${abs}`);
253
- }
385
+ // Case 5b: blob auto-materialise
386
+ const autoName = `${autoPrefix}${autoCounter++}${ext}`;
254
387
 
388
+ console.warn(
389
+ `pageHTML(${ext}): received non-literal WorkFile; auto-materialising as ${autoName}`
390
+ );
255
391
 
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
- });
392
+ const mat = materialise(input, autoName);
393
+ return wrap(mat);
394
+ }
395
+
396
+
397
+ // --------------------------------------------------
398
+ // build-inputs.json manifest
399
+ // --------------------------------------------------
400
+ process.on("exit", () => {
401
+ if (process.env.BULDNG_HOT !== "1") return;
402
+ let DEST = getDest();
403
+ let INPUTS = getInputs();
404
+ const manifest = Array.from(INPUTS);
405
+ const out = path.join(DEST, "build-inputs.json");
406
+ fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
407
+ });
408
+