mnfst-run 1.0.25 → 1.0.27
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 +1 -1
- package/serve.mjs +44 -12
package/package.json
CHANGED
package/serve.mjs
CHANGED
|
@@ -382,6 +382,8 @@ const APPWRITE_PROXY_TARGET = (process.env.APPWRITE_PROXY_TARGET || privateEnv.A
|
|
|
382
382
|
const APPWRITE_PROXY_PREFIX = '/_appwrite';
|
|
383
383
|
|
|
384
384
|
let envInjectScript = buildEnvInjectScript(publicEnv);
|
|
385
|
+
// Edit mode marker: the edit plugin reads /__edit/base only when it is served
|
|
386
|
+
if (EDIT_ENABLED) envInjectScript += '<script>window.__mnfstEdit=true;</script>';
|
|
385
387
|
if (APPWRITE_PROXY_TARGET) {
|
|
386
388
|
// Point the Appwrite endpoint at the same-origin proxy. Computed in the browser
|
|
387
389
|
// from location.origin so it's port-independent (auto-port safe) and prod-safe
|
|
@@ -792,11 +794,15 @@ function parseCsvLine(line) {
|
|
|
792
794
|
out.push(cur); return out;
|
|
793
795
|
}
|
|
794
796
|
const csvCell = (v) => /[",\n]/.test(v) ? '"' + String(v).replace(/"/g, '""') + '"' : String(v);
|
|
797
|
+
// Why a data source is not a file mnfst-run writes.
|
|
798
|
+
const dataSourceRefusal = (name, manifest) => (manifest.data || {})[name] == null
|
|
799
|
+
? `data source '${name}' is not declared in manifest.json: mnfst-run writes data only to CSV/JSON/YAML files declared there`
|
|
800
|
+
: `data source '${name}' is not a file (a cloud or remote source): save it with $x.${name}.$update, not mnfst-run`;
|
|
795
801
|
// Data-value write-back (local CSV/JSON cell). Cloud sources are not files → caller should
|
|
796
802
|
// persist via $x.<source>.$update(id,{field}) instead.
|
|
797
803
|
function writeDataValue(p, manifest) {
|
|
798
804
|
const src = (manifest.data || {})[p.source], rel = typeof src === 'string' ? src : null;
|
|
799
|
-
if (!rel) return { kind: 'data-val', source: p.source, status: '
|
|
805
|
+
if (!rel) return { kind: 'data-val', source: p.source, status: 'error', reason: dataSourceRefusal(p.source, manifest) };
|
|
800
806
|
const file = resolveRel(rel); if (!file || !isFile(file)) return { kind: 'data-val', source: p.source, status: 'error', reason: `file not found: ${rel}` };
|
|
801
807
|
const refused = writeRefusal(file, 'data'); if (refused) return { kind: 'data-val', source: p.source, status: 'error', reason: `refused: ${refused}` };
|
|
802
808
|
const ext = extname(file).toLowerCase(), text = vread(file);
|
|
@@ -1085,7 +1091,7 @@ const SAN_DROP = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link'
|
|
|
1085
1091
|
// The one attribute Rich Text keeps on each of these inline tags.
|
|
1086
1092
|
const TEXT_ATTR = { abbr: 'title', dfn: 'title', bdo: 'dir', data: 'value', time: 'datetime' };
|
|
1087
1093
|
function textAttrOk(tag, n, v) {
|
|
1088
|
-
if (tag === 'a' && n === 'href') return !attrRefusal(n, v, tag);
|
|
1094
|
+
if (tag === 'a' && (n === 'href' || n === 'title')) return !attrRefusal(n, v, tag);
|
|
1089
1095
|
if (tag === 'img' && (n === 'alt' || n === 'src')) return !attrRefusal(n, v, tag);
|
|
1090
1096
|
if (TEXT_ATTR[tag] === n) return n === 'dir' ? /^(ltr|rtl|auto)$/i.test(v) : !attrRefusal(n, v, tag);
|
|
1091
1097
|
if (tag === 'input') return n === 'type' ? /^checkbox$/i.test(v) : n === 'checked';
|
|
@@ -1115,7 +1121,10 @@ function sanitizeHTML(html) {
|
|
|
1115
1121
|
if (!SAN_OK.has(name)) continue;
|
|
1116
1122
|
if (closing) { out += `</${name}>`; continue; }
|
|
1117
1123
|
let tag = '<' + name;
|
|
1124
|
+
const seen = new Set();
|
|
1118
1125
|
for (const a of scanTag(s.slice(lt, end + 1)).attrs) {
|
|
1126
|
+
if (seen.has(a.lower)) continue; // the first of a repeated attribute wins, as in the browser
|
|
1127
|
+
seen.add(a.lower);
|
|
1119
1128
|
const v = a.value == null ? '' : decodeEntities(a.value, true);
|
|
1120
1129
|
if (textAttrOk(name, a.lower, v)) tag += a.lower === 'checked' ? ' checked' : ` ${a.lower}="${escAttr(v)}"`;
|
|
1121
1130
|
}
|
|
@@ -1545,8 +1554,8 @@ function resolveComponentFile(manifest, name) {
|
|
|
1545
1554
|
}
|
|
1546
1555
|
|
|
1547
1556
|
// Theme var write: rewrite (or append) a single `--var: value;` in the target CSS file.
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1557
|
+
// Global vars go to the theme stylesheet the page links (else the default path);
|
|
1558
|
+
// a patch may name another stylesheet the page links. Surgical — rewrites the existing declaration in place, else
|
|
1550
1559
|
// appends into the first :root{} block, else creates one.
|
|
1551
1560
|
const DEFAULT_THEME_FILE = 'styles/core/manifest.theme.css';
|
|
1552
1561
|
// A custom-property value must stay inside its declaration. A small tokenizer: strings
|
|
@@ -1588,10 +1597,21 @@ function themeValueRefusal(v) {
|
|
|
1588
1597
|
}
|
|
1589
1598
|
return stack.length ? 'unbalanced brackets' : null;
|
|
1590
1599
|
}
|
|
1591
|
-
// Stylesheets a theme edit may write: the default theme file
|
|
1592
|
-
// links,
|
|
1600
|
+
// Stylesheets a theme edit may write: the default theme file and stylesheets index.html links.
|
|
1601
|
+
// The theme stylesheet index.html links (manifest.theme.css, wherever it lives), else the framework's default path.
|
|
1602
|
+
function defaultThemeFile() {
|
|
1603
|
+
let html = '';
|
|
1604
|
+
try { html = readFileSync(join(root, 'index.html'), 'utf8'); } catch { return DEFAULT_THEME_FILE; }
|
|
1605
|
+
for (const n of allElements(parseHTML(html))) {
|
|
1606
|
+
if (n.implied || n.name !== 'link') continue;
|
|
1607
|
+
const open = openOf(html, n), href = String(attrOf(open, 'href') || '').trim().split(/[?#]/)[0];
|
|
1608
|
+
if (!/(^|\s)stylesheet(\s|$)/i.test(attrOf(open, 'rel') || '') || /^[a-z][\w+.-]*:|^\/\//i.test(href)) continue;
|
|
1609
|
+
if (basename(href) === 'manifest.theme.css') { const rel = posix.normalize(href.replace(/^\/+/, '')); if (!rel.startsWith('..')) return rel; }
|
|
1610
|
+
}
|
|
1611
|
+
return DEFAULT_THEME_FILE;
|
|
1612
|
+
}
|
|
1593
1613
|
function themeTargets() {
|
|
1594
|
-
const out = new Set([DEFAULT_THEME_FILE]);
|
|
1614
|
+
const out = new Set([DEFAULT_THEME_FILE, defaultThemeFile()]);
|
|
1595
1615
|
let html = '';
|
|
1596
1616
|
try { html = readFileSync(join(root, 'index.html'), 'utf8'); } catch { return out; } // read off-batch: not a base-checked input
|
|
1597
1617
|
const add = (href) => {
|
|
@@ -1604,14 +1624,26 @@ function themeTargets() {
|
|
|
1604
1624
|
if (n.implied) continue;
|
|
1605
1625
|
const open = openOf(html, n);
|
|
1606
1626
|
if (n.name === 'link' && /(^|\s)stylesheet(\s|$)/i.test(attrOf(open, 'rel') || '')) add(attrOf(open, 'href'));
|
|
1607
|
-
const tf = attrOf(open, 'data-edit-theme-file'); if (tf != null) add(tf);
|
|
1608
1627
|
}
|
|
1609
1628
|
return out;
|
|
1610
1629
|
}
|
|
1630
|
+
// Why a scoped theme value is not written to a stylesheet, for the case at hand
|
|
1631
|
+
function scopedThemeRefusal(p) {
|
|
1632
|
+
const what = `scoped value ${p.scope}:${p.var}`;
|
|
1633
|
+
for (const rel of Object.keys(baseHashes()).filter(f => /\.html?$/i.test(f))) {
|
|
1634
|
+
const file = safeResolve('/' + rel); if (!file) continue;
|
|
1635
|
+
let html = ''; try { html = readFileSync(file, 'utf8'); } catch { continue; }
|
|
1636
|
+
if (locateEditEl(html, p.scope)) return `${what} applies on its scope element (x-edit="${p.scope}" in ${rel}): it is written there as an inline style edit, not to a stylesheet, which would apply it site-wide`;
|
|
1637
|
+
}
|
|
1638
|
+
return `${what}: no source file mnfst-run can write holds x-edit="${p.scope}" (generated content, or not on any page), and a stylesheet would apply it site-wide`;
|
|
1639
|
+
}
|
|
1611
1640
|
function writeThemeVar(p) {
|
|
1612
1641
|
if (!/^--[\w-]+$/.test(p.var || '')) return { region: p.var, status: 'error', reason: 'bad var name' };
|
|
1613
|
-
|
|
1614
|
-
|
|
1642
|
+
// A scoped value applies on its scope element: the page writes it there (its inline style).
|
|
1643
|
+
// One sent here has no element mnfst-run can address; a stylesheet would apply it site-wide.
|
|
1644
|
+
if (p.scope) return { region: `${p.scope}:${p.var}`, status: 'error', reason: scopedThemeRefusal(p) };
|
|
1645
|
+
const rel = posix.normalize(String(p.file || defaultThemeFile()).replace(/^\/+/, ''));
|
|
1646
|
+
if (!themeTargets().has(rel)) return { region: p.var, status: 'error', reason: `${rel} is not a stylesheet this page links` };
|
|
1615
1647
|
const file = safeResolve('/' + rel);
|
|
1616
1648
|
if (!file) return { region: p.var, status: 'error', reason: `path outside root: ${rel}` };
|
|
1617
1649
|
const refused = writeRefusal(file, 'css'); if (refused) return { region: p.var, status: 'error', reason: `refused: ${refused}` };
|
|
@@ -1664,7 +1696,7 @@ function writeComponentEdits(p, manifest) {
|
|
|
1664
1696
|
const seg = ed.path === '' ? 'root' : ed.path.replace(/\./g, '_');
|
|
1665
1697
|
|
|
1666
1698
|
if (prop === 'style') {
|
|
1667
|
-
if (p.scope !== 'main') { applied.push({ path: ed.path, prop, status: '
|
|
1699
|
+
if (p.scope !== 'main') { applied.push({ path: ed.path, prop, status: 'error', reason: 'instance styles are not written (only the component\'s own, in main scope)' }); continue; }
|
|
1668
1700
|
const why = attrRefusal('style', ed.value, node.tag);
|
|
1669
1701
|
if (why) { applied.push({ path: ed.path, prop, status: 'error', reason: why }); continue; }
|
|
1670
1702
|
vwrite(compFile, html.slice(0, node.tagStart) + setAttr(openTag, 'style', ed.value ? String(ed.value) : null) + html.slice(node.openEnd + 1));
|
|
@@ -2055,7 +2087,7 @@ const server = createServer((req, res) => {
|
|
|
2055
2087
|
if (p.kind === 'data') {
|
|
2056
2088
|
const src = (manifest.data || {})[p.source];
|
|
2057
2089
|
const rel = typeof src === 'string' ? src : null;
|
|
2058
|
-
if (!rel) return { region: p.region, status: 'error', reason:
|
|
2090
|
+
if (!rel) return { region: p.region, status: 'error', reason: dataSourceRefusal(p.source, manifest) };
|
|
2059
2091
|
const file = resolveRel(rel);
|
|
2060
2092
|
if (!file || !isFile(file)) return { region: p.region, status: 'error', reason: `file not found: ${rel}` };
|
|
2061
2093
|
const refused = writeRefusal(file, 'data'); if (refused) return { region: p.region, status: 'error', reason: `refused: ${refused}` };
|