mnfst-run 1.0.23 → 1.0.24

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/serve.mjs +80 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-run",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Zero-dependency dev server for Manifest projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/andrewmatlock/Manifest.git",
27
+ "url": "git+https://github.com/Manifest-X/Manifest.git",
28
28
  "directory": "packages/run"
29
29
  }
30
30
  }
package/serve.mjs CHANGED
@@ -731,35 +731,97 @@ function serverStaticKey(html, child) { // mirror the client's staticKey (tag
731
731
  const text = html.slice(child.innerStart, child.innerEnd).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim().slice(0, 24);
732
732
  return child.tag.toUpperCase() + ':' + text;
733
733
  }
734
- function writeStaticOps(file, key, edits, order) {
735
- for (const ed of (edits || [])) { // re-read+re-locate each (offsets shift after writes)
736
- let html = readFileSync(file, 'utf8');
737
- const loc = locateEditEl(html, key); if (!loc) return { region: key, status: 'error', reason: `x-edit="${key}" not found in ${basename(file)}` };
738
- const node = ed.path === '' ? { tagStart: loc.openStart, openEnd: loc.openEnd, innerStart: loc.innerStart, innerEnd: loc.innerEnd } : navInRegion(html, loc, ed.path);
739
- if (!node) continue;
740
- const openTag = html.slice(node.tagStart, node.openEnd + 1);
741
- if (ed.prop === 'text') html = html.slice(0, node.innerStart) + ed.value + html.slice(node.innerEnd);
742
- else if (ed.prop === 'class') html = html.slice(0, node.tagStart) + setAttr(openTag, 'class', ed.value) + html.slice(node.openEnd + 1);
743
- else if (ed.prop === 'style') html = html.slice(0, node.tagStart) + setAttr(openTag, 'style', ed.value) + html.slice(node.openEnd + 1);
744
- writeFileSync(file, html);
745
- }
734
+ function writeStaticOps(file, key, edits, order, aliases) {
735
+ // Order FIRST: keys match on authored content (edits haven't rewritten it yet) and
736
+ // edit paths are current DOM positions, valid only once the file order matches.
746
737
  let reordered = false;
747
738
  if (order && order.length) {
748
739
  let html = readFileSync(file, 'utf8'); const loc = locateEditEl(html, key);
749
740
  if (loc) {
750
741
  const kids = []; let n = 0, c;
751
742
  while ((c = childAt(html, loc.innerStart, loc.innerEnd, n++))) kids.push(c);
752
- const byKey = {}; kids.forEach(k => { byKey[serverStaticKey(html, k)] = k; });
743
+ const keys = dedupedKeys(html, kids); // ordinal-deduped, mirroring the client
744
+ // A block edited BEFORE its move carries a live key derived from the EDITED text;
745
+ // the file holds the authored text, so file keys can't resolve it and the whole
746
+ // order would be skipped. writeStaticMove recorded (live toKey → inserted slice):
747
+ // bind those entries to their exact inserted child, then key the remaining kids
748
+ // counting the live base the client's rekey counted (twins keep their ordinals).
749
+ const bound = new Map();
750
+ for (const a of (aliases || [])) {
751
+ if (!order.includes(a.key) || keys.includes(a.key)) continue; // unedited arrival: file keys already match
752
+ const kid = kids.find(k => !bound.has(k) && html.slice(k.tagStart, k.closeEnd) === a.slice);
753
+ if (kid) bound.set(kid, a.key);
754
+ }
755
+ const byKey = {};
756
+ if (bound.size) {
757
+ const seen = Object.create(null);
758
+ kids.forEach(k => {
759
+ const live = bound.get(k), base = live ? ((/^([\s\S]*)#\d+$/.exec(live) || [, live])[1]) : serverStaticKey(html, k); // ordinal = last #digits suffix, matching the client
760
+ const d = seen[base] = (seen[base] || 0) + 1;
761
+ byKey[live || (d > 1 ? base + '#' + d : base)] = k;
762
+ });
763
+ } else kids.forEach((k, i) => { byKey[keys[i]] = k; });
753
764
  const seq = order.map(k => byKey[k]).filter(Boolean);
754
- if (seq.length === kids.length && seq.length) {
765
+ if (seq.length === kids.length && seq.length && seq.some((k, i) => k !== kids[i])) { // skip when already in order
755
766
  const reassembled = seq.map(k => html.slice(k.tagStart, k.closeEnd)).join('\n ');
756
767
  html = html.slice(0, kids[0].tagStart) + reassembled + html.slice(kids[kids.length - 1].closeEnd);
757
768
  writeFileSync(file, html); reordered = true;
758
769
  }
759
770
  }
760
771
  }
772
+ for (const ed of (edits || [])) { // re-read+re-locate each (offsets shift after writes)
773
+ let html = readFileSync(file, 'utf8');
774
+ const loc = locateEditEl(html, key); if (!loc) return { region: key, status: 'error', reason: `x-edit="${key}" not found in ${basename(file)}` };
775
+ const node = ed.path === '' ? { tagStart: loc.openStart, openEnd: loc.openEnd, innerStart: loc.innerStart, innerEnd: loc.innerEnd } : navInRegion(html, loc, ed.path);
776
+ if (!node) continue;
777
+ const openTag = html.slice(node.tagStart, node.openEnd + 1);
778
+ if (ed.prop === 'text') html = html.slice(0, node.innerStart) + ed.value + html.slice(node.innerEnd);
779
+ else if (ed.prop === 'class') html = html.slice(0, node.tagStart) + setAttr(openTag, 'class', ed.value) + html.slice(node.openEnd + 1);
780
+ else if (ed.prop === 'style') html = html.slice(0, node.tagStart) + setAttr(openTag, 'style', ed.value) + html.slice(node.openEnd + 1);
781
+ writeFileSync(file, html);
782
+ }
761
783
  return { region: key, status: 'written', file: basename(file), edits: (edits || []).length, reordered };
762
784
  }
785
+ // Cross-region move: lift a child's authored markup out of one x-edit region and
786
+ // splice it into another (same file). Keys mirror the client's staticKeys (ordinal-deduped).
787
+ function regionChildren(html, loc) {
788
+ const kids = []; let n = 0, c;
789
+ while ((c = childAt(html, loc.innerStart, loc.innerEnd, n++))) kids.push(c);
790
+ return kids;
791
+ }
792
+ function dedupedKeys(html, kids) {
793
+ const seen = Object.create(null);
794
+ return kids.map(k => { const base = serverStaticKey(html, k); const n = seen[base] = (seen[base] || 0) + 1; return n > 1 ? base + '#' + n : base; });
795
+ }
796
+ function writeStaticMove(file, p, aliases) {
797
+ let html = readFileSync(file, 'utf8');
798
+ const fromLoc = locateEditEl(html, p.from);
799
+ if (!fromLoc) return { region: p.from, status: 'error', reason: `x-edit="${p.from}" not found in ${basename(file)}` };
800
+ if (!locateEditEl(html, p.to)) return { region: p.to, status: 'error', reason: `x-edit="${p.to}" not found in ${basename(file)}` };
801
+ const kids = regionChildren(html, fromLoc);
802
+ const i = dedupedKeys(html, kids).indexOf(p.key);
803
+ if (i < 0) return { region: p.from, status: 'error', reason: `child '${p.key}' not found in region '${p.from}'` };
804
+ const child = kids[i], slice = html.slice(child.tagStart, child.closeEnd);
805
+ let remStart = child.tagStart;
806
+ const pre = html.slice(0, remStart), nl = pre.lastIndexOf('\n');
807
+ if (nl >= 0 && /^[ \t]*$/.test(pre.slice(nl + 1))) remStart = nl; // take the indent + newline with it
808
+ html = html.slice(0, remStart) + html.slice(child.closeEnd);
809
+ const toLoc = locateEditEl(html, p.to); // re-locate: offsets shifted
810
+ if (!toLoc) return { region: p.to, status: 'error', reason: `x-edit="${p.to}" not found after removal` };
811
+ const toKids = regionChildren(html, toLoc);
812
+ // Positional placement: toOrder minus the arrival is the destination's children, so
813
+ // the arrival's index in toOrder is where it goes — robust against ordinal re-keys
814
+ // (a collision renames the twin, so key matching would miss). Any residual order
815
+ // drift is settled by the region's own order patch, which follows the move.
816
+ const at = (p.toOrder || []).indexOf(p.toKey);
817
+ let insertAt = toLoc.innerEnd; // default: end of region
818
+ if (at >= 0 && at < toKids.length) insertAt = toKids[at].tagStart;
819
+ const head = html.slice(0, insertAt), hnl = head.lastIndexOf('\n');
820
+ const indent = hnl >= 0 && /^[ \t]*$/.test(head.slice(hnl + 1)) ? head.slice(hnl + 1) : '';
821
+ writeFileSync(file, head + slice + '\n' + indent + html.slice(insertAt));
822
+ if (aliases) (aliases[p.to] = aliases[p.to] || []).push({ key: p.toKey, slice }); // the region's order patch may address this child by its LIVE key
823
+ return { region: `${p.from} → ${p.to}`, status: 'written', file: basename(file), key: p.key };
824
+ }
763
825
  function writeStaticRegion(file, key, innerHTML, style) {
764
826
  let html = readFileSync(file, 'utf8');
765
827
  const loc = locateEditEl(html, key);
@@ -784,6 +846,7 @@ function childAt(html, from, to, idx) { // nth element child within [f
784
846
  const selfClose = html[openEnd - 1] === '/' || VOID_TAGS.has(tag.toLowerCase());
785
847
  let innerStart = openEnd + 1, innerEnd = innerStart, closeEnd = openEnd + 1;
786
848
  if (!selfClose) { const cl = matchingClose(html, tag, innerStart); if (!cl) return null; innerEnd = cl.start; closeEnd = cl.end; }
849
+ if (/\sdata-edit-(handle|ghost)[\s=/>]/.test(html.slice(lt, openEnd + 1))) { i = closeEnd; continue; } // plugin-injected, not an authored child (mirrors the client's realChildren)
787
850
  if (count === idx) return { tag, tagStart: lt, openEnd, innerStart, innerEnd, closeEnd };
788
851
  count++; i = closeEnd;
789
852
  }
@@ -1184,6 +1247,7 @@ const server = createServer((req, res) => {
1184
1247
  if (!Array.isArray(patches)) patches = [patches];
1185
1248
  let manifest = {}; try { manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8')); } catch {}
1186
1249
  const indexFile = join(root, 'index.html');
1250
+ const moveAliases = {}; // region → [{key: live toKey, slice}] from this batch's moves
1187
1251
  const results = patches.map(p => {
1188
1252
  try {
1189
1253
  if (p.kind === 'data') {
@@ -1195,7 +1259,8 @@ const server = createServer((req, res) => {
1195
1259
  return { region: p.region, status: reorderDataFile(file, p.order) ? 'written' : 'noop', file: rel };
1196
1260
  }
1197
1261
  if (p.kind === 'data-val') return writeDataValue(p, manifest);
1198
- if (p.kind === 'static') return writeStaticOps(indexFile, p.region, p.edits, p.order);
1262
+ if (p.kind === 'static') return writeStaticOps(indexFile, p.region, p.edits, p.order, moveAliases[p.region]);
1263
+ if (p.kind === 'static-move') return writeStaticMove(indexFile, p, moveAliases);
1199
1264
  if (p.kind === 'component') return writeComponentEdits(p, manifest);
1200
1265
  if (p.kind === 'theme') return writeThemeVar(p);
1201
1266
  return { region: p.region, status: 'skipped', reason: `unknown kind ${p.kind}` };