@pajh/buldng 0.0.3 → 0.0.5

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/src/serve.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ // serve.js
3
+ import http from "node:http";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { exec } from "node:child_process";
7
+ import MIME from "./MIME.json" with { type: "json" };
8
+
9
+ import {
10
+ createState,
11
+ initHotReload,
12
+ initDevErrors,
13
+ handleHttpRequest,
14
+ processArgs,
15
+ registerInject,
16
+ sanitiseRoot,
17
+ sanitisePort,
18
+ local,
19
+ ARGS
20
+ } from "./serve-lib.js";
21
+
22
+ // --------------------------------------------------
23
+ // CONFIG + STATIC DATA
24
+ // --------------------------------------------------
25
+
26
+ const DEFAULT_PORT = 5173;
27
+ const DEFAULT_ROOT = "dist";
28
+
29
+ // --------------------------------------------------
30
+ // HTML INJECTIONS
31
+ // --------------------------------------------------
32
+
33
+ function registerHtmlInjections(state) {
34
+
35
+ if (state.DEV_ERRORS) {
36
+ console.log("[dev-errors] creating injection for dev-errors.js");
37
+
38
+ // add mapping entry
39
+ state.MAPPINGS.push({
40
+ from: "/dev-errors.js",
41
+ to: "buldng://dev-errors.js",
42
+ type: "file"
43
+ });
44
+
45
+ registerInject(state, "html", "<!--POSTLAYOUT-->", `
46
+ <script type="module" src="/dev-errors.js"></script>
47
+ `, "post");
48
+ }
49
+
50
+ if (state.hotEnabled) {
51
+ registerInject(state, "html", "</head>", `
52
+ <script>
53
+ const es = new EventSource("/__hot");
54
+ es.onmessage = () => location.reload();
55
+ </script>
56
+ `, "pre");
57
+ }
58
+ }
59
+
60
+
61
+ // --------------------------------------------------
62
+ // INITIALIZATION
63
+ // --------------------------------------------------
64
+
65
+ function init() {
66
+
67
+ processArgs();
68
+ let ROOT = ARGS.get("root");
69
+ let PORT = ARGS.get("port");
70
+
71
+ if (!ROOT) {
72
+ ROOT = DEFAULT_ROOT;
73
+ console.log(`[args] root: ${ROOT} (DEFAULT)`);
74
+ } else {
75
+ console.log(`[args] root: ${ROOT}`);
76
+ }
77
+
78
+ ROOT = sanitiseRoot(ROOT);
79
+
80
+ if (!PORT) {
81
+ PORT = DEFAULT_PORT;
82
+ console.log(`[args] port: ${PORT} (DEFAULT)`);
83
+ } else {
84
+ console.log(`[args] port: ${PORT}`);
85
+ }
86
+
87
+ PORT = sanitisePort(PORT);
88
+
89
+ const state = createState({
90
+ ROOT: ROOT,
91
+ PORT: PORT,
92
+ MAPPINGS: [],
93
+ MIME: MIME,
94
+ LOG_ALL: ARGS.get("log"),
95
+ HOT_REQUESTED: ARGS.get("hot"),
96
+ DEV_ERRORS: ARGS.get("dev-errors")
97
+ });
98
+
99
+ loadMappings(state);
100
+ initHotReload(state);
101
+ initDevErrors(state);
102
+ registerHtmlInjections(state);
103
+
104
+ return state;
105
+ }
106
+
107
+ function loadMappings(state) {
108
+ const mapPath = ARGS.get("map");
109
+
110
+ if (!mapPath) {
111
+ console.log("[map] OFF");
112
+ state.MAPPINGS = [];
113
+ return;
114
+ }
115
+
116
+ try {
117
+ const full = path.resolve(mapPath);
118
+ const json = JSON.parse(fs.readFileSync(full, "utf8"));
119
+ state.MAPPINGS = json;
120
+ console.log(`[map] ${full} (${json.length} rules)`);
121
+ } catch (err) {
122
+ console.log(`[map] FAILED to load ${mapPath}`);
123
+ process.exit(13);
124
+ }
125
+ }
126
+
127
+
128
+ // --------------------------------------------------
129
+ // SERVE LOOP (VISIBLE SKELETON)
130
+ // --------------------------------------------------
131
+
132
+ function serve(state) {
133
+ const server = http.createServer((req, res) => {
134
+ // -----------------------------
135
+ // The bones of the loop stay here
136
+ // -----------------------------
137
+
138
+ state.running = true;
139
+
140
+ // 1. Receive request
141
+ const urlPath = req.url;
142
+
143
+ // 2. Delegate to serve-lib.js to produce a response object
144
+ const response = handleHttpRequest(urlPath, req, state);
145
+
146
+ // 3. Send response
147
+ response.send(res);
148
+ });
149
+
150
+ process.on("SIGTERM", () => {
151
+ // clean shutdown logic
152
+ server.close(() => {
153
+ process.exit(0);
154
+ });
155
+ });
156
+
157
+ // Optional graceful shutdown
158
+ process.on("SIGINT", () => {
159
+ teardown(state);
160
+ server.close(() => process.exit(0));
161
+ });
162
+
163
+ server.listen(state.PORT, () => {
164
+ console.log(`[serve] root: ${state.ROOT} at http://localhost:${state.PORT}`);
165
+ const browser = ARGS.get("browser");
166
+ if (browser)
167
+ exec(`${browser} http://localhost:${state.PORT}`);
168
+ });
169
+
170
+ return server;
171
+ }
172
+
173
+ // --------------------------------------------------
174
+ // TEARDOWN
175
+ // --------------------------------------------------
176
+
177
+ function teardown(state) {
178
+ state.running = false;
179
+ console.log("Server shutting down.");
180
+ }
181
+
182
+ // --------------------------------------------------
183
+ // MAIN
184
+ // --------------------------------------------------
185
+
186
+ function main() {
187
+ const state = init();
188
+ const server = serve(state);
189
+
190
+
191
+ }
192
+
193
+ main();
@@ -0,0 +1,19 @@
1
+ export {};
2
+
3
+ declare global {
4
+ interface Window {
5
+ __ready: {
6
+ a: Promise<void>;
7
+ _resolveA: () => void;
8
+ };
9
+
10
+ __error_handler?: (
11
+ event: ErrorEvent | PromiseRejectionEvent,
12
+ page: string
13
+ ) => void;
14
+
15
+ __buldngErrorOverlay?: boolean;
16
+
17
+ __buldng_listenersInstalled?: boolean;
18
+ }
19
+ }
@@ -13,6 +13,7 @@
13
13
  window.__ready.a = new Promise((resolve) => { window.__ready._resolveA = resolve; });
14
14
  </script>
15
15
  <script type="module" src="/layout.js"></script>
16
+ <!--POSTLAYOUT-->
16
17
  <!--SCRIPTS-->
17
18
  </head>
18
19
  <body>
@@ -20,5 +21,6 @@
20
21
  <!-- Project outline (camera-style) will be stitched here -->
21
22
  <!--CAMERA-STYLE-->
22
23
  </div>
24
+ <div id="b-modal-root"></div>
23
25
  </body>
24
26
  </html>
@@ -1,408 +0,0 @@
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
-
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`);
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
- }
59
-
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()}`);
73
- }
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
-
146
- // --------------------------------------------------
147
- // mkdir("puzzle")
148
- // --------------------------------------------------
149
- export function mkdir(relDir) {
150
- const abs = assertDestWrite(relDir);
151
- fs.mkdirSync(abs, { recursive: true });
152
- }
153
-
154
- // --------------------------------------------------
155
- // deepCopy — raw filesystem path
156
- // --------------------------------------------------
157
- export function deepCopy(from, to) {
158
- let DEST = getDest();
159
-
160
- console.log(`deepCopy: ${from} → ${to}`);
161
-
162
- const absFrom = path.resolve(from);
163
-
164
- if (to.includes("..")) {
165
- throw new Error("SECURITY: Illegal path traversal in deepCopy()");
166
- }
167
-
168
- const absTo = to === "/" ? DEST : path.resolve(DEST, to);
169
-
170
- if (!absTo.startsWith(DEST)) {
171
- throw new Error(`SECURITY: deepCopy target escapes dest:
172
- dest: ${DEST}
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);
196
- }
197
-
198
- export function header() {
199
- printHeader(import.meta);
200
- }
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]);
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);
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);
347
- }
348
-
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
- }
353
-
354
- // Case 3: already a link
355
- if (input.tag === tag) {
356
- return input;
357
- }
358
-
359
- // Case 4: literal WorkFile
360
- if (input.op === "literal") {
361
- const value = input.config.value;
362
-
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
- );
370
- }
371
-
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);
383
- }
384
-
385
- // Case 5b: blob → auto-materialise
386
- const autoName = `${autoPrefix}${autoCounter++}${ext}`;
387
-
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);
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
-
@@ -1,10 +0,0 @@
1
- export {};
2
-
3
- declare global {
4
- interface Window {
5
- __ready: {
6
- a: Promise<void>;
7
- _resolveA: () => void;
8
- };
9
- }
10
- }
File without changes
File without changes
File without changes
File without changes