mnfst-run 1.0.0 → 1.0.2
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/idiomorph.min.js +1 -0
- package/package.json +3 -2
- package/serve.mjs +61 -17
package/idiomorph.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var Idiomorph=function(){"use strict";const e=()=>{};const n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:"merge",shouldPreserve:e=>e.getAttribute("im-preserve")==="true",shouldReAppend:e=>e.getAttribute("im-re-append")==="true",shouldRemove:e,afterHeadMorphed:e},restoreFocus:true};function t(t,e,n={}){t=d(t);const r=f(e);const i=u(t,r,n);const o=a(i,()=>{return c(i,t,r,e=>{if(e.morphStyle==="innerHTML"){s(e,t,r);return Array.from(t.childNodes)}else{return l(e,t,r)}})});i.pantry.remove();return o}function l(e,t,n){const r=f(t);s(e,r,n,t,t.nextSibling);return Array.from(r.childNodes)}function a(e,t){if(!e.config.restoreFocus)return t();let n=document.activeElement;if(!(n instanceof HTMLInputElement||n instanceof HTMLTextAreaElement)){return t()}const{id:r,selectionStart:i,selectionEnd:o}=n;const l=t();if(r&&r!==document.activeElement?.getAttribute("id")){n=e.target.querySelector(`[id="${r}"]`);n?.focus()}if(n&&!n.selectionEnd&&o){n.setSelectionRange(i,o)}return l}const s=function(){function e(e,t,n,r=null,i=null){if(t instanceof HTMLTemplateElement&&n instanceof HTMLTemplateElement){t=t.content;n=n.content}r||=t.firstChild;for(const o of n.childNodes){if(r&&r!=i){const a=f(e,o,r,i);if(a){if(a!==r){h(e,r,a)}b(a,o,e);r=a.nextSibling;continue}}if(o instanceof Element){const s=o.getAttribute("id");if(e.persistentIds.has(s)){const c=p(t,s,r,e);b(c,o,e);r=c.nextSibling;continue}}const l=d(t,o,r,e);if(l){r=l.nextSibling}}while(r&&r!=i){const u=r;r=r.nextSibling;m(e,u)}}function d(e,t,n,r){if(r.callbacks.beforeNodeAdded(t)===false)return null;if(r.idMap.has(t)){const i=document.createElement(t.tagName);e.insertBefore(i,n);b(i,t,r);r.callbacks.afterNodeAdded(i);return i}else{const o=document.importNode(t,true);e.insertBefore(o,n);r.callbacks.afterNodeAdded(o);return o}}const f=function(){function e(e,t,n,r){let i=null;let o=t.nextSibling;let l=0;let a=n;while(a&&a!=r){if(c(a,t)){if(s(e,a,t)){return a}if(i===null){if(!e.idMap.has(a)){i=a}}}if(i===null&&o&&c(a,o)){l++;o=o.nextSibling;if(l>=2){i=undefined}}if(e.activeElementAndParents.includes(a))break;a=a.nextSibling}return i||null}function s(e,t,n){let r=e.idMap.get(t);let i=e.idMap.get(n);if(!i||!r)return false;for(const o of r){if(i.has(o)){return true}}return false}function c(e,t){const n=e;const r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.getAttribute?.("id")||n.getAttribute?.("id")===r.getAttribute?.("id"))}return e}();function m(e,t){if(e.idMap.has(t)){l(e.pantry,t,null)}else{if(e.callbacks.beforeNodeRemoved(t)===false)return;t.parentNode?.removeChild(t);e.callbacks.afterNodeRemoved(t)}}function h(t,e,n){let r=e;while(r&&r!==n){let e=r;r=r.nextSibling;m(t,e)}return r}function p(e,t,n,r){const i=r.target.getAttribute?.("id")===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);o(i,r);l(e,i,n);return i}function o(t,n){const r=t.getAttribute("id");while(t=t.parentNode){let e=n.idMap.get(t);if(e){e.delete(r);if(!e.size){n.idMap.delete(t)}}}}function l(t,n,r){if(t.moveBefore){try{t.moveBefore(n,r)}catch(e){t.insertBefore(n,r)}}else{t.insertBefore(n,r)}}return e}();const b=function(){function e(e,t,n){if(n.ignoreActive&&e===document.activeElement){return null}if(n.callbacks.beforeNodeMorphed(e,t)===false){return e}if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){m(e,t,n)}else{r(e,t,n);if(!f(e,n)){s(n,e,t)}}n.callbacks.afterNodeMorphed(e,t);return e}function r(e,t,n){let r=t.nodeType;if(r===1){const i=e;const o=t;const l=i.attributes;const a=o.attributes;for(const s of a){if(d(s.name,i,"update",n)){continue}if(i.getAttribute(s.name)!==s.value){i.setAttribute(s.name,s.value)}}for(let e=l.length-1;0<=e;e--){const c=l[e];if(!c)continue;if(!o.hasAttribute(c.name)){if(d(c.name,i,"remove",n)){continue}i.removeAttribute(c.name)}}if(!f(i,n)){u(i,o,n)}}if(r===8||r===3){if(e.nodeValue!==t.nodeValue){e.nodeValue=t.nodeValue}}}function u(n,r,i){if(n instanceof HTMLInputElement&&r instanceof HTMLInputElement&&r.type!=="file"){let e=r.value;let t=n.value;o(n,r,"checked",i);o(n,r,"disabled",i);if(!r.hasAttribute("value")){if(!d("value",n,"remove",i)){n.value="";n.removeAttribute("value")}}else if(t!==e){if(!d("value",n,"update",i)){n.setAttribute("value",e);n.value=e}}}else if(n instanceof HTMLOptionElement&&r instanceof HTMLOptionElement){o(n,r,"selected",i)}else if(n instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value;let t=n.value;if(d("value",n,"update",i)){return}if(e!==t){n.value=e}if(n.firstChild&&n.firstChild.nodeValue!==e){n.firstChild.nodeValue=e}}}function o(e,t,n,r){const i=t[n],o=e[n];if(i!==o){const l=d(n,e,"update",r);if(!l){e[n]=t[n]}if(i){if(!l){e.setAttribute(n,"")}}else{if(!d(n,e,"remove",r)){e.removeAttribute(n)}}}}function d(e,t,n,r){if(e==="value"&&r.ignoreActiveValue&&t===document.activeElement){return true}return r.callbacks.beforeAttributeUpdated(e,t,n)===false}function f(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return e}();function c(t,e,n,r){if(t.head.block){const i=e.querySelector("head");const o=n.querySelector("head");if(i&&o){const l=m(i,o,t);return Promise.all(l).then(()=>{const e=Object.assign(t,{head:{block:false,ignore:true}});return r(e)})}}return r(t)}function m(e,t,r){let i=[];let o=[];let l=[];let a=[];let s=new Map;for(const n of t.children){s.set(n.outerHTML,n)}for(const u of e.children){let e=s.has(u.outerHTML);let t=r.head.shouldReAppend(u);let n=r.head.shouldPreserve(u);if(e||n){if(t){o.push(u)}else{s.delete(u.outerHTML);l.push(u)}}else{if(r.head.style==="append"){if(t){o.push(u);a.push(u)}}else{if(r.head.shouldRemove(u)!==false){o.push(u)}}}}a.push(...s.values());let c=[];for(const d of a){let n=document.createRange().createContextualFragment(d.outerHTML).firstChild;if(r.callbacks.beforeNodeAdded(n)!==false){if("href"in n&&n.href||"src"in n&&n.src){let t;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});c.push(e)}e.appendChild(n);r.callbacks.afterNodeAdded(n);i.push(n)}}for(const f of o){if(r.callbacks.beforeNodeRemoved(f)!==false){e.removeChild(f);r.callbacks.afterNodeRemoved(f)}}r.head.afterHeadMorphed(e,{added:i,kept:l,removed:o});return c}const u=function(){function e(e,t,n){const{persistentIds:r,idMap:i}=f(e,t);const o=a(n);const l=o.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(l)){throw`Do not understand how to morph style ${l}`}return{target:e,newContent:t,config:o,morphStyle:l,ignoreActive:o.ignoreActive,ignoreActiveValue:o.ignoreActiveValue,restoreFocus:o.restoreFocus,idMap:i,persistentIds:r,pantry:s(),activeElementAndParents:c(e),callbacks:o.callbacks,head:o.head}}function a(e){let t=Object.assign({},n);Object.assign(t,e);t.callbacks=Object.assign({},n.callbacks,e.callbacks);t.head=Object.assign({},n.head,e.head);return t}function s(){const e=document.createElement("div");e.hidden=true;document.body.insertAdjacentElement("afterend",e);return e}function c(e){let t=[];let n=document.activeElement;if(n?.tagName!=="BODY"&&e.contains(n)){while(n){t.push(n);if(n===e)break;n=n.parentElement}}return t}function u(e){let t=Array.from(e.querySelectorAll("[id]"));if(e.getAttribute?.("id")){t.push(e)}return t}function d(n,e,r,t){for(const i of t){const o=i.getAttribute("id");if(e.has(o)){let t=i;while(t){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(o);if(t===r)break;t=t.parentElement}}}}function f(e,t){const n=u(e);const r=u(t);const i=m(n,r);let o=new Map;d(o,i,e,n);const l=t.__idiomorphRoot||t;d(o,i,l,r);return{persistentIds:i,idMap:o}}function m(e,t){let n=new Set;let r=new Map;for(const{id:o,tagName:l}of e){if(r.has(o)){n.add(o)}else{r.set(o,l)}}let i=new Set;for(const{id:o,tagName:l}of t){if(i.has(o)){n.add(o)}else if(r.get(o)===l){i.add(o)}}for(const o of n){i.delete(o)}return i}return e}();const{normalizeElement:d,normalizeParent:f}=function(){const i=new WeakSet;function e(e){if(e instanceof Document){return e.documentElement}else{return e}}function r(e){if(e==null){return document.createElement("div")}else if(typeof e==="string"){return r(l(e))}else if(i.has(e)){return e}else if(e instanceof Node){if(e.parentNode){return new o(e)}else{const t=document.createElement("div");t.append(e);return t}}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}class o{constructor(e){this.originalNode=e;this.realParentNode=e.parentNode;this.previousSibling=e.previousSibling;this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;while(t&&t!=this.nextSibling){e.push(t);t=t.nextSibling}return e}querySelectorAll(r){return this.childNodes.reduce((t,e)=>{if(e instanceof Element){if(e.matches(r))t.push(e);const n=e.querySelectorAll(r);for(let e=0;e<n.length;e++){t.push(n[e])}}return t},[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}function l(n){let r=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=r.parseFromString(n,"text/html");if(e.match(/<\/html>/)){i.add(t);return t}else{let e=t.firstChild;if(e){i.add(e)}return e}}else{let e=r.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;i.add(t);return t}}return{normalizeElement:e,normalizeParent:r}}();return{morph:t,defaults:n}}();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mnfst-run",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Zero-dependency dev server for Manifest projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"serve.mjs",
|
|
12
|
+
"idiomorph.min.js",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"publishConfig": {
|
|
@@ -27,4 +28,4 @@
|
|
|
27
28
|
"url": "git+https://github.com/andrewmatlock/Manifest.git",
|
|
28
29
|
"directory": "packages/run"
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
|
+
}
|
package/serve.mjs
CHANGED
|
@@ -13,12 +13,17 @@
|
|
|
13
13
|
* SPA vs MPA is auto-detected: if the root index.html contains
|
|
14
14
|
* <meta name="manifest:prerendered"> the server disables SPA fallback.
|
|
15
15
|
*
|
|
16
|
-
* Live reload:
|
|
17
|
-
*
|
|
16
|
+
* Live reload:
|
|
17
|
+
* .css → hot-swaps the matching stylesheet href (no reload, no flash)
|
|
18
|
+
* .html → morphs changed nodes into the live DOM via idiomorph (no reload,
|
|
19
|
+
* Alpine state on unchanged nodes is preserved)
|
|
20
|
+
* other → full page reload
|
|
18
21
|
*/
|
|
19
|
-
import { createServer }
|
|
22
|
+
import { createServer } from 'http';
|
|
20
23
|
import { readFileSync, statSync, watch } from 'fs';
|
|
21
|
-
import { join, extname, resolve }
|
|
24
|
+
import { join, extname, resolve, basename } from 'path';
|
|
25
|
+
import { exec } from 'child_process';
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
22
27
|
|
|
23
28
|
const MIME = {
|
|
24
29
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -43,9 +48,16 @@ const MIME = {
|
|
|
43
48
|
'.map': 'application/json',
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
const
|
|
51
|
+
// Read idiomorph from its own file to avoid backtick/template-literal escaping issues.
|
|
52
|
+
// idiomorph.min.js ships alongside serve.mjs in the mnfst-run package.
|
|
53
|
+
const __dir = fileURLToPath(new URL('.', import.meta.url));
|
|
54
|
+
const IDIOMORPH_SRC = readFileSync(join(__dir, 'idiomorph.min.js'), 'utf8');
|
|
55
|
+
|
|
56
|
+
// Built once at startup. Injected only into full HTML documents (not fragments).
|
|
57
|
+
// - CSS changes → hot-swaps the matching <link> href (no reload, no flash)
|
|
58
|
+
// - HTML changes → morphs only changed nodes via idiomorph; Alpine state survives
|
|
59
|
+
// - other changes → full page reload
|
|
60
|
+
const LIVE_RELOAD_SCRIPT = '<script>\n' + IDIOMORPH_SRC + `
|
|
49
61
|
(function () {
|
|
50
62
|
var es = new EventSource('/__mnfst_sse__');
|
|
51
63
|
es.onmessage = function (e) {
|
|
@@ -55,13 +67,28 @@ const LIVE_RELOAD_SCRIPT = `<script>
|
|
|
55
67
|
var base = l.href.split('?')[0];
|
|
56
68
|
if (base.endsWith(d.file)) l.href = base + '?t=' + Date.now();
|
|
57
69
|
});
|
|
70
|
+
} else if (d.type === 'html') {
|
|
71
|
+
fetch(location.href, { cache: 'no-store' })
|
|
72
|
+
.then(function (r) { return r.text(); })
|
|
73
|
+
.then(function (html) {
|
|
74
|
+
var newDoc = new DOMParser().parseFromString(html, 'text/html');
|
|
75
|
+
Idiomorph.morph(document.documentElement, newDoc.documentElement, {
|
|
76
|
+
callbacks: {
|
|
77
|
+
afterNodeAdded: function (node) {
|
|
78
|
+
if (node.nodeType === 1 && window.Alpine) Alpine.initTree(node);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
} else if (d.type === 'data') {
|
|
84
|
+
window.dispatchEvent(new CustomEvent('manifest:dev-reload'));
|
|
58
85
|
} else {
|
|
59
86
|
location.reload();
|
|
60
87
|
}
|
|
61
88
|
};
|
|
62
89
|
es.onerror = function () { es.close(); };
|
|
63
90
|
})();
|
|
64
|
-
|
|
91
|
+
\x3c/script>`;
|
|
65
92
|
|
|
66
93
|
// --- CLI args ---
|
|
67
94
|
const args = process.argv.slice(2);
|
|
@@ -103,6 +130,10 @@ try {
|
|
|
103
130
|
const ext = extname(filename).toLowerCase();
|
|
104
131
|
if (ext === '.css') {
|
|
105
132
|
broadcast({ type: 'css', file: '/' + filename.replace(/\\/g, '/') });
|
|
133
|
+
} else if (ext === '.html') {
|
|
134
|
+
broadcast({ type: 'html' });
|
|
135
|
+
} else if (['.csv', '.json', '.yaml', '.yml', '.md'].includes(ext)) {
|
|
136
|
+
broadcast({ type: 'data' });
|
|
106
137
|
} else {
|
|
107
138
|
broadcast({ type: 'reload' });
|
|
108
139
|
}
|
|
@@ -122,12 +153,15 @@ function serveFile(res, filePath) {
|
|
|
122
153
|
const mime = MIME[ext] || 'application/octet-stream';
|
|
123
154
|
let body = readFileSync(filePath);
|
|
124
155
|
if (ext === '.html') {
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
const html = body.toString('utf8');
|
|
157
|
+
// Only inject into full HTML documents — not component fragments
|
|
158
|
+
const isFullDoc = /<!doctype\s/i.test(html) || /<html[\s>]/i.test(html);
|
|
159
|
+
if (isFullDoc) {
|
|
160
|
+
const injected = html.includes('</body>')
|
|
161
|
+
? html.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>')
|
|
162
|
+
: html + LIVE_RELOAD_SCRIPT;
|
|
163
|
+
body = Buffer.from(injected, 'utf8');
|
|
164
|
+
}
|
|
131
165
|
}
|
|
132
166
|
res.writeHead(200, { 'Content-Type': mime });
|
|
133
167
|
res.end(body);
|
|
@@ -166,6 +200,16 @@ const server = createServer((req, res) => {
|
|
|
166
200
|
});
|
|
167
201
|
|
|
168
202
|
// --- Auto-port ---
|
|
203
|
+
// Human-readable label: the dir arg as given, or the cwd folder name if serving '.'
|
|
204
|
+
const label = dir === '.' ? basename(process.cwd()) : dir.replace(/\\/g, '/');
|
|
205
|
+
|
|
206
|
+
function openBrowser(url) {
|
|
207
|
+
const cmd = process.platform === 'win32' ? `start ${url}`
|
|
208
|
+
: process.platform === 'darwin' ? `open ${url}`
|
|
209
|
+
: `xdg-open ${url}`;
|
|
210
|
+
exec(cmd);
|
|
211
|
+
}
|
|
212
|
+
|
|
169
213
|
function tryListen(p, attempt = 0) {
|
|
170
214
|
if (attempt > 20) {
|
|
171
215
|
console.error('mnfst-run: could not find a free port after 20 attempts.');
|
|
@@ -176,9 +220,9 @@ function tryListen(p, attempt = 0) {
|
|
|
176
220
|
else throw err;
|
|
177
221
|
});
|
|
178
222
|
server.listen(p, () => {
|
|
179
|
-
|
|
180
|
-
console.log(`\n
|
|
181
|
-
|
|
223
|
+
const url = `http://localhost:${p}`;
|
|
224
|
+
console.log(`\n${label} running at ${url}\n`);
|
|
225
|
+
openBrowser(url);
|
|
182
226
|
});
|
|
183
227
|
}
|
|
184
228
|
|