@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 +60 -9
- package/package.json +11 -3
- package/src/test-leaf.mjs +87 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Leaf
|
|
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');
|