@pajh/buldng 0.0.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pajh/buldng",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "./src/buldng-lib.js",
6
6
  "files": [
@@ -18,6 +18,11 @@
18
18
  },
19
19
  "exports": {
20
20
  ".": "./src/buldng-lib.js",
21
- "./web": "./src/buldng-web.ts"
21
+ "./web": "./src/buldng-web.ts",
22
+ "./error": "./src/layout-err.ts",
23
+ "./MIME.json": "./src/MIME.json"
24
+ },
25
+ "bin": {
26
+ "buldng-serve": "./src/serve.js"
22
27
  }
23
28
  }
package/src/MIME.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ ".html": "text/html",
3
+ ".css": "text/css",
4
+ ".js": "application/javascript",
5
+ ".json": "application/json",
6
+ ".svg": "image/svg+xml",
7
+ ".png": "image/png",
8
+ ".woff2": "font/woff2",
9
+ ".jpg": "image/jpeg",
10
+ ".jpeg": "image/jpeg"
11
+ }
@@ -0,0 +1,20 @@
1
+ // Experimental async building functions
2
+
3
+ export function task(fn) {
4
+ return new Promise((resolve, reject) => {
5
+ try {
6
+ fn();
7
+ resolve();
8
+ } catch (err) {
9
+ reject(err);
10
+ }
11
+ });
12
+ }
13
+
14
+ export function parallel(...jobs) {
15
+ return Promise.all(jobs);
16
+ }
17
+
18
+ export function finalise(fn) {
19
+ return fn();
20
+ }
@@ -0,0 +1,58 @@
1
+ // buldng-files.js
2
+ // file handling functions for buldng
3
+ import { getDest, setSource, setDest, assertDestWrite, assertSourceRead, getInputs, looksLikeFilename, looksLikeHTML,
4
+ looksLikeCSS, looksLikeJS } from "./buldng-validate.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ // --------------------------------------------------
9
+ // mkdir("puzzle")
10
+ // --------------------------------------------------
11
+ export function mkdir(relDir) {
12
+ const abs = assertDestWrite(relDir);
13
+ fs.mkdirSync(abs, { recursive: true });
14
+ }
15
+
16
+ // --------------------------------------------------
17
+ // deepCopy — raw filesystem path
18
+ // --------------------------------------------------
19
+ export function deepCopy(from, to) {
20
+ let DEST = getDest();
21
+
22
+ console.log(`deepCopy: ${from} → ${to}`);
23
+
24
+ const absFrom = path.resolve(from);
25
+
26
+ if (to.includes("..")) {
27
+ throw new Error("SECURITY: Illegal path traversal in deepCopy()");
28
+ }
29
+
30
+ const absTo = to === "/" ? DEST : path.resolve(DEST, to);
31
+
32
+ if (!absTo.startsWith(DEST)) {
33
+ throw new Error(`SECURITY: deepCopy target escapes dest:
34
+ dest: ${DEST}
35
+ target: ${absTo}`);
36
+ }
37
+
38
+ if (!fs.existsSync(absFrom)) {
39
+ throw new Error(`deepCopy: source does not exist: ${absFrom}`);
40
+ }
41
+
42
+ function copyRecursive(src, dst) {
43
+ const stat = fs.statSync(src);
44
+
45
+ if (stat.isDirectory()) {
46
+ fs.mkdirSync(dst, { recursive: true });
47
+ const entries = fs.readdirSync(src);
48
+ for (const entry of entries) {
49
+ copyRecursive(path.join(src, entry), path.join(dst, entry));
50
+ }
51
+ } else {
52
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
53
+ fs.copyFileSync(src, dst);
54
+ }
55
+ }
56
+
57
+ copyRecursive(absFrom, absTo);
58
+ }
@@ -0,0 +1,262 @@
1
+ // buldng-html.js — HTML assembly subsystem
2
+
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { readFileSync } from "node:fs";
6
+ import { WorkFile } from "./workfile.js";
7
+ import {
8
+ ensureWorkFile,
9
+ ensureLeadingSlash
10
+ } from "./buldng-validate.js";
11
+ import {
12
+ replace,
13
+ append,
14
+ materialise,
15
+ compile
16
+ } from "./buldng-ops.js";
17
+
18
+ // --------------------------------------------------
19
+ // Registry for shell + projectCSS
20
+ // --------------------------------------------------
21
+ const registry = new Map();
22
+
23
+ export function reg(key, value) {
24
+ if (typeof key !== "string") {
25
+ throw new Error(`reg(): key must be a string`);
26
+ }
27
+
28
+ const wf = ensureWorkFile(value, "reg");
29
+
30
+ if (registry.has(key)) {
31
+ console.log(`reg(): overwriting existing key '${key}'`);
32
+ }
33
+
34
+ registry.set(key, wf);
35
+ return wf;
36
+ }
37
+
38
+ function makeLinkOp({ name, ext, pre, post }) {
39
+ return function linkOp(input) {
40
+
41
+ // Case 1: string
42
+ if (typeof input === "string") {
43
+ if (!input.endsWith(ext)) {
44
+ throw new Error(`${name}(): expected a ${ext} filename or WorkFile`);
45
+ }
46
+
47
+ const fixed = ensureLeadingSlash(input);
48
+
49
+ return new WorkFile(
50
+ "literal",
51
+ { value: pre + fixed + post },
52
+ [],
53
+ name
54
+ );
55
+ }
56
+
57
+ // Case 2: WorkFile
58
+ if (input instanceof WorkFile) {
59
+ return new WorkFile(
60
+ "wrap",
61
+ { pre, post },
62
+ [input],
63
+ name
64
+ );
65
+ }
66
+
67
+ throw new Error(`${name}(): expected a ${ext} filename or WorkFile`);
68
+ };
69
+ }
70
+
71
+ export const csslink = makeLinkOp({
72
+ name: "csslink",
73
+ ext: ".css",
74
+ pre: `<link rel="stylesheet" href="`,
75
+ post: `">`
76
+ });
77
+
78
+ export const scriptlink = makeLinkOp({
79
+ name: "scriptlink",
80
+ ext: ".js",
81
+ pre: `<script type="module" src="`,
82
+ post: `"></script>`
83
+ });
84
+
85
+ // --------------------------------------------------
86
+ // Helper: absolute path to package files
87
+ // --------------------------------------------------
88
+ function packageFileAbs(filename) {
89
+ const __filename = fileURLToPath(import.meta.url);
90
+ const __dirname = dirname(__filename);
91
+ return join(__dirname, filename);
92
+ }
93
+
94
+ // --------------------------------------------------
95
+ // buldngHTMLWrapper()
96
+ // --------------------------------------------------
97
+ export function buldngHTMLWrapper(source) {
98
+ const srcWF = ensureWorkFile(source, "source");
99
+
100
+ // Compile buldng's layout.ts (lazy)
101
+ const layoutTS = new WorkFile("file", { absPath: packageFileAbs("layout.ts") }, []);
102
+ const layoutJS = compile(layoutTS);
103
+
104
+ // Save compiled layout.js to site root
105
+ layoutJS.save("layout.js");
106
+
107
+ // Save buldng.css to site root
108
+ const buldngCSS = new WorkFile("file", { absPath: packageFileAbs("buldng.css") }, []);
109
+ buldngCSS.save("buldng.css");
110
+
111
+ // Wrap project shell in wrapper.html
112
+ const wrapperWF = new WorkFile("file", { absPath: packageFileAbs("wrapper.html") }, []);
113
+ return replace(wrapperWF, "<!--CAMERA-STYLE-->", srcWF);
114
+ }
115
+
116
+ // --------------------------------------------------
117
+ // pageHTML()
118
+ // --------------------------------------------------
119
+ const _warned = {
120
+ shell: false,
121
+ projectCSS: false
122
+ };
123
+
124
+ export function pageHTML({ app, css = null, js = null, out }) {
125
+ if (!out || typeof out !== "string") {
126
+ throw new Error("pageHTML(): 'out' must be a non-empty string");
127
+ }
128
+
129
+ if (!app) {
130
+ throw new Error("pageHTML(): 'app' is required and cannot be null");
131
+ }
132
+
133
+ const appWF = ensureWorkFile(app, "app");
134
+
135
+ const shellWF = registry.get("shell") ?? null;
136
+ const projectCSS = registry.get("projectCSS") ?? null;
137
+
138
+ if (!shellWF && !_warned.shell) {
139
+ console.warn(
140
+ "pageHTML(): no 'shell' registered — building raw page HTML.\n" +
141
+ "To register a shell use: reg('shell', file`layout/shell.html`)"
142
+ );
143
+ _warned.shell = true;
144
+ }
145
+
146
+ if (!projectCSS && !_warned.projectCSS) {
147
+ console.warn(
148
+ "pageHTML(): no 'projectCSS' registered — page will not include project CSS.\n" +
149
+ "To register project CSS use: reg('projectCSS', file`layout/buldr.css`)"
150
+ );
151
+ _warned.projectCSS = true;
152
+ }
153
+
154
+ let htmlWF = shellWF
155
+ ? replace(shellWF, "<!--APP-->", appWF)
156
+ : appWF;
157
+
158
+ // Insert JS
159
+ if (js) {
160
+ const jsWF = resolveAsset(js, {
161
+ ext: ".js",
162
+ tag: "scriptlink",
163
+ wrap: scriptlink,
164
+ autoPrefix: "js"
165
+ });
166
+ htmlWF = replace(htmlWF, "<!--SCRIPTS-->", jsWF);
167
+ }
168
+
169
+ // Insert CSS (project first)
170
+ const cssWFs = [];
171
+
172
+ if (projectCSS) {
173
+ cssWFs.push(resolveAsset(projectCSS, {
174
+ ext: ".css",
175
+ tag: "csslink",
176
+ wrap: csslink,
177
+ autoPrefix: "css"
178
+ }));
179
+ console.log(`${out}: has projectCSS`);
180
+ }
181
+
182
+ if (css) {
183
+ cssWFs.push(resolveAsset(css, {
184
+ ext: ".css",
185
+ tag: "csslink",
186
+ wrap: csslink,
187
+ autoPrefix: "css"
188
+ }));
189
+ console.log(`${out}: has pageCSS`);
190
+ }
191
+
192
+ if (cssWFs.length === 1) {
193
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssWFs[0]);
194
+ } else if (cssWFs.length > 1) {
195
+ const cssListWF = append(...cssWFs);
196
+ htmlWF = replace(htmlWF, "<!--CSS-->", cssListWF);
197
+ }
198
+
199
+ return htmlWF.save(out);
200
+ }
201
+
202
+ // --------------------------------------------------
203
+ // resolveAsset()
204
+ // --------------------------------------------------
205
+ let autoCounter = 1;
206
+
207
+ function resolveAsset(input, {
208
+ ext,
209
+ tag,
210
+ wrap,
211
+ autoPrefix
212
+ }) {
213
+ if (input == null) return null;
214
+
215
+ if (typeof input === "string") {
216
+ if (!input.endsWith(ext)) {
217
+ throw new Error(`pageHTML(${ext}): expected a ${ext} filename, got '${input}'`);
218
+ }
219
+ return wrap(input);
220
+ }
221
+
222
+ if (!(input instanceof WorkFile)) {
223
+ throw new Error(`pageHTML(${ext}): expected string or WorkFile, got ${typeof input}`);
224
+ }
225
+
226
+ if (input.tag === tag) {
227
+ return input;
228
+ }
229
+
230
+ if (input.op === "literal") {
231
+ const value = input.config.value;
232
+
233
+ if (value.endsWith(ext)) {
234
+ return wrap(input);
235
+ }
236
+
237
+ throw new Error(
238
+ `pageHTML(${ext}): literal WorkFile does not end in ${ext} → '${value}'`
239
+ );
240
+ }
241
+
242
+ if (input.op === "materialise") {
243
+ const target = input.config.target;
244
+
245
+ if (!target.endsWith(ext)) {
246
+ throw new Error(
247
+ `pageHTML(${ext}): materialise target '${target}' does not end in ${ext}`
248
+ );
249
+ }
250
+
251
+ return wrap(input);
252
+ }
253
+
254
+ const autoName = `${autoPrefix}${autoCounter++}${ext}`;
255
+
256
+ console.warn(
257
+ `pageHTML(${ext}): received non-literal WorkFile; auto-materialising as ${autoName}`
258
+ );
259
+
260
+ const mat = materialise(input, autoName);
261
+ return wrap(mat);
262
+ }