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.
- package/package.json +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.
|
|
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/
|
|
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
|
-
|
|
736
|
-
|
|
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
|
|
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}` };
|