@pajh/buldng 0.0.2 → 0.0.4

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/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@pajh/buldng",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
- "main": "dist/init.js",
6
- "files": ["dist"],
5
+ "main": "./src/buldng-lib.js",
6
+ "files": [
7
+ "src"
8
+ ],
7
9
  "scripts": {
8
- "build": "node build.js"
10
+ "check": "tsc --noEmit",
11
+ "build": "npm run check"
9
12
  },
10
13
  "dependencies": {
11
- "esbuild": "^0.28.1",
12
- "typescript": "^7.0.2"
14
+ "esbuild": "^0.28.2"
13
15
  },
14
16
  "devDependencies": {
15
- "stylelint": "^17.14.1",
16
- "stylelint-config-standard": "^40.0.0"
17
- }
18
-
17
+ "typescript": "^6.0.3"
18
+ },
19
+ "exports": {
20
+ ".": "./src/buldng-lib.js",
21
+ "./web": "./src/buldng-web.ts"
22
+ }
19
23
  }
@@ -0,0 +1,84 @@
1
+ // Development-only error overlay installed by the local preview server.
2
+ // The production build never references this file from its HTML.
3
+
4
+ window.__buldngErrorOverlay = true;
5
+
6
+ let shown = false;
7
+
8
+ window.addEventListener("error", (event) => {
9
+ showError(event.error ?? event.message, "error");
10
+ });
11
+
12
+ window.addEventListener("unhandledrejection", (event) => {
13
+ showError(event.reason, "unhandledrejection");
14
+ });
15
+
16
+ function showError(reason, type) {
17
+ if (shown) return;
18
+ shown = true;
19
+
20
+ const message = getMessage(reason);
21
+ const stack = getStack(reason);
22
+ const overlay = document.createElement("aside");
23
+ overlay.id = "buldng-dev-error-overlay";
24
+ overlay.style.cssText = [
25
+ "position: fixed",
26
+ "z-index: 2147483647",
27
+ "left: 1rem",
28
+ "right: 1rem",
29
+ "bottom: 1rem",
30
+ "max-height: 70vh",
31
+ "overflow: auto",
32
+ "padding: 1rem 1.25rem",
33
+ "border: 2px solid #d33",
34
+ "border-radius: 8px",
35
+ "background: #240b0b",
36
+ "color: #fff",
37
+ "font: 16px/1.4 monospace",
38
+ "box-shadow: 0 4px 24px #0008",
39
+ ].join(";");
40
+
41
+ const close = document.createElement("button");
42
+ close.type = "button";
43
+ close.textContent = "Close";
44
+ close.style.cssText = "float:right; padding:.35rem .6rem; cursor:pointer";
45
+ close.addEventListener("click", () => overlay.remove());
46
+
47
+ const title = document.createElement("strong");
48
+ title.textContent = "BULDR preview: something broke";
49
+
50
+ const hint = document.createElement("p");
51
+ hint.textContent = `(${type}) Check the DevTools console for the full mapped stack.`;
52
+
53
+ const text = document.createElement("p");
54
+ text.textContent = message;
55
+
56
+ overlay.append(close, title, hint, text);
57
+ if (stack) {
58
+ const details = document.createElement("details");
59
+ const summary = document.createElement("summary");
60
+ summary.textContent = "Raw stack";
61
+ const pre = document.createElement("pre");
62
+ pre.textContent = stack;
63
+ pre.style.cssText = "white-space:pre-wrap; overflow-wrap:anywhere";
64
+ details.append(summary, pre);
65
+ overlay.appendChild(details);
66
+ }
67
+
68
+ document.body?.appendChild(overlay);
69
+ }
70
+
71
+ function getMessage(reason) {
72
+ if (reason instanceof Error && reason.message) return reason.message;
73
+ if (typeof reason === "string" && reason) return reason;
74
+ try {
75
+ const text = JSON.stringify(reason);
76
+ return text || "Unknown error";
77
+ } catch {
78
+ return String(reason) || "Unknown error";
79
+ }
80
+ }
81
+
82
+ function getStack(reason) {
83
+ return reason && typeof reason.stack === "string" ? reason.stack : "";
84
+ }
@@ -0,0 +1,465 @@
1
+ // tools/build-lib.js
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import esbuild from "esbuild";
5
+
6
+
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+ import { readFileSync } from "node:fs";
10
+ import { createRequire } from "node:module";
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
+ typescriptCheck
40
+ };
41
+
42
+ const here = dirname(fileURLToPath(import.meta.url));
43
+ const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
44
+ const registry = new Map();
45
+
46
+ export function reg(key, value) {
47
+ if (typeof key !== "string") {
48
+ throw new Error(`reg(): key must be a string`);
49
+ }
50
+
51
+ const wf = ensureWorkFile(value, "reg");
52
+
53
+ if (registry.has(key)) {
54
+ console.log(`reg(): overwriting existing key '${key}'`);
55
+ }
56
+
57
+ registry.set(key, wf);
58
+ return wf;
59
+ }
60
+
61
+ function findTSC() {
62
+ const localTSC = path.resolve("node_modules/.bin/tsc");
63
+
64
+ if (fs.existsSync(localTSC)) {
65
+ return localTSC; // Local compiler — correct
66
+ }
67
+
68
+ try {
69
+ const globalTSC = execSync("which tsc").toString().trim();
70
+ return globalTSC; // Global compiler — acceptable
71
+ } catch {
72
+ return null; // No compiler found
73
+ }
74
+ }
75
+
76
+ export function typescriptCheck() {
77
+ let hasTS = false;
78
+ const require = createRequire(import.meta.url);
79
+
80
+ try {
81
+ // Detect ANY installed TypeScript (local or global)
82
+ require.resolve("typescript");
83
+
84
+ hasTS = true;
85
+ } catch {
86
+ hasTS = false;
87
+ }
88
+
89
+ if (!hasTS) {
90
+ console.log("TypeScript not found — skipping TS checks");
91
+ return;
92
+ }
93
+
94
+ const { execSync } = require("node:child_process");
95
+ const tscPath = findTSC();
96
+
97
+ if (!tscPath) {
98
+ console.error("TypeScript compiler not found (local or global)");
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log("Using TypeScript compiler:", tscPath);
103
+
104
+
105
+ try {
106
+ // npx will run local tsc if present, otherwise global tsc
107
+ execSync(`${tscPath} --noEmit`, { stdio: "inherit" });
108
+ } catch (err) {
109
+ throw new Error("TypeScript check failed");
110
+ }
111
+ }
112
+
113
+
114
+ let headerPrinted = false;
115
+ export function printHeader(importMeta) {
116
+ if (headerPrinted) return;
117
+ headerPrinted = true;
118
+
119
+ const version = pkg.version;
120
+ const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
121
+
122
+ const callerFile = fileURLToPath(importMeta.url);
123
+ const callerDir = dirname(callerFile);
124
+
125
+ console.log(`*local* buldng build system v${version} — ${timestamp}`);
126
+ console.log(`${callerDir}/${callerFile.split("/").pop()}`);
127
+ }
128
+
129
+ export function csslink(input) {
130
+ const pre = `<link rel="stylesheet" href="`;
131
+ const post = `">`;
132
+
133
+ // --------------------------------------------------
134
+ // Case 1: string
135
+ // --------------------------------------------------
136
+ if (typeof input === "string") {
137
+ if (!input.endsWith(".css")) {
138
+ throw new Error(`csslink(): expected a .css filename, got '${input}'`);
139
+ }
140
+
141
+ return new WorkFile(
142
+ "literal",
143
+ { value: pre + ensureLeadingSlash(input) + post },
144
+ [],
145
+ "csslink"
146
+ );
147
+ }
148
+
149
+ // --------------------------------------------------
150
+ // Case 2: WorkFile
151
+ // --------------------------------------------------
152
+ if (input instanceof WorkFile) {
153
+ return new WorkFile(
154
+ "wrap",
155
+ { pre, post },
156
+ [input],
157
+ "csslink"
158
+ );
159
+ }
160
+
161
+ throw new Error(`csslink(): expected string or WorkFile, got ${typeof input}`);
162
+ }
163
+
164
+ export function scriptlink(input) {
165
+ const pre = `<script type="module" src="/`;
166
+ const post = `"></script>`;
167
+
168
+ // --------------------------------------------------
169
+ // Case 1: string
170
+ // --------------------------------------------------
171
+ if (typeof input === "string") {
172
+ if (!input.endsWith(".js")) {
173
+ throw new Error(`scriptlink(): expected a .js filename, got '${input}'`);
174
+ }
175
+
176
+ return new WorkFile(
177
+ "literal",
178
+ { value: pre + input + post },
179
+ [],
180
+ "scriptlink"
181
+ );
182
+ }
183
+
184
+ // --------------------------------------------------
185
+ // Case 2: WorkFile
186
+ // --------------------------------------------------
187
+ if (input instanceof WorkFile) {
188
+ return new WorkFile(
189
+ "wrap",
190
+ { pre, post },
191
+ [input],
192
+ "scriptlink"
193
+ );
194
+ }
195
+
196
+ throw new Error(`scriptlink(): expected string or WorkFile, got ${typeof input}`);
197
+ }
198
+
199
+
200
+ // --------------------------------------------------
201
+ // mkdir("puzzle")
202
+ // --------------------------------------------------
203
+ export function mkdir(relDir) {
204
+ const abs = assertDestWrite(relDir);
205
+ fs.mkdirSync(abs, { recursive: true });
206
+ }
207
+
208
+ // --------------------------------------------------
209
+ // deepCopy — raw filesystem path
210
+ // --------------------------------------------------
211
+ export function deepCopy(from, to) {
212
+ let DEST = getDest();
213
+
214
+ console.log(`deepCopy: ${from} → ${to}`);
215
+
216
+ const absFrom = path.resolve(from);
217
+
218
+ if (to.includes("..")) {
219
+ throw new Error("SECURITY: Illegal path traversal in deepCopy()");
220
+ }
221
+
222
+ const absTo = to === "/" ? DEST : path.resolve(DEST, to);
223
+
224
+ if (!absTo.startsWith(DEST)) {
225
+ throw new Error(`SECURITY: deepCopy target escapes dest:
226
+ dest: ${DEST}
227
+ target: ${absTo}`);
228
+ }
229
+
230
+ if (!fs.existsSync(absFrom)) {
231
+ throw new Error(`deepCopy: source does not exist: ${absFrom}`);
232
+ }
233
+
234
+ function copyRecursive(src, dst) {
235
+ const stat = fs.statSync(src);
236
+
237
+ if (stat.isDirectory()) {
238
+ fs.mkdirSync(dst, { recursive: true });
239
+ const entries = fs.readdirSync(src);
240
+ for (const entry of entries) {
241
+ copyRecursive(path.join(src, entry), path.join(dst, entry));
242
+ }
243
+ } else {
244
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
245
+ fs.copyFileSync(src, dst);
246
+ }
247
+ }
248
+
249
+ copyRecursive(absFrom, absTo);
250
+ }
251
+
252
+ export function header() {
253
+ printHeader(import.meta);
254
+ }
255
+
256
+
257
+ function packageFileAbs(filename) {
258
+ const __filename = fileURLToPath(import.meta.url);
259
+ const __dirname = dirname(__filename);
260
+ return join(__dirname, filename);
261
+ }
262
+
263
+ // wraps whatever you provide in the overall buldng html wrapper.
264
+ // also compiles buldng's layout.ts and saves layout.js to the site root.
265
+ export function buldngHTMLWrapper(source) {
266
+ const srcWF = ensureWorkFile(source, "source");
267
+
268
+ // 1. Compile buldng's layout.ts (lazy)
269
+ const layoutTS = new WorkFile("file", { absPath: packageFileAbs("layout.ts") }, []);
270
+ const layoutJS = compile(layoutTS);
271
+
272
+ // 2. Save compiled layout.js to the site root (this triggers execution)
273
+ layoutJS.save("layout.js");
274
+
275
+ // 2.5 Save camera.css to the site root (this triggers execution)
276
+ const buldngCSS = new WorkFile("file", { absPath: packageFileAbs("buldng.css") }, []);
277
+ buldngCSS.save("buldng.css");
278
+
279
+ const devErrors = new WorkFile("file", { absPath: packageFileAbs("buldng-dev-errors.js") }, []);
280
+ devErrors.save("buldng-dev-errors.js");
281
+
282
+ // 3. Wrap project shell in buldng's wrapper.html
283
+ const wrapperWF = new WorkFile("file", { absPath: packageFileAbs("wrapper.html") }, []);
284
+ return replace(wrapperWF, "<!--CAMERA-STYLE-->", srcWF);
285
+ }
286
+
287
+
288
+
289
+ const _warned = {
290
+ shell: false,
291
+ projectCSS: false
292
+ };
293
+
294
+ export function pageHTML({ app, css = null, js = null, out }) {
295
+ if (!out || typeof out !== "string") {
296
+ throw new Error("pageHTML(): 'out' must be a non-empty string");
297
+ }
298
+
299
+ if (!app) {
300
+ throw new Error("pageHTML(): 'app' is required and cannot be null");
301
+ }
302
+
303
+ const appWF = ensureWorkFile(app, "app");
304
+
305
+ // Look up optional registered assets
306
+ const shellWF = registry.get("shell") ?? null;
307
+ const projectCSS = registry.get("projectCSS") ?? null;
308
+
309
+ // One-time warnings
310
+ if (!shellWF && !_warned.shell) {
311
+ console.warn(
312
+ "pageHTML(): no 'shell' registered — building raw page HTML.\n" +
313
+ "To register a shell use: reg('shell', file`layout/shell.html`)"
314
+ );
315
+ _warned.shell = true;
316
+ }
317
+
318
+ if (!projectCSS && !_warned.projectCSS) {
319
+ console.warn(
320
+ "pageHTML(): no 'projectCSS' registered — page will not include project CSS.\n" +
321
+ "To register project CSS use: reg('projectCSS', file`layout/buldng.css`)"
322
+ );
323
+ _warned.projectCSS = true;
324
+ }
325
+
326
+ // --------------------------------------------------
327
+ // Build HTML WorkFile lazily
328
+ // --------------------------------------------------
329
+
330
+ let htmlWF;
331
+
332
+ if (shellWF) {
333
+ htmlWF = replace(shellWF, "<!--APP-->", appWF);
334
+ } else {
335
+ htmlWF = appWF;
336
+ }
337
+
338
+ // Insert JS lazily
339
+ if (js) {
340
+ const jsWF = resolveAsset(js, {
341
+ ext: ".js",
342
+ tag: "scriptlink",
343
+ wrap: scriptlink,
344
+ autoPrefix: "js"
345
+ });
346
+ htmlWF = replace(htmlWF, "<!--SCRIPTS-->", jsWF);
347
+ }
348
+
349
+ // Insert CSS lazily (project first)
350
+ const cssWFs = [];
351
+
352
+ if (projectCSS) {
353
+ cssWFs.push(resolveAsset(projectCSS, {
354
+ ext: ".css",
355
+ tag: "csslink",
356
+ wrap: csslink,
357
+ autoPrefix: "css"
358
+ }));
359
+ console.log(`${out}: has projectCSS`);
360
+ }
361
+
362
+ if (css) {
363
+ cssWFs.push(resolveAsset(css, {
364
+ ext: ".css",
365
+ tag: "csslink",
366
+ wrap: csslink,
367
+ autoPrefix: "css"
368
+ }));
369
+ console.log(`${out}: has pageCSS`);
370
+ }
371
+
372
+ if (cssWFs.length === 0) {
373
+ // No CSS at all → do nothing
374
+ // (This is allowed)
375
+ } else if (cssWFs.length === 1) {
376
+ // One CSS → inject directly
377
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssWFs[0]);
378
+ } else {
379
+ // Two CSS → append them
380
+ const cssListWF = append(...cssWFs);
381
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssListWF);
382
+ }
383
+
384
+ // The ONLY public collapse operator
385
+ return htmlWF.save(out);
386
+ }
387
+
388
+ let autoCounter = 1;
389
+
390
+ function resolveAsset(input, {
391
+ ext, // ".css" or ".js"
392
+ tag, // "csslink" or "jslink"
393
+ wrap, // csslink() or stylelink()
394
+ autoPrefix // "css" or "js"
395
+ }) {
396
+ if (input == null) return null;
397
+
398
+ // Case 1: string
399
+ if (typeof input === "string") {
400
+ if (!input.endsWith(ext)) {
401
+ throw new Error(`pageHTML(${ext}): expected a ${ext} filename, got '${input}'`);
402
+ }
403
+ return wrap(input);
404
+ }
405
+
406
+ // Case 2: must be WorkFile
407
+ if (!(input instanceof WorkFile)) {
408
+ throw new Error(`pageHTML(${ext}): expected string or WorkFile, got ${typeof input}`);
409
+ }
410
+
411
+ // Case 3: already a link
412
+ if (input.tag === tag) {
413
+ return input;
414
+ }
415
+
416
+ // Case 4: literal WorkFile
417
+ if (input.op === "literal") {
418
+ const value = input.config.value;
419
+
420
+ if (value.endsWith(ext)) {
421
+ return wrap(input);
422
+ }
423
+
424
+ throw new Error(
425
+ `pageHTML(${ext}): literal WorkFile does not end in ${ext} → '${value}'`
426
+ );
427
+ }
428
+
429
+ // Case 5a: materialise WorkFile
430
+ if (input.op === "materialise") {
431
+ const target = input.config.target;
432
+
433
+ if (!target.endsWith(ext)) {
434
+ throw new Error(
435
+ `pageHTML(${ext}): materialise target '${target}' does not end in ${ext}`
436
+ );
437
+ }
438
+
439
+ return wrap(input);
440
+ }
441
+
442
+ // Case 5b: blob → auto-materialise
443
+ const autoName = `${autoPrefix}${autoCounter++}${ext}`;
444
+
445
+ console.warn(
446
+ `pageHTML(${ext}): received non-literal WorkFile; auto-materialising as ${autoName}`
447
+ );
448
+
449
+ const mat = materialise(input, autoName);
450
+ return wrap(mat);
451
+ }
452
+
453
+
454
+ // --------------------------------------------------
455
+ // build-inputs.json manifest
456
+ // --------------------------------------------------
457
+ process.on("exit", () => {
458
+ if (process.env.BULDNG_HOT !== "1") return;
459
+ let DEST = getDest();
460
+ let INPUTS = getInputs();
461
+ const manifest = Array.from(INPUTS);
462
+ const out = path.join(DEST, "build-inputs.json");
463
+ fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
464
+ });
465
+