@deneb-ui/cli 2.0.98 → 2.0.99
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/README.md +10 -1
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +37 -2
- package/src/arc/field-paths.cjs +8 -1
- package/src/arc/fivora-schema-authority.cjs +2 -2
- package/src/arc/manifest.cjs +14 -2
- package/src/arc/semantic.cjs +17 -8
- package/src/tools/local-template-lab.cjs +24 -23
package/README.md
CHANGED
|
@@ -60,11 +60,20 @@ npm install -D @deneb-ui/cli
|
|
|
60
60
|
| `deneb zip` | Create upload-ready `fivora-template.zip` |
|
|
61
61
|
| `deneb doctor` | Environment, dependency, and export diagnostics |
|
|
62
62
|
| `deneb add <component>` | Copy DENEB UI components into your project |
|
|
63
|
-
| `deneb lab` | Launch local visual
|
|
63
|
+
| `deneb lab` | Launch the local visual editor, preview changes, and save them to the declared `siteDataFile` |
|
|
64
64
|
| `deneb update` | Update `@deneb-ui/*` packages and synced components |
|
|
65
65
|
|
|
66
66
|
Full reference: [deneb.fivora.site/docs/cli](https://deneb.fivora.site/docs/cli)
|
|
67
67
|
|
|
68
|
+
### Local preview before upload
|
|
69
|
+
|
|
70
|
+
Run `deneb lab .` in a template project, then paste the printed controller URL
|
|
71
|
+
and token into **Developer Portal → Local Test Lab**. Content and style edits are
|
|
72
|
+
applied to the preview immediately. Use **Save local** to persist the current
|
|
73
|
+
editor state to the `siteDataFile` declared in `fivora-template.json`; the write
|
|
74
|
+
stays on your machine and no template files are uploaded. Run **Test** after
|
|
75
|
+
saving to perform the same local preflight checks used before packaging.
|
|
76
|
+
|
|
68
77
|
---
|
|
69
78
|
|
|
70
79
|
## What `deneb init` does (ARC)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.99",
|
|
4
4
|
"description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deneb": "bin/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@babel/parser": "^7.28.0",
|
|
52
|
-
"@deneb-ui/core": "^2.0.
|
|
52
|
+
"@deneb-ui/core": "^2.0.99",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -12,7 +12,13 @@ const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
|
|
|
12
12
|
const { analyzeFile } = require('../semantic.cjs');
|
|
13
13
|
const { planTransformations } = require('../planner.cjs');
|
|
14
14
|
const { applyFilePlan } = require('../transformer.cjs');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
buildFieldPath,
|
|
17
|
+
classifyFieldType,
|
|
18
|
+
inferFieldName,
|
|
19
|
+
isListActionCtaKey,
|
|
20
|
+
} = require('../field-paths.cjs');
|
|
21
|
+
const { normalizeFieldType } = require('../fivora-schema-authority.cjs');
|
|
16
22
|
const { enrichSchemasFromContent } = require('../manifest.cjs');
|
|
17
23
|
const { runDenebArc } = require('../index.cjs');
|
|
18
24
|
const { parseSource } = require('../ast.cjs');
|
|
@@ -67,6 +73,16 @@ test('field-paths recognizes list action CTA keys', () => {
|
|
|
67
73
|
assert.equal(isListActionCtaKey('features'), false);
|
|
68
74
|
});
|
|
69
75
|
|
|
76
|
+
test('price fields use numeric data while currency and formatted prices stay text', () => {
|
|
77
|
+
assert.equal(normalizeFieldType('price', 'price'), 'number');
|
|
78
|
+
assert.equal(normalizeFieldType('currency', 'currency'), 'text');
|
|
79
|
+
assert.equal(classifyFieldType('text', 1000, 'price'), 'number');
|
|
80
|
+
assert.equal(classifyFieldType('text', '1000', 'price'), 'number');
|
|
81
|
+
assert.equal(classifyFieldType('text', '1000.50', 'amount'), 'number');
|
|
82
|
+
assert.equal(classifyFieldType('text', 'Rs. 1,000', 'price'), 'text');
|
|
83
|
+
assert.equal(classifyFieldType('text', 'LKR 1000', 'price'), 'text');
|
|
84
|
+
});
|
|
85
|
+
|
|
70
86
|
test('manifest enrichSchemasFromContent registers list CTA and paired directions fields', () => {
|
|
71
87
|
const sections = [
|
|
72
88
|
{
|
|
@@ -447,10 +463,30 @@ test('static-array collections become list contracts without changing render log
|
|
|
447
463
|
const products = home.fields.find((f) => f.key === 'products');
|
|
448
464
|
assert.equal(products.type, 'list');
|
|
449
465
|
assert.deepEqual(products.fields.map((f) => f.key).sort(), ['description', 'image', 'price', 'title']);
|
|
466
|
+
assert.equal(products.fields.find((field) => field.key === 'price').type, 'text');
|
|
450
467
|
assert.equal(siteData.content.home.products.length, 3);
|
|
451
468
|
assert.equal(siteData.content.home.products[0].title, 'Minimalist Smart Watch');
|
|
452
469
|
});
|
|
453
470
|
|
|
471
|
+
test('numeric catalog prices stay numbers in generated site data and editor schema', () => {
|
|
472
|
+
const dir = copyOf(STOREFRONT_FIXTURE);
|
|
473
|
+
const gridPath = path.join(dir, 'src', 'components', 'ProductGrid.tsx');
|
|
474
|
+
const grid = fs
|
|
475
|
+
.readFileSync(gridPath, 'utf8')
|
|
476
|
+
.replace(/price: '\$(\d+)\.00'/g, 'price: $1');
|
|
477
|
+
fs.writeFileSync(gridPath, grid, 'utf8');
|
|
478
|
+
|
|
479
|
+
silence(() => runDenebArc(dir, 'numeric-price-store', { telemetry: 'off' }));
|
|
480
|
+
const { manifest, siteData } = auditFivora(dir);
|
|
481
|
+
const home = manifest.editorSchema.sections.find((section) => section.id === 'home');
|
|
482
|
+
const products = home.fields.find((field) => field.key === 'products');
|
|
483
|
+
const price = products.fields.find((field) => field.key === 'price');
|
|
484
|
+
|
|
485
|
+
assert.equal(price.type, 'number');
|
|
486
|
+
assert.equal(siteData.content.home.products[0].price, 249);
|
|
487
|
+
assert.equal(typeof siteData.content.home.products[0].price, 'number');
|
|
488
|
+
});
|
|
489
|
+
|
|
454
490
|
test('collections holding component references still bind primitive item fields', () => {
|
|
455
491
|
const dir = copyOf(STOREFRONT_FIXTURE);
|
|
456
492
|
silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
|
|
@@ -3923,4 +3959,3 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
|
|
|
3923
3959
|
|
|
3924
3960
|
|
|
3925
3961
|
|
|
3926
|
-
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -181,7 +181,14 @@ function classifyFieldType(kind, value, fieldName = '') {
|
|
|
181
181
|
/price|amount|rating|count|score|stars|qty|quantity|discount|fee|cost/i.test(name)
|
|
182
182
|
) {
|
|
183
183
|
if (typeof value === 'string' && /\$|lkr|usd|rs\.?\s*\d/i.test(value)) return 'text';
|
|
184
|
-
|
|
184
|
+
if (typeof value === 'number') return 'number';
|
|
185
|
+
if (
|
|
186
|
+
typeof value === 'string' &&
|
|
187
|
+
/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())
|
|
188
|
+
) {
|
|
189
|
+
return 'number';
|
|
190
|
+
}
|
|
191
|
+
return 'text';
|
|
185
192
|
}
|
|
186
193
|
if (typeof value === 'boolean' || /enabled|visible|show|active/i.test(name)) return 'boolean';
|
|
187
194
|
if (kind === 'textarea' || (typeof value === 'string' && (value.length > 70 || value.includes('\n')))) return 'textarea';
|
|
@@ -25,8 +25,8 @@ const PLATFORM_ALLOWED_TYPES = new Set([
|
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
const TYPE_COERCION_MAP = {
|
|
28
|
-
currency: 'text',
|
|
29
|
-
price: '
|
|
28
|
+
currency: 'text',
|
|
29
|
+
price: 'number',
|
|
30
30
|
richtext: 'textarea',
|
|
31
31
|
phone: 'tel',
|
|
32
32
|
link: 'url',
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -370,7 +370,12 @@ const PLATFORM_CONTROLLED_PATHS = new Set(
|
|
|
370
370
|
);
|
|
371
371
|
|
|
372
372
|
function slimListItems(items, itemFields) {
|
|
373
|
-
const
|
|
373
|
+
const fieldsByKey = new Map(
|
|
374
|
+
(itemFields || [])
|
|
375
|
+
.filter((field) => field?.key)
|
|
376
|
+
.map((field) => [field.key, field]),
|
|
377
|
+
);
|
|
378
|
+
const keys = [...fieldsByKey.keys()];
|
|
374
379
|
if (!keys.length || !Array.isArray(items)) return items || [];
|
|
375
380
|
return items.map((item) => {
|
|
376
381
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
|
|
@@ -378,7 +383,14 @@ function slimListItems(items, itemFields) {
|
|
|
378
383
|
for (const key of keys) {
|
|
379
384
|
if (!(key in item)) continue;
|
|
380
385
|
if (Array.isArray(item[key])) continue;
|
|
381
|
-
|
|
386
|
+
const field = fieldsByKey.get(key);
|
|
387
|
+
const value = item[key];
|
|
388
|
+
out[key] =
|
|
389
|
+
field?.type === 'number' &&
|
|
390
|
+
typeof value === 'string' &&
|
|
391
|
+
/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())
|
|
392
|
+
? Number(value)
|
|
393
|
+
: value;
|
|
382
394
|
}
|
|
383
395
|
return Object.keys(out).length ? out : item;
|
|
384
396
|
});
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -33,6 +33,7 @@ const { resolveImportSpecifier } = require('./scanner.cjs');
|
|
|
33
33
|
const { resolvePublicAssetUrl } = require('./ir-builder.cjs');
|
|
34
34
|
const { analyzeElementFragments } = require('./text-fragment-analyzer.cjs');
|
|
35
35
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
36
|
+
const { classifyFieldType } = require('./field-paths.cjs');
|
|
36
37
|
|
|
37
38
|
const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
|
|
38
39
|
const ARIA_ONLY_RE = /^(aria-|data-state|data-slot|data-orientation)/;
|
|
@@ -1105,6 +1106,21 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
1105
1106
|
const stringItems = arr.items.every((item) => item.type === 'string');
|
|
1106
1107
|
const rootTagName = getJsxName(mapInfo.rootElement);
|
|
1107
1108
|
const isChildCustomComponent = /^[A-Z]/.test(rootTagName);
|
|
1109
|
+
const sampleItem = objectItems
|
|
1110
|
+
? arr.items.find((item) => item?.value && typeof item.value === 'object')?.value
|
|
1111
|
+
: null;
|
|
1112
|
+
|
|
1113
|
+
// JSX text positions alone look like generic text, but commerce values
|
|
1114
|
+
// such as a bare numeric `price` must remain numeric for catalog range
|
|
1115
|
+
// filters, sorting, totals, and database persistence. Formatted values
|
|
1116
|
+
// such as "$249" intentionally remain text for legacy templates.
|
|
1117
|
+
if (sampleItem) {
|
|
1118
|
+
for (const [key, role] of itemUsage) {
|
|
1119
|
+
if (role !== 'text') continue;
|
|
1120
|
+
const inferredRole = classifyFieldType('text', sampleItem[key], key);
|
|
1121
|
+
if (inferredRole !== 'text') itemUsage.set(key, inferredRole);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1108
1124
|
|
|
1109
1125
|
// If the map returns a custom child component (e.g. <ProductCard product={item} />),
|
|
1110
1126
|
// derive item fields from the sample object items if direct JSX member usage was empty
|
|
@@ -1112,13 +1128,7 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
1112
1128
|
const sampleObj = arr.items[0].value;
|
|
1113
1129
|
for (const key of Object.keys(sampleObj)) {
|
|
1114
1130
|
if (/^(id|_id|key|slug)$/i.test(key)) continue;
|
|
1115
|
-
const role =
|
|
1116
|
-
? 'image'
|
|
1117
|
-
: /price|cost|amount|count|rating|stars/i.test(key)
|
|
1118
|
-
? 'number'
|
|
1119
|
-
: /url|link|href/i.test(key)
|
|
1120
|
-
? 'url'
|
|
1121
|
-
: 'text';
|
|
1131
|
+
const role = classifyFieldType('text', sampleObj[key], key);
|
|
1122
1132
|
itemUsage.set(key, role);
|
|
1123
1133
|
}
|
|
1124
1134
|
}
|
|
@@ -1207,4 +1217,3 @@ module.exports = {
|
|
|
1207
1217
|
isStaticSkipText,
|
|
1208
1218
|
resolveChildText,
|
|
1209
1219
|
};
|
|
1210
|
-
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
3
|
-
\`),document.head.appendChild(T),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l));function $(t){return t.source!==window.parent?!1:(g==="*"&&t.origin&&t.origin!=="null"&&(g=t.origin),g==="*"||t.origin===g)}function I(t){window.parent.postMessage(t,g)}function L(){s&&(s.removeAttribute(C),s.removeAttribute(P)),s=null}function X(t){o&&o!==t&&(o.removeAttribute(_),o.removeAttribute(h)),o=t,o.setAttribute(_,"true"),o.setAttribute(h,"true")}function q(){o&&(o.removeAttribute(_),o.removeAttribute(h)),o=null}function G(t){return t instanceof Element?t.closest(H):null}function z(t,e){const d=f.get(e);return d&&["string","number","boolean"].includes(typeof d.value)?d.value:t instanceof HTMLImageElement?t.currentSrc||t.src||"":(t.textContent||"").trim()}function J(t){if(!l)return;const e=G(t.target);!e||e===s||(L(),s=e,s.setAttribute(C,"true"))}function Q(t){if(!l||!s)return;const e=t.relatedTarget;e instanceof Node&&s.contains(e)||L()}function Z(t,e,d){const A=[],p=new Set(e?[e]:[]),n=d||e&&e.replace(/(\\[\\d+\\])?\\.\\w+$/,"").replace(/\\[\\d+\\]$/,"");if(n)for(const[i,a]of f)i!==e&&!p.has(i)&&(i.startsWith(n+".")||i.startsWith(n+"["))&&a?.kind!=="collection"&&(p.add(i),A.push({path:i,label:a.label||i.split(".").pop()||"Content",type:a.type||"text"}));let r=t instanceof Element?t:null,c=0;for(;r&&r!==document.body&&c<3;){const i=r.getAttribute(R);if(i&&i!==e&&!p.has(i)){const a=f.get(i);a?.kind!=="collection"&&(p.add(i),A.push({path:i,label:a?.label||i.split(".").pop()||"Content",type:a?.type||"text"}))}r=r.parentElement,c+=1}return A}function tt(t){if(!l)return;const e=G(t.target);if(!e)return;t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation(),X(e);const d=e.closest("["+R+"]"),A=e.closest("["+V+"]"),p=e.closest("["+x+"]"),n=d?.getAttribute(R)||null,r=A?.getAttribute(V)||null,c=p?.getAttribute(x)||null,i=n?f.get(n):null,a=e.getBoundingClientRect(),U=c?.match(/\\[(\\d+)\\](?!.*\\[\\d+\\])/),y=[],E=(M,w,b)=>{!w||y.some(S=>S.key===w)||y.push({kind:M,key:w,label:b})};E("element",n,"This element"),E("card",c,"This card"),E("grid",r,"Entire grid");const D=e.closest("[data-preview-page-key]")?.getAttribute("data-preview-page-key")||"",u=e.closest("section,[data-design-section],[data-section-id]");if(u){const M=u.getAttribute("data-design-section")||u.getAttribute("data-section-id")||u.id,b=(u.parentElement?Array.from(u.parentElement.children).filter(et=>et.tagName==="SECTION"):[]).indexOf(u)+1,S=M||(b>0?"nth-"+b:"");E("section",S?"section:"+(D||"all")+":"+S:null,"This section")}E("page",D?"page:"+D:null,"Current page"),E("site","site:all","Whole website");const B={type:N,fieldPath:n,fieldValue:n?z(d||e,n):"",elementTag:e.tagName.toLowerCase(),isImage:e instanceof HTMLImageElement||i?.type==="image",boundingRect:{top:a.top,left:a.left,width:a.width,height:a.height},collectionPath:r,listPath:r,itemPath:c,itemIndex:U?Number(U[1]):null,descriptorKind:n?"field":r?"collection":null,relatedFields:Z(e,n,c),styleScopes:y};I(B),I({...B,type:W})}window.addEventListener("message",t=>{!$(t)||!t.data||typeof t.data!="object"||t.data.type!==F&&t.data.type!==K||(l=t.data.editMode===!0,f=new Map(Array.isArray(t.data.fields)?t.data.fields.filter(e=>e&&typeof e.path=="string").map(e=>[e.path,e]):[]),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),l||(L(),q()))}),document.addEventListener("mouseover",J,!0),document.addEventListener("mouseout",Q,!0),document.addEventListener("click",tt,!0),I({type:k,pathname:window.location.pathname}),I({type:j,pathname:window.location.pathname})})();`;let
|
|
4
|
-
`;
|
|
5
|
-
`),process.exit(1)}try{new Function(
|
|
6
|
-
`)),process.exit(0)):
|
|
2
|
+
const q=require("node:crypto"),m=require("node:fs"),T=require("node:http"),h=require("node:path"),{spawn:$}=require("node:child_process"),ee=4174,te=4173,W=96*1024,ae=10*1024*1024,g="127.0.0.1",B="/__fivora_local_preview",ne=`(()=>{const m=["market","place"].join(""),O="__"+m.toUpperCase()+"_LOCAL_VISUAL_BRIDGE__",Y=m.toUpperCase()+"_PREVIEW_",v=t=>Y+t;if(window.__FIVORA_LOCAL_VISUAL_BRIDGE__||window[O])return;window.__FIVORA_LOCAL_VISUAL_BRIDGE__=!0,window[O]=!0;const F="FIVORA_PREVIEW_EDIT_MODE",K=v("EDIT_MODE"),N="FIVORA_PREVIEW_ELEMENT_CLICKED",W=v("ELEMENT_CLICKED"),k="FIVORA_PREVIEW_READY",j=v("READY"),R="data-preview-field-path",V="data-preview-list-path",x="data-preview-item-path",C="data-fivora-local-edit-target",_="data-fivora-local-selected-target",P="data-"+m+"-local-edit-target",h="data-"+m+"-local-selected-target",H="[data-preview-field-path], [data-preview-list-path], [data-preview-item-path]";let l=window.parent!==window,f=new Map,s=null,o=null,g="*";const T=document.createElement("style");T.setAttribute("data-fivora-local-visual-bridge",""),T.setAttribute("data-"+m+"-local-visual-bridge",""),T.textContent=["["+C+"], ["+P+"] {"," outline: 2px solid #06b6d4 !important;"," outline-offset: 3px !important;"," cursor: pointer !important;"," box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.16) !important;","}","["+_+"], ["+h+"] {"," outline: 2px dashed #06b6d4 !important;"," outline-offset: 3px !important;"," box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.18) !important;","}",'html[data-fivora-local-edit-mode="true"] [data-preview-field-path],','html[data-fivora-local-edit-mode="true"] [data-preview-list-path],','html[data-fivora-local-edit-mode="true"] [data-preview-item-path],','html[data-fivora-local-edit-mode="true"] [data-preview-field-path],','html[data-fivora-local-edit-mode="true"] [data-preview-list-path],','html[data-fivora-local-edit-mode="true"] [data-preview-item-path] {'," cursor: pointer !important;","}"].join(\`
|
|
3
|
+
\`),document.head.appendChild(T),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l));function $(t){return t.source!==window.parent?!1:(g==="*"&&t.origin&&t.origin!=="null"&&(g=t.origin),g==="*"||t.origin===g)}function I(t){window.parent.postMessage(t,g)}function L(){s&&(s.removeAttribute(C),s.removeAttribute(P)),s=null}function X(t){o&&o!==t&&(o.removeAttribute(_),o.removeAttribute(h)),o=t,o.setAttribute(_,"true"),o.setAttribute(h,"true")}function q(){o&&(o.removeAttribute(_),o.removeAttribute(h)),o=null}function G(t){return t instanceof Element?t.closest(H):null}function z(t,e){const d=f.get(e);return d&&["string","number","boolean"].includes(typeof d.value)?d.value:t instanceof HTMLImageElement?t.currentSrc||t.src||"":(t.textContent||"").trim()}function J(t){if(!l)return;const e=G(t.target);!e||e===s||(L(),s=e,s.setAttribute(C,"true"))}function Q(t){if(!l||!s)return;const e=t.relatedTarget;e instanceof Node&&s.contains(e)||L()}function Z(t,e,d){const A=[],p=new Set(e?[e]:[]),n=d||e&&e.replace(/(\\[\\d+\\])?\\.\\w+$/,"").replace(/\\[\\d+\\]$/,"");if(n)for(const[i,a]of f)i!==e&&!p.has(i)&&(i.startsWith(n+".")||i.startsWith(n+"["))&&a?.kind!=="collection"&&(p.add(i),A.push({path:i,label:a.label||i.split(".").pop()||"Content",type:a.type||"text"}));let r=t instanceof Element?t:null,c=0;for(;r&&r!==document.body&&c<3;){const i=r.getAttribute(R);if(i&&i!==e&&!p.has(i)){const a=f.get(i);a?.kind!=="collection"&&(p.add(i),A.push({path:i,label:a?.label||i.split(".").pop()||"Content",type:a?.type||"text"}))}r=r.parentElement,c+=1}return A}function tt(t){if(!l)return;const e=G(t.target);if(!e)return;t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation(),X(e);const d=e.closest("["+R+"]"),A=e.closest("["+V+"]"),p=e.closest("["+x+"]"),n=d?.getAttribute(R)||null,r=A?.getAttribute(V)||null,c=p?.getAttribute(x)||null,i=n?f.get(n):null,a=e.getBoundingClientRect(),U=c?.match(/\\[(\\d+)\\](?!.*\\[\\d+\\])/),y=[],E=(M,w,b)=>{!w||y.some(S=>S.key===w)||y.push({kind:M,key:w,label:b})};E("element",n,"This element"),E("card",c,"This card"),E("grid",r,"Entire grid");const D=e.closest("[data-preview-page-key]")?.getAttribute("data-preview-page-key")||"",u=e.closest("section,[data-design-section],[data-section-id]");if(u){const M=u.getAttribute("data-design-section")||u.getAttribute("data-section-id")||u.id,b=(u.parentElement?Array.from(u.parentElement.children).filter(et=>et.tagName==="SECTION"):[]).indexOf(u)+1,S=M||(b>0?"nth-"+b:"");E("section",S?"section:"+(D||"all")+":"+S:null,"This section")}E("page",D?"page:"+D:null,"Current page"),E("site","site:all","Whole website");const B={type:N,fieldPath:n,fieldValue:n?z(d||e,n):"",elementTag:e.tagName.toLowerCase(),isImage:e instanceof HTMLImageElement||i?.type==="image",boundingRect:{top:a.top,left:a.left,width:a.width,height:a.height},collectionPath:r,listPath:r,itemPath:c,itemIndex:U?Number(U[1]):null,descriptorKind:n?"field":r?"collection":null,relatedFields:Z(e,n,c),styleScopes:y};I(B),I({...B,type:W})}window.addEventListener("message",t=>{!$(t)||!t.data||typeof t.data!="object"||t.data.type!==F&&t.data.type!==K||(l=t.data.editMode===!0,f=new Map(Array.isArray(t.data.fields)?t.data.fields.filter(e=>e&&typeof e.path=="string").map(e=>[e.path,e]):[]),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(l)),l||(L(),q()))}),document.addEventListener("mouseover",J,!0),document.addEventListener("mouseout",Q,!0),document.addEventListener("click",tt,!0),I({type:k,pathname:window.location.pathname}),I({type:j,pathname:window.location.pathname})})();`;let E=ne;try{const e=require("./template-preview-focus-bridge.cjs");typeof e=="string"&&e.trim()&&(E=e)}catch{}const re=`var __name = typeof __name === "function" ? __name : ((target, value) => (typeof Object.defineProperty === "function" ? Object.defineProperty(target, "name", { value, configurable: true }) : target));
|
|
4
|
+
`;E.includes("__name")&&!E.includes("var __name")&&(E=`${re}${E}`);function u(e){process.stderr.write(`Local Template Lab: ${e}
|
|
5
|
+
`),process.exit(1)}try{new Function(E)}catch(e){u(`Visual editor bridge is invalid: ${e.message}`)}function j(e,t,n){if(e===void 0)return n;const a=Number(e);return(!Number.isInteger(a)||a<1024||a>65535)&&u(`${t} must be a port between 1024 and 65535.`),a}function ie(e){const t=[...e];let n="",a,r,s=!1;for(let c=0;c<t.length;c+=1){const o=t[c];o==="--api-port"?(a=t[c+1],c+=1):o.startsWith("--api-port=")?a=o.slice(11):o==="--preview-port"?(r=t[c+1],c+=1):o.startsWith("--preview-port=")?r=o.slice(15):o==="--skip-install"?s=!0:o==="--help"||o==="-h"?(process.stdout.write(["Fivora Local Template Lab","","Usage:"," npm run lab -- <template-directory> [options]","","Options:"," --api-port <port> Loopback controller port (default: 4174)"," --preview-port <port> Template dev-server port (default: 4173)"," --skip-install Do not install missing local dependencies",""].join(`
|
|
6
|
+
`)),process.exit(0)):o.startsWith("-")?u(`Unknown option: ${o}`):n?u(`Unexpected argument: ${o}`):n=o}return n||u("A template directory is required. Run with --help for usage."),{templatePath:h.resolve(n),apiPort:j(a,"--api-port",ee),previewPort:j(r,"--preview-port",te),skipInstall:s}}function x(e,t){try{return JSON.parse(m.readFileSync(e,"utf8"))}catch(n){u(`${t} is missing or invalid at ${e}: ${n instanceof Error?n.message:"unknown error"}`)}}const i=ie(process.argv.slice(2));m.existsSync(i.templatePath)||u(`Template directory does not exist: ${i.templatePath}`),m.statSync(i.templatePath).isDirectory()||u(`Template path must be a directory: ${i.templatePath}`);const oe=["fivora-template.json","fivora-template.json"].find(e=>m.existsSync(h.join(i.templatePath,e)))||"fivora-template.json",H=h.join(i.templatePath,oe),se=h.join(i.templatePath,"package.json"),p=x(H,"Template manifest"),I=x(se,"package.json");p.framework!=="nextjs-static-export"&&u('Template manifest framework must be "nextjs-static-export".'),(!I.scripts||typeof I.scripts.dev!="string")&&u("Template package.json must define a dev script for live preview."),(typeof p.siteDataFile!="string"||!p.siteDataFile.trim())&&u("Template manifest siteDataFile is required.");const _=h.resolve(i.templatePath,p.siteDataFile.trim()),k=h.relative(i.templatePath,_);(k.startsWith("..")||h.isAbsolute(k))&&u("Template manifest siteDataFile must stay inside the template directory.");const G=q.randomBytes(24).toString("base64url"),P=`http://${g}:${i.apiPort}`,J=`http://${g}:${i.previewPort}`,V=`${P}${B}`,le=h.join(__dirname,"deneb-template-validator.cjs");let A=!1,f=null,w=null,S=null,b=null,R=0;const X=5;let M="",O="";const l={protocolVersion:2,connected:!0,templateName:typeof p.name=="string"&&p.name.trim()?p.name.trim():typeof I.name=="string"?I.name:h.basename(i.templatePath),templatePath:i.templatePath,previewUrl:V,apiUrl:P,devStatus:"starting",devError:null,startedAt:new Date().toISOString(),validation:{status:"idle",startedAt:null,completedAt:null,exitCode:null}};function d(e,t){const n=t.toString();e==="dev"?M=`${M}${n}`.slice(-W):O=`${O}${n}`.slice(-W),process.stdout.write(n)}function Y(){return{...l,devLog:M,validationLog:O}}function de(e,t){const n=e.headers.origin;n&&/^(https?:\/\/|null$)/.test(n)&&(t.setHeader("Access-Control-Allow-Origin",n),t.setHeader("Vary","Origin")),t.setHeader("Access-Control-Allow-Headers","Authorization, Content-Type"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Private-Network","true"),t.setHeader("Cache-Control","no-store"),t.setHeader("X-Content-Type-Options","nosniff")}function v(e,t,n){const a=JSON.stringify(n);e.statusCode=t,e.setHeader("Content-Type","application/json; charset=utf-8"),e.setHeader("Content-Length",Buffer.byteLength(a)),e.end(a)}function ce(e){return e.headers.authorization===`Bearer ${G}`}function pe(){try{return JSON.parse(m.readFileSync(_,"utf8"))}catch(e){throw new Error(`Unable to read ${p.siteDataFile}: ${e instanceof Error?e.message:"unknown error"}`)}}function ue(e,t=ae){return new Promise((n,a)=>{const r=[];let s=0,c=!1;e.on("data",o=>{if(!c){if(s+=o.length,s>t){c=!0,a(new Error(`Request body is too large. Local site data must be ${Math.floor(t/1024/1024)} MB or less.`));return}r.push(o)}}),e.on("end",()=>{if(!c)try{const o=Buffer.concat(r).toString("utf8");n(JSON.parse(o))}catch{a(new Error("Request body must be valid JSON."))}}),e.on("error",o=>{c||(c=!0,a(o))})})}function F(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function me(e){const t=`${_}.${process.pid}.${Date.now()}.tmp`,n=m.existsSync(_)?m.statSync(_).mode:420;try{m.writeFileSync(t,`${JSON.stringify(e,null,2)}
|
|
7
|
+
`,{encoding:"utf8",flag:"wx",mode:n}),m.renameSync(t,_)}catch(a){if(m.existsSync(t))try{m.unlinkSync(t)}catch{}throw a}}function fe(){return w?!1:(O="",l.validation={status:"running",startedAt:new Date().toISOString(),completedAt:null,exitCode:null},w=$(process.execPath,[le,"validate",i.templatePath],{cwd:__dirname,env:process.env,stdio:["ignore","pipe","pipe"]}),w.stdout.on("data",e=>d("validation",e)),w.stderr.on("data",e=>d("validation",e)),w.on("error",e=>{d("validation",`
|
|
7
8
|
Unable to start validation: ${e.message}
|
|
8
|
-
`)}),
|
|
9
|
-
Live preview ready: ${
|
|
10
|
-
`);return}
|
|
9
|
+
`)}),w.on("close",e=>{l.validation={...l.validation,status:e===0?"passed":"failed",completedAt:new Date().toISOString(),exitCode:e},w=null}),!0)}function U(){if(A||l.devStatus==="ready")return;const e=T.get(J,t=>{if(t.resume(),t.statusCode&&t.statusCode<500){l.devStatus="ready",l.devError=null,R=0,process.stdout.write(`
|
|
10
|
+
Live preview ready: ${V}
|
|
11
|
+
`);return}S=setTimeout(U,600)});e.setTimeout(900,()=>e.destroy()),e.on("error",()=>{S=setTimeout(U,600)})}function ve(e){if(!(A||b)){if(R>=X){l.devStatus="failed",l.devError=e;return}R+=1,l.devStatus="starting",l.devError=null,d("dev",`
|
|
11
12
|
Preview server stopped during navigation (${e}). Restarting (${R}/${X})...
|
|
12
|
-
`),
|
|
13
|
-
`);const
|
|
14
|
-
`);const e=process.platform==="win32"?"npm.cmd":"npm";
|
|
13
|
+
`),b=setTimeout(()=>{b=null,N()},900)}}function he(){const e=h.join(i.templatePath,"node_modules");if(!i.skipInstall&&!m.existsSync(e)){l.devStatus="installing";const t=typeof p.installCommand=="string"&&p.installCommand.trim()?p.installCommand.trim():"npm install";d("dev",`Installing local dependencies with: ${t}
|
|
14
|
+
`);const n=$(t,{cwd:i.templatePath,env:process.env,shell:!0,stdio:["ignore","pipe","pipe"]});f=n,n.stdout.on("data",a=>d("dev",a)),n.stderr.on("data",a=>d("dev",a)),n.on("error",a=>{l.devStatus="failed",l.devError=a.message,f=null}),n.on("close",a=>{if(f=null,a!==0){l.devStatus="failed",l.devError=`Dependency installation exited with code ${a}.`;return}N()});return}N()}function N(){l.devStatus="starting",l.devError=null,d("dev",`Starting template source server on ${J}. Source edits will hot reload.
|
|
15
|
+
`);const e=process.platform==="win32"?"npm.cmd":"npm";f=$(e,["run","dev","--","--hostname",g,"--port",String(i.previewPort)],{cwd:i.templatePath,env:{...process.env,NEXT_PUBLIC_SITE_BASE_PATH:""},shell:process.platform==="win32",stdio:["ignore","pipe","pipe"]}),f.stdout.on("data",t=>d("dev",t)),f.stderr.on("data",t=>d("dev",t)),f.on("error",t=>{l.devStatus="failed",l.devError=t.message,f=null}),f.on("close",(t,n)=>{if(f=null,!A){const a=n?`signal ${n}`:`exit code ${t}`;ve(a)}}),U()}function ge(){return`<!doctype html>
|
|
15
16
|
<html lang="en">
|
|
16
17
|
<head>
|
|
17
18
|
<meta charset="utf-8">
|
|
@@ -29,7 +30,7 @@ Preview server stopped during navigation (${e}). Restarting (${R}/${X})...
|
|
|
29
30
|
<iframe id="template-preview" title="Local template preview"></iframe>
|
|
30
31
|
<script>
|
|
31
32
|
(() => {
|
|
32
|
-
const BRIDGE_SOURCE = ${JSON.stringify(
|
|
33
|
+
const BRIDGE_SOURCE = ${JSON.stringify(E)};
|
|
33
34
|
const preview = document.getElementById('template-preview');
|
|
34
35
|
const errorBox = document.getElementById('bridge-error');
|
|
35
36
|
const PREVIOUS_PREVIEW_PREFIX = ['MARKET', 'PLACE'].join('') + '_PREVIEW_';
|
|
@@ -120,23 +121,23 @@ Preview server stopped during navigation (${e}). Restarting (${R}/${X})...
|
|
|
120
121
|
})();
|
|
121
122
|
<\/script>
|
|
122
123
|
</body>
|
|
123
|
-
</html>`}function
|
|
124
|
+
</html>`}function y(e){if(!e||typeof e!="object")return!1;const t="code"in e?String(e.code):"";return t==="ECONNRESET"||t==="ECONNABORTED"||t==="EPIPE"||t==="ERR_STREAM_DESTROYED"}function we(e,t){const n={...e.headers};n.host=`${g}:${i.previewPort}`,delete n["accept-encoding"],delete n.authorization;const a=T.request({hostname:g,port:i.previewPort,method:e.method,path:e.url,headers:n},r=>{t.writeHead(r.statusCode||502,r.statusMessage,r.headers),r.on("error",s=>{y(s)||d("dev",`
|
|
124
125
|
Preview proxy upstream error: ${s.message}
|
|
125
|
-
`),t.writableEnded||t.destroy()}),t.on("error",s=>{
|
|
126
|
+
`),t.writableEnded||t.destroy()}),t.on("error",s=>{y(s)||d("dev",`
|
|
126
127
|
Preview proxy response error: ${s.message}
|
|
127
|
-
`),r.destroy()}),r.pipe(t)});
|
|
128
|
+
`),r.destroy()}),r.pipe(t)});a.on("error",r=>{y(r)||d("dev",`
|
|
128
129
|
Preview proxy request error: ${r.message}
|
|
129
|
-
`),t.headersSent?t.writableEnded||t.destroy():
|
|
130
|
+
`),t.headersSent?t.writableEnded||t.destroy():v(t,502,{message:`Local preview is not ready: ${r.message}`})}),e.on("error",r=>{y(r)||d("dev",`
|
|
130
131
|
Preview proxy client error: ${r.message}
|
|
131
|
-
`),
|
|
132
|
+
`),a.destroy()}),e.pipe(a)}const C=T.createServer(async(e,t)=>{const n=new URL(e.url||"/",P);if(e.method==="GET"&&n.pathname===B){const a=ge();t.writeHead(200,{"Cache-Control":"no-store","Content-Length":Buffer.byteLength(a),"Content-Type":"text/html; charset=utf-8"}),t.end(a);return}if(!n.pathname.startsWith("/api/")){we(e,t);return}if(de(e,t),e.method==="OPTIONS"){t.statusCode=204,t.end();return}if(!ce(e)){v(t,401,{message:"Invalid Local Template Lab token."});return}if(e.method==="GET"&&n.pathname==="/api/status"){v(t,200,Y());return}if(e.method==="GET"&&n.pathname==="/api/site-data"){try{let a=p;try{a=x(H,"Template manifest")}catch{}v(t,200,{manifest:a,siteData:pe(),siteDataFile:a.siteDataFile||p.siteDataFile})}catch(a){v(t,500,{message:a instanceof Error?a.message:"Unable to read site data."})}return}if(e.method==="POST"&&n.pathname==="/api/site-data"){try{const a=await ue(e),r=F(a)?a.siteData:null;if(!F(r)||!F(r.content)){v(t,400,{message:"siteData must be a JSON object with a content object."});return}me(r),v(t,200,{saved:!0,siteData:r,siteDataFile:p.siteDataFile})}catch(a){v(t,400,{message:a instanceof Error?a.message:"Unable to save local site data."})}return}if(e.method==="POST"&&n.pathname==="/api/validate"){if(!fe()){v(t,409,{message:"Validation is already running."});return}v(t,202,Y());return}v(t,404,{message:"Local Template Lab endpoint not found."})});function Ee(e,t){const n=()=>{e.destroyed||e.destroy(),t.destroyed||t.destroy()},a=r=>{y(r)||d("dev",`
|
|
132
133
|
Preview proxy socket error: ${r.message}
|
|
133
|
-
`),
|
|
134
|
+
`),n()};e.on("error",a),t.on("error",a),e.on("close",()=>{t.destroyed||t.end()}),t.on("close",()=>{e.destroyed||e.end()}),t.pipe(e),e.pipe(t)}C.on("upgrade",(e,t,n)=>{if(e.url?.startsWith("/api/")){t.destroy();return}t.on("error",s=>{y(s)||d("dev",`
|
|
134
135
|
Preview proxy upgrade client error: ${s.message}
|
|
135
|
-
`)});const
|
|
136
|
+
`)});const a={...e.headers};a.host=`${g}:${i.previewPort}`;const r=T.request({hostname:g,port:i.previewPort,method:e.method,path:e.url,headers:a});r.on("upgrade",(s,c,o)=>{const Q=Object.entries(s.headers).flatMap(([Z,L])=>(Array.isArray(L)?L:[L]).filter(D=>D!==void 0).map(D=>`${Z}: ${D}`)).join(`\r
|
|
136
137
|
`);t.write(`HTTP/1.1 ${s.statusCode||101} ${s.statusMessage||"Switching Protocols"}\r
|
|
137
|
-
${
|
|
138
|
+
${Q}\r
|
|
138
139
|
\r
|
|
139
|
-
`),
|
|
140
|
+
`),n.length&&c.write(n),o.length&&t.write(o),Ee(t,c)}),r.on("error",s=>{y(s)||d("dev",`
|
|
140
141
|
Preview proxy upgrade upstream error: ${s.message}
|
|
141
|
-
`),t.destroyed||t.destroy()}),r.end()});function
|
|
142
|
-
`)),
|
|
142
|
+
`),t.destroyed||t.destroy()}),r.end()});function z(e){!e||e.killed||e.kill("SIGTERM")}function K(){A||(A=!0,S&&clearTimeout(S),b&&clearTimeout(b),z(w),z(f),C.close(()=>process.exit(0)),setTimeout(()=>process.exit(0),1500).unref())}C.on("error",e=>{u(`Unable to start loopback controller at ${P}: ${e instanceof Error?e.message:"unknown error"}`)}),C.listen(i.apiPort,g,()=>{process.stdout.write(["","Fivora Local Template Lab is running.",`Template: ${l.templateName}`,`Controller URL: ${P}`,`Connection token: ${G}`,`Preview URL: ${V}`,"","Paste the controller URL and connection token into Developer Portal > Local Test Lab.","Use Save local in the portal to write edits to the template site-data file.","Press Ctrl+C to stop. No template files are uploaded by this process.",""].join(`
|
|
143
|
+
`)),he()}),process.on("SIGINT",K),process.on("SIGTERM",K);
|