mnfst-run 1.0.2 → 1.0.6

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": "mnfst-run",
3
- "version": "1.0.2",
3
+ "version": "1.0.6",
4
4
  "description": "Zero-dependency dev server for Manifest projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,6 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "serve.mjs",
12
- "idiomorph.min.js",
13
12
  "README.md"
14
13
  ],
15
14
  "publishConfig": {
package/serve.mjs CHANGED
@@ -3,27 +3,32 @@
3
3
  * mnfst-run — zero-dependency dev server for Manifest projects.
4
4
  *
5
5
  * Usage:
6
- * npx mnfst-run [dir] [--port 5001]
6
+ * npx mnfst-run [dir] [--port 5001] [--idle-shutdown 30] [--no-idle-shutdown]
7
7
  *
8
- * dir Directory to serve (default: current directory). Any depth of
9
- * nesting is valid, e.g. npx mnfst-run docs/articles/publishing
10
- * --port Preferred port (default: PORT env var, then 5001). Auto-increments
11
- * if the port is already in use.
8
+ * dir Directory to serve (default: current directory). Any
9
+ * depth of nesting is valid, e.g.
10
+ * npx mnfst-run docs/articles/publishing
11
+ * --port Preferred port (default: PORT env var, then 5001).
12
+ * Auto-increments if the port is already in use.
13
+ * --idle-shutdown N Exit after N seconds with no open browser tabs
14
+ * (default 30). Only arms once a tab has connected, so
15
+ * the auto-launched browser has time to load.
16
+ * --no-idle-shutdown Disable auto-shutdown (useful in CI / headless cases
17
+ * where no browser will connect).
12
18
  *
13
19
  * SPA vs MPA is auto-detected: if the root index.html contains
14
20
  * <meta name="manifest:prerendered"> the server disables SPA fallback.
15
21
  *
16
22
  * 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
23
+ * .css → hot-swaps the matching stylesheet href (no reload, no flash)
24
+ * .csv/.json/etc. dispatches manifest:dev-reload; data plugin re-fetches local
25
+ * sources and updates Alpine store reactively (no reload)
26
+ * other → full page reload
21
27
  */
22
28
  import { createServer } from 'http';
23
29
  import { readFileSync, statSync, watch } from 'fs';
24
30
  import { join, extname, resolve, basename } from 'path';
25
31
  import { exec } from 'child_process';
26
- import { fileURLToPath } from 'url';
27
32
 
28
33
  const MIME = {
29
34
  '.html': 'text/html; charset=utf-8',
@@ -48,16 +53,11 @@ const MIME = {
48
53
  '.map': 'application/json',
49
54
  };
50
55
 
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
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 + `
57
+ // - CSS changes → hot-swaps the matching <link> href (no reload, no flash)
58
+ // - data file changes dispatches manifest:dev-reload (data plugin re-fetches)
59
+ // - other changes → full page reload
60
+ const LIVE_RELOAD_SCRIPT = `<script>
61
61
  (function () {
62
62
  var es = new EventSource('/__mnfst_sse__');
63
63
  es.onmessage = function (e) {
@@ -67,19 +67,6 @@ const LIVE_RELOAD_SCRIPT = '<script>\n' + IDIOMORPH_SRC + `
67
67
  var base = l.href.split('?')[0];
68
68
  if (base.endsWith(d.file)) l.href = base + '?t=' + Date.now();
69
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
70
  } else if (d.type === 'data') {
84
71
  window.dispatchEvent(new CustomEvent('manifest:dev-reload'));
85
72
  } else {
@@ -94,9 +81,17 @@ const LIVE_RELOAD_SCRIPT = '<script>\n' + IDIOMORPH_SRC + `
94
81
  const args = process.argv.slice(2);
95
82
  let dir = '.';
96
83
  let port = process.env.PORT ? parseInt(process.env.PORT, 10) : 5001;
84
+ // Auto-shutdown: when the last open browser tab disconnects (SSE drops to 0
85
+ // clients) and stays gone for `idleShutdownSec`, the server exits. Cancelled
86
+ // by `--no-idle-shutdown` (e.g. CI, headless smoke tests, or any case where
87
+ // no browser will ever connect).
88
+ let idleShutdownSec = 30;
89
+ let idleShutdownEnabled = true;
97
90
 
98
91
  for (let i = 0; i < args.length; i++) {
99
92
  if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) { port = parseInt(args[++i], 10); continue; }
93
+ if (args[i] === '--no-idle-shutdown') { idleShutdownEnabled = false; continue; }
94
+ if (args[i] === '--idle-shutdown' && args[i + 1]) { idleShutdownSec = parseInt(args[++i], 10); continue; }
100
95
  if (!args[i].startsWith('-')) dir = args[i];
101
96
  }
102
97
 
@@ -108,8 +103,7 @@ function detectMPA(rootDir) {
108
103
  return /name=["']manifest:prerendered["']/i.test(readFileSync(join(rootDir, 'index.html'), 'utf8'));
109
104
  } catch { return false; }
110
105
  }
111
- const spa = !detectMPA(root);
112
- const mode = spa ? 'SPA' : 'MPA';
106
+ const spa = !detectMPA(root);
113
107
 
114
108
  // --- SSE clients ---
115
109
  let clients = [];
@@ -120,6 +114,51 @@ function broadcast(data) {
120
114
  clients.forEach(res => { try { res.write(msg); } catch { /* client gone */ } });
121
115
  }
122
116
 
117
+ // --- Idle auto-shutdown ---
118
+ // `everConnected` keeps the timer dormant until at least one tab has opened —
119
+ // otherwise the server would exit before the auto-launched browser tab finishes
120
+ // loading. `idleTimer` runs only while clients.length === 0; any new SSE
121
+ // connection cancels it. The grace window also covers hard-reload churn (Cmd+R
122
+ // drops the SSE briefly, then reconnects in well under a second).
123
+ let everConnected = false;
124
+ let idleTimer = null;
125
+
126
+ function armIdleShutdown() {
127
+ if (!idleShutdownEnabled || !everConnected || idleTimer) return;
128
+ if (clients.length > 0) return;
129
+ idleTimer = setTimeout(() => {
130
+ console.log(`\nmnfst-run: no open tabs for ${idleShutdownSec}s — shutting down.\n`);
131
+ process.exit(0);
132
+ }, idleShutdownSec * 1000);
133
+ }
134
+
135
+ function cancelIdleShutdown() {
136
+ if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
137
+ }
138
+
139
+ // --- .env support ---
140
+ // Minimal dotenv parser. Skips comments/blank lines, splits on first `=`,
141
+ // trims whitespace, strips wrapping single/double quotes. No multiline values,
142
+ // no `${VAR}` substitution within .env itself — we just want plain KEY=VALUE.
143
+ function parseDotenv(text) {
144
+ const out = {};
145
+ for (const rawLine of text.split(/\r?\n/)) {
146
+ const line = rawLine.trim();
147
+ if (!line || line.startsWith('#')) continue;
148
+ const eq = line.indexOf('=');
149
+ if (eq === -1) continue;
150
+ const key = line.slice(0, eq).trim();
151
+ if (!key) continue;
152
+ let value = line.slice(eq + 1).trim();
153
+ if ((value.startsWith('"') && value.endsWith('"')) ||
154
+ (value.startsWith("'") && value.endsWith("'"))) {
155
+ value = value.slice(1, -1);
156
+ }
157
+ out[key] = value;
158
+ }
159
+ return out;
160
+ }
161
+
123
162
  // --- File watcher ---
124
163
  const IGNORE = /node_modules|\.git/;
125
164
  try {
@@ -128,10 +167,11 @@ try {
128
167
  clearTimeout(debounce);
129
168
  debounce = setTimeout(() => {
130
169
  const ext = extname(filename).toLowerCase();
131
- if (ext === '.css') {
170
+ const base = basename(filename);
171
+ if (base === '.env') {
172
+ broadcast({ type: 'reload' });
173
+ } else if (ext === '.css') {
132
174
  broadcast({ type: 'css', file: '/' + filename.replace(/\\/g, '/') });
133
- } else if (ext === '.html') {
134
- broadcast({ type: 'html' });
135
175
  } else if (['.csv', '.json', '.yaml', '.yml', '.md'].includes(ext)) {
136
176
  broadcast({ type: 'data' });
137
177
  } else {
@@ -171,6 +211,27 @@ function serveFile(res, filePath) {
171
211
  const server = createServer((req, res) => {
172
212
  const urlPath = decodeURIComponent(req.url.split('?')[0]);
173
213
 
214
+ // Virtual /env.js — generated from .env at the project root.
215
+ // Loaded by HTML before manifest.data.js so window.env is populated for
216
+ // ${VAR} interpolation in manifest.json. Returns an empty no-op if no
217
+ // .env exists, so the <script src="/env.js"> tag is always safe to include.
218
+ if (urlPath === '/env.js') {
219
+ const envPath = join(root, '.env');
220
+ let body = 'window.env = window.env || {};';
221
+ try {
222
+ if (isFile(envPath)) {
223
+ const env = parseDotenv(readFileSync(envPath, 'utf8'));
224
+ body = `window.env = Object.assign(window.env || {}, ${JSON.stringify(env)});`;
225
+ }
226
+ } catch { /* fall through to no-op */ }
227
+ res.writeHead(200, {
228
+ 'Content-Type': 'application/javascript; charset=utf-8',
229
+ 'Cache-Control': 'no-store',
230
+ });
231
+ res.end(body);
232
+ return;
233
+ }
234
+
174
235
  // SSE endpoint for live reload
175
236
  if (urlPath === '/__mnfst_sse__') {
176
237
  res.writeHead(200, {
@@ -180,7 +241,12 @@ const server = createServer((req, res) => {
180
241
  });
181
242
  res.write(':\n\n'); // initial keep-alive comment
182
243
  clients.push(res);
183
- req.on('close', () => { clients = clients.filter(c => c !== res); });
244
+ everConnected = true;
245
+ cancelIdleShutdown();
246
+ req.on('close', () => {
247
+ clients = clients.filter(c => c !== res);
248
+ if (clients.length === 0) armIdleShutdown();
249
+ });
184
250
  return;
185
251
  }
186
252
 
@@ -215,15 +281,25 @@ function tryListen(p, attempt = 0) {
215
281
  console.error('mnfst-run: could not find a free port after 20 attempts.');
216
282
  process.exit(1);
217
283
  }
218
- server.once('error', err => {
219
- if (err.code === 'EADDRINUSE') tryListen(p + 1, attempt + 1);
220
- else throw err;
221
- });
222
- server.listen(p, () => {
284
+ // Use explicit listeners so we can remove the pending 'listening' handler
285
+ // when retrying otherwise each failed attempt leaves a once('listening')
286
+ // handler registered, and the eventual successful listen fires ALL of them,
287
+ // opening a browser tab for every port that was tried (including ports
288
+ // already taken by other projects).
289
+ const onListening = () => {
290
+ server.removeListener('error', onError);
223
291
  const url = `http://localhost:${p}`;
224
292
  console.log(`\n${label} running at ${url}\n`);
225
293
  openBrowser(url);
226
- });
294
+ };
295
+ const onError = err => {
296
+ server.removeListener('listening', onListening);
297
+ if (err.code === 'EADDRINUSE') tryListen(p + 1, attempt + 1);
298
+ else throw err;
299
+ };
300
+ server.once('listening', onListening);
301
+ server.once('error', onError);
302
+ server.listen(p);
227
303
  }
228
304
 
229
305
  tryListen(port);
package/idiomorph.min.js DELETED
@@ -1 +0,0 @@
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}}();