@mrkt_frwd/leaf 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,16 +1,67 @@
1
1
  # @mrkt_frwd/leaf
2
2
 
3
- Take a self-contained HTML page apart into editable CSS and JS, put it back, and
4
- prove it is the same file.
3
+ **Take a self-contained HTML page apart into editable CSS and JS, put it back, and
4
+ prove it is the same bytes.**
5
5
 
6
6
  ```bash
7
- npx leaf unpack page.html --output-dir ./out
8
- npx leaf pack ./out --out page.html
9
- npx leaf check page.html
7
+ npx @mrkt_frwd/leaf unpack page.html --output-dir ./out
8
+ npx @mrkt_frwd/leaf pack ./out --out page.html
10
9
  ```
11
10
 
12
- Leaf never rebuilds the document from a parse tree — it cuts block bodies out by
13
- byte offset and puts them back. Unchanged in, unchanged out, verified against
14
- every page in the studio. `docs/getting-started.md` has the rest.
11
+ ## What it is for
15
12
 
16
- MIT.
13
+ Single-file pages are wonderful to ship and miserable to edit — one 40 KB file
14
+ with the stylesheet, the shaders and the app logic in it. Leaf splits that into
15
+ real files you can open in an editor, and puts it back together byte for byte.
16
+
17
+ ## The claim, and the evidence for it
18
+
19
+ Run against a real 22,891-byte page:
20
+
21
+ ```
22
+ $ npx @mrkt_frwd/leaf unpack engines/job.html --output-dir ./out
23
+ unpacked job.html → ./out
24
+ sha256 7d3437b28d34… 4 part(s)
25
+ parts/00-script-importmap.json 104 B
26
+ parts/01-style.css 7970 B
27
+ parts/02-script.js 2128 B
28
+ parts/03-script-module.js 3671 B
29
+
30
+ $ npx @mrkt_frwd/leaf pack ./out --out rebuilt.html
31
+ packed → rebuilt.html
32
+ sha256 7d3437b28d34… 22891 B
33
+ identical to the source it was unpacked from
34
+ ```
35
+
36
+ Same digest on both ends, confirmed independently with `shasum` and `cmp`.
37
+
38
+ ## Why byte-exact and not "close enough"
39
+
40
+ A round trip that is *nearly* right is the worst available outcome. It returns a
41
+ page that still loads, still looks plausible, and has quietly lost a newline
42
+ inside a shader or a tab inside a template literal. You find out weeks later.
43
+
44
+ So Leaf does not re-serialise a parse tree. It records where each block sat and
45
+ what surrounded it, edits nothing, and reassembles from the original shell. And
46
+ it reports the digest rather than a success message — when the bytes do not
47
+ match, it says so instead of claiming victory.
48
+
49
+ This replaced twenty-one hand-written repack scripts that each failed the same
50
+ way: nearly.
51
+
52
+ ## Commands
53
+
54
+ ```
55
+ leaf unpack <page.html> --output-dir <dir> take it apart
56
+ leaf pack <dir> [--out <page.html>] put it back, report the digest
57
+ leaf check <page.html> is it genuinely self-contained?
58
+ ```
59
+
60
+ `check` answers a different question worth asking: does this page actually fetch
61
+ nothing from another origin, or does it only look like it?
62
+
63
+ ## Requirements
64
+
65
+ Node 18+. No dependencies.
66
+
67
+ MIT © Joe Asare. Built at [Joe Asare Studio](https://joeasare.com).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrkt_frwd/leaf",
3
- "version": "0.1.0",
4
- "description": "Leaf take a self-contained HTML page apart into editable CSS and JS, put it back, and prove it is the same file.",
3
+ "version": "0.1.1",
4
+ "description": "Leaf \u2014 take a self-contained HTML page apart into editable CSS and JS, put it back, and prove it is the same file.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.mjs"
@@ -16,5 +16,13 @@
16
16
  "engines": {
17
17
  "node": ">=20"
18
18
  },
19
- "license": "MIT"
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mrktfrwd/leaf.git"
23
+ },
24
+ "homepage": "https://joeasare.com/engines/leaf",
25
+ "scripts": {
26
+ "test": "node src/test-leaf.mjs"
27
+ }
20
28
  }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Leaf's promise is narrow and total: unpack a page, change nothing, pack it, get the
4
+ * same bytes. A round trip that is *nearly* right is the worst possible outcome, so the
5
+ * tests below recompute the digest from the rebuilt file rather than trusting a
6
+ * success message the code printed about itself.
7
+ *
8
+ * node src/test-leaf.mjs
9
+ */
10
+ import assert from 'assert';
11
+ import crypto from 'crypto';
12
+ import { unpack, pack, check, findBlocks } from './index.mjs';
13
+ import fs from 'fs';
14
+ import os from 'os';
15
+ import path from 'path';
16
+
17
+ const fails = [];
18
+ const it = (n, f) => { try { f(); console.log(` ok ${n}`); } catch (e) { fails.push(n); console.log(` FAIL ${n}\n ${e.message}`); } };
19
+ const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
20
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leaf-'));
21
+
22
+ const PAGE = `<!doctype html>
23
+ <html lang="en"><head><meta charset="utf-8"><title>t</title>
24
+ <style>
25
+ body { color: red }
26
+ /* a tab and a trailing space follow: */
27
+ </style>
28
+ <script type="importmap">{"imports":{"three":"/x.js"}}</script>
29
+ </head><body><h1>hi</h1>
30
+ <script>
31
+ const s = \`a
32
+ b \`;
33
+ </script>
34
+ </body></html>`;
35
+
36
+ console.log('\n LEAF\n ----------------------------------');
37
+
38
+ it('a round trip returns the identical bytes', () => {
39
+ const src = path.join(tmp, 'p.html');
40
+ fs.writeFileSync(src, PAGE);
41
+ const out = path.join(tmp, 'out');
42
+ unpack(src, out);
43
+ const rebuilt = path.join(tmp, 'r.html');
44
+ pack(out, { outFile: rebuilt });
45
+ assert.equal(sha(fs.readFileSync(rebuilt)), sha(fs.readFileSync(src)), 'digests differ');
46
+ });
47
+
48
+ it('whitespace inside a template literal survives', () => {
49
+ const src = path.join(tmp, 'w.html');
50
+ fs.writeFileSync(src, PAGE);
51
+ const out = path.join(tmp, 'wout');
52
+ unpack(src, out);
53
+ pack(out, { outFile: path.join(tmp, 'w2.html') });
54
+ const back = fs.readFileSync(path.join(tmp, 'w2.html'), 'utf8');
55
+ assert.ok(back.includes('\`a\n\tb \`'), 'a tab or newline was normalised away');
56
+ });
57
+
58
+ it('a mutated part changes the digest — pack does not paper over it', () => {
59
+ const src = path.join(tmp, 'm.html');
60
+ fs.writeFileSync(src, PAGE);
61
+ const out = path.join(tmp, 'mout');
62
+ unpack(src, out);
63
+ const parts = fs.readdirSync(path.join(out, 'parts'));
64
+ const css = parts.find((f) => f.endsWith('.css'));
65
+ const p = path.join(out, 'parts', css);
66
+ fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace('red', 'blue'));
67
+ pack(out, { outFile: path.join(tmp, 'm2.html') });
68
+ assert.notEqual(sha(fs.readFileSync(path.join(tmp, 'm2.html'))), sha(fs.readFileSync(src)),
69
+ 'an edited part must not produce the original digest');
70
+ });
71
+
72
+ it('findBlocks reports blocks in document order', () => {
73
+ const offs = findBlocks(PAGE).map((b) => b.contentStart);
74
+ assert.deepEqual(offs, [...offs].sort((a, b) => a - b));
75
+ });
76
+
77
+ it('check names a page that fetches from another origin', () => {
78
+ const remote = PAGE.replace('<h1>hi</h1>', '<img src="https://cdn.example.com/a.png">');
79
+ const f = path.join(tmp, 'x.html');
80
+ fs.writeFileSync(f, remote);
81
+ assert.equal(check(f).selfContained, false);
82
+ assert.equal(check(path.join(tmp, 'p.html')).selfContained, true);
83
+ });
84
+
85
+ fs.rmSync(tmp, { recursive: true, force: true });
86
+ if (fails.length) { console.error(`\n ${fails.length} failure(s)\n`); process.exit(1); }
87
+ console.log('\n all leaf checks passed\n');