@pajh/buldng 0.0.2 → 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.
@@ -3,32 +3,146 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import esbuild from "esbuild";
5
5
 
6
+
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { dirname, join } from "node:path";
8
9
  import { readFileSync } from "node:fs";
9
10
 
10
11
  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";
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
+ };
13
40
 
14
41
  const here = dirname(fileURLToPath(import.meta.url));
15
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`);
48
+ }
49
+
50
+ const wf = ensureWorkFile(value, "reg");
51
+
52
+ if (registry.has(key)) {
53
+ console.log(`reg(): overwriting existing key '${key}'`);
54
+ }
55
+
56
+ registry.set(key, wf);
57
+ return wf;
58
+ }
16
59
 
17
60
  let headerPrinted = false;
18
61
  export function printHeader(importMeta) {
19
62
  if (headerPrinted) return;
20
63
  headerPrinted = true;
21
-
64
+
22
65
  const version = pkg.version;
23
66
  const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
24
-
67
+
25
68
  const callerFile = fileURLToPath(importMeta.url);
26
69
  const callerDir = dirname(callerFile);
27
-
28
- console.log(`Xx buldng build system v${version} — ${timestamp}`);
70
+
71
+ console.log(`*local* buldng build system v${version} — ${timestamp}`);
29
72
  console.log(`${callerDir}/${callerFile.split("/").pop()}`);
30
73
  }
31
74
 
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
+ );
93
+ }
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
+ );
105
+ }
106
+
107
+ throw new Error(`csslink(): expected string or WorkFile, got ${typeof input}`);
108
+ }
109
+
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"
127
+ );
128
+ }
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}`);
143
+ }
144
+
145
+
32
146
  // --------------------------------------------------
33
147
  // mkdir("puzzle")
34
148
  // --------------------------------------------------
@@ -42,153 +156,253 @@ export function mkdir(relDir) {
42
156
  // --------------------------------------------------
43
157
  export function deepCopy(from, to) {
44
158
  let DEST = getDest();
45
-
159
+
46
160
  console.log(`deepCopy: ${from} → ${to}`);
47
-
161
+
48
162
  const absFrom = path.resolve(from);
49
-
163
+
50
164
  if (to.includes("..")) {
51
165
  throw new Error("SECURITY: Illegal path traversal in deepCopy()");
52
166
  }
53
-
167
+
54
168
  const absTo = to === "/" ? DEST : path.resolve(DEST, to);
55
-
169
+
56
170
  if (!absTo.startsWith(DEST)) {
57
171
  throw new Error(`SECURITY: deepCopy target escapes dest:
58
172
  dest: ${DEST}
59
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);
60
196
  }
61
-
62
- if (!fs.existsSync(absFrom)) {
63
- throw new Error(`deepCopy: source does not exist: ${absFrom}`);
197
+
198
+ export function header() {
199
+ printHeader(import.meta);
64
200
  }
65
-
66
- function copyRecursive(src, dst) {
67
- const stat = fs.statSync(src);
68
-
69
- if (stat.isDirectory()) {
70
- fs.mkdirSync(dst, { recursive: true });
71
- const entries = fs.readdirSync(src);
72
- for (const entry of entries) {
73
- copyRecursive(path.join(src, entry), path.join(dst, entry));
74
- }
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);
75
277
  } else {
76
- fs.mkdirSync(path.dirname(dst), { recursive: true });
77
- fs.copyFileSync(src, dst);
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`);
78
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]);
321
+ } else {
322
+ // Two CSS → append them
323
+ const cssListWF = append(...cssWFs);
324
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssListWF);
325
+ }
326
+
327
+ // The ONLY public collapse operator
328
+ return htmlWF.save(out);
79
329
  }
80
-
81
- copyRecursive(absFrom, absTo);
82
- }
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");
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);
94
347
  }
95
348
 
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
-
109
- // --------------------------------------------------
110
- // ensureWorkFile(x)
111
- // --------------------------------------------------
112
- function ensureWorkFile(x, fieldName = "value") {
113
- if (x instanceof WorkFile) return x;
114
-
115
- if (typeof x === "string") {
116
- return literal(x);
349
+ // Case 2: must be WorkFile
350
+ if (!(input instanceof WorkFile)) {
351
+ throw new Error(`pageHTML(${ext}): expected string or WorkFile, got ${typeof input}`);
117
352
  }
118
353
 
119
- throw new Error(`${fieldName} must be WorkFile or string`);
120
- }
121
-
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
- }
354
+ // Case 3: already a link
355
+ if (input.tag === tag) {
356
+ return input;
357
+ }
132
358
 
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
- }
359
+ // Case 4: literal WorkFile
360
+ if (input.op === "literal") {
361
+ const value = input.config.value;
143
362
 
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
- });
363
+ if (value.endsWith(ext)) {
364
+ return wrap(input);
365
+ }
153
366
 
154
- if (children.length < 2) {
155
- throw new Error("append(): requires at least 2 items");
367
+ throw new Error(
368
+ `pageHTML(${ext}): literal WorkFile does not end in ${ext} → '${value}'`
369
+ );
156
370
  }
157
371
 
158
- return new WorkFile("append", {}, children);
159
- }
372
+ // Case 5a: materialise WorkFile
373
+ if (input.op === "materialise") {
374
+ const target = input.config.target;
160
375
 
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");
376
+ if (!target.endsWith(ext)) {
377
+ throw new Error(
378
+ `pageHTML(${ext}): materialise target '${target}' does not end in ${ext}`
379
+ );
380
+ }
168
381
 
169
- if (tagWF.op === "literal" && tagWF.config.value.trim() === "") {
170
- throw new Error(`replace(): tag cannot be blank`);
382
+ return wrap(input);
171
383
  }
172
384
 
173
- return new WorkFile("replace", {}, [srcWF, tagWF, repWF]);
174
- }
385
+ // Case 5b: blob auto-materialise
386
+ const autoName = `${autoPrefix}${autoCounter++}${ext}`;
175
387
 
176
- // --------------------------------------------------
177
- // compile(source)
178
- // --------------------------------------------------
179
- export function compile(source, flags = {}) {
180
- const srcWF = ensureWorkFile(source, "source");
181
- return new WorkFile("compile", { flags }, [srcWF]);
388
+ console.warn(
389
+ `pageHTML(${ext}): received non-literal WorkFile; auto-materialising as ${autoName}`
390
+ );
391
+
392
+ const mat = materialise(input, autoName);
393
+ return wrap(mat);
182
394
  }
183
395
 
184
- // --------------------------------------------------
185
- // build-inputs.json manifest
186
- // --------------------------------------------------
187
- process.on("exit", () => {
188
- if (process.env.BULDNG_HOT !== "1") return;
189
- let DEST = getDest();
190
- let INPUTS = getInputs();
191
- const manifest = Array.from(INPUTS);
192
- const out = path.join(DEST, "build-inputs.json");
193
- fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
194
- });
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
+