Skip to content

Commit 1582dc8

Browse files
csAdityaPachaurihiteshshetty-devhitesh-shetty-cstkkarancs06csAyushDubey
authored
v4.3.0 (#566)
* feat: implement isValidCslp function for CSLP value validation and add corresponding tests * test: mock extractDetailsFromCslp function in useRevalidateFieldDataPostMessageEvent tests * test: enhance tests by mocking isValidCslp function in variant-related test cases * chore: update tsup configuration to ignore legacy build & dts during dev mode * chore: upgrade tsup to latest version * chore: update doc for dev build mode * chore: version bump * chore: lodash-es version upgrade * feat: vb to ve * Vp 444 stag sync 2 (#562) * feat: added enableLivePreviewOutsideIframe key to on or off the outside iframe feature by default * test: added enableLivePreviewOutsideIframe key to on or off the outside iframe feature by default * Vp 444 by default bug fix (#558) * fix: live preview fixed new tab default setup * chore: updated readme file * fix: updated stackSDK usage in init new data flag --------- Co-authored-by: Mridul Sharma <mridul.sharma@contentstack.com> Co-authored-by: Mridul Sharma <109782971+contentstackMridul@users.noreply.github.com> * chore: audit fix --------- Co-authored-by: Hitesh Shetty <48558898+hiteshshetty-dev@users.noreply.github.com> Co-authored-by: hiteshshetty-dev <hitesh.shetty@contentstack.com> Co-authored-by: Karan Gandhi <karan.gandhi@contentstack.com> Co-authored-by: csAyushDubey <ayush.dubey@contentstack.com> Co-authored-by: Mridul Sharma <mridul.sharma@contentstack.com> Co-authored-by: Mridul Sharma <109782971+contentstackMridul@users.noreply.github.com>
1 parent 046f81d commit 1582dc8

35 files changed

Lines changed: 707 additions & 259 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Alternatively, if you want to include the package directly in your website HTML
1616

1717
```html
1818
<script type='module' crossorigin="anonymous">
19-
import ContentstackLivePreview from 'https://esm.sh/@contentstack/live-preview-utils@4.2.1';
19+
import ContentstackLivePreview from 'https://esm.sh/@contentstack/live-preview-utils@4.3.0';
2020
2121
ContentstackLivePreview.init({
2222
stackDetails: {

package-lock.json

Lines changed: 196 additions & 140 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/live-preview-utils",
3-
"version": "4.2.1",
3+
"version": "4.3.0",
44
"description": "Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.",
55
"type": "module",
66
"types": "dist/legacy/index.d.ts",
@@ -29,7 +29,7 @@
2929
"test:watch": "vitest",
3030
"test:once": "vitest run",
3131
"test:coverage": "vitest --coverage",
32-
"dev": "NODE_OPTIONS='--max-old-space-size=16384' tsup --watch",
32+
"dev": "NODE_OPTIONS='--max-old-space-size=16384' tsup --watch --config tsup.config.dev.js",
3333
"prepare": "husky",
3434
"lint": "eslint src",
3535
"lint:fix": "eslint --fix",
@@ -73,7 +73,7 @@
7373
"prettier-eslint": "^15.0.1",
7474
"ts-node": "^10.9.2",
7575
"tsc": "^2.0.4",
76-
"tsup": "^8.0.1",
76+
"tsup": "^8.5.1",
7777
"tsx": "^4.19.1",
7878
"typedoc": "^0.25.13",
7979
"typescript": "^5.4.5",

src/configManager/__test__/configManager.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Config, { updateConfigFromUrl } from "../configManager";
2-
import { getDefaultConfig } from "../config.default";
2+
import { getDefaultConfig, getUserInitData } from "../config.default";
33
import { DeepSignal } from "deepsignal";
44
import { IConfig } from "../../types/types";
55

@@ -90,6 +90,18 @@ describe("Config", () => {
9090
});
9191
});
9292

93+
describe("config default flags", () => {
94+
test("enableLivePreviewOutsideIframe defaults to undefined in getDefaultConfig", () => {
95+
const defaultConfig = getDefaultConfig();
96+
expect(defaultConfig.enableLivePreviewOutsideIframe).toBeUndefined();
97+
});
98+
99+
test("enableLivePreviewOutsideIframe defaults to undefined in getUserInitData", () => {
100+
const initData = getUserInitData();
101+
expect(initData.enableLivePreviewOutsideIframe).toBeUndefined();
102+
});
103+
});
104+
93105
describe("update config from url", () => {
94106
let config: DeepSignal<IConfig>;
95107

src/configManager/__test__/handleUserConfig.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,41 @@ describe("handleInitData()", () => {
411411
});
412412
});
413413

414+
describe("handleInitData() - enableLivePreviewOutsideIframe", () => {
415+
let config: DeepSignal<IConfig>;
416+
417+
beforeEach(() => {
418+
Config.reset();
419+
config = Config.get();
420+
});
421+
422+
afterAll(() => {
423+
Config.reset();
424+
});
425+
426+
test("should default to undefined when not provided", () => {
427+
const initData: Partial<IInitData> = {};
428+
handleInitData(initData);
429+
expect(config.enableLivePreviewOutsideIframe).toBeUndefined();
430+
});
431+
432+
test("should set to true when provided as true", () => {
433+
const initData: Partial<IInitData> = {
434+
enableLivePreviewOutsideIframe: true,
435+
};
436+
handleInitData(initData);
437+
expect(config.enableLivePreviewOutsideIframe).toBe(true);
438+
});
439+
440+
test("should remain false when provided as false", () => {
441+
const initData: Partial<IInitData> = {
442+
enableLivePreviewOutsideIframe: false,
443+
};
444+
handleInitData(initData);
445+
expect(config.enableLivePreviewOutsideIframe).toBe(false);
446+
});
447+
});
448+
414449
describe("handleClientUrlParams()", () => {
415450
let config: DeepSignal<IConfig>;
416451

src/configManager/config.default.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function getUserInitData(): IInitData {
3939
environment: "",
4040
},
4141
runScriptsOnUpdate: false,
42+
enableLivePreviewOutsideIframe: undefined,
4243
};
4344
}
4445

@@ -110,5 +111,6 @@ export function getDefaultConfig(): IConfig {
110111
},
111112
payload: [],
112113
},
114+
enableLivePreviewOutsideIframe: undefined,
113115
};
114116
}

src/configManager/handleUserConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ export const handleInitData = (initData: Partial<IInitData>): void => {
121121
"bottom-right",
122122
})
123123

124+
Config.set(
125+
"enableLivePreviewOutsideIframe",
126+
initData.enableLivePreviewOutsideIframe ??
127+
config.enableLivePreviewOutsideIframe
128+
);
129+
124130
// client URL params
125131
handleClientUrlParams(
126132
initData.clientUrlParams ??

src/cslp/__test__/cslpdata.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,124 @@
1-
import { extractDetailsFromCslp } from "../cslpdata";
1+
import { extractDetailsFromCslp, isValidCslp } from "../cslpdata";
2+
3+
describe("isValidCslp", () => {
4+
describe("valid cases", () => {
5+
test("should return true for valid v1 format with 3 parts", () => {
6+
expect(
7+
isValidCslp("content_type_uid.entry_uid.locale")
8+
).toBeTruthy();
9+
});
10+
11+
test("should return true for valid v1 format with field path", () => {
12+
expect(
13+
isValidCslp("content_type_uid.entry_uid.locale.field_path")
14+
).toBeTruthy();
15+
});
16+
17+
test("should return true for valid v2 format with 3 parts", () => {
18+
expect(
19+
isValidCslp("v2:content_type_uid.entry_uid_variant_uid.locale")
20+
).toBeTruthy();
21+
});
22+
23+
test("should return true for valid v2 format with field path", () => {
24+
expect(
25+
isValidCslp(
26+
"v2:content_type_uid.entry_uid_variant_uid.locale.field_path"
27+
)
28+
).toBeTruthy();
29+
});
30+
});
31+
32+
describe("invalid cases", () => {
33+
test("should return false for null", () => {
34+
expect(isValidCslp(null)).toBeFalsy();
35+
});
36+
37+
test("should return false for undefined", () => {
38+
expect(isValidCslp(undefined)).toBeFalsy();
39+
});
40+
41+
test("should return false for empty string", () => {
42+
expect(isValidCslp("")).toBeFalsy();
43+
});
44+
45+
test("should return false for string with less than 3 parts", () => {
46+
expect(isValidCslp("invalid")).toBeFalsy();
47+
});
48+
49+
test("should return false for string with only 2 parts", () => {
50+
expect(isValidCslp("a.b")).toBeFalsy();
51+
});
52+
53+
test("should return false for v2 prefix with no data", () => {
54+
expect(isValidCslp("v2:")).toBeFalsy();
55+
});
56+
57+
test("should return false for v2 prefix with only 2 parts", () => {
58+
expect(isValidCslp("v2:a.b")).toBeFalsy();
59+
});
60+
61+
test("should return false for v2 format where entry_uid_variant_uid has no underscore", () => {
62+
expect(
63+
isValidCslp("v2:content_type_uid.entry.locale")
64+
).toBeFalsy();
65+
});
66+
67+
test("should return false for v2 format where entry_uid_variant_uid is missing variant_uid", () => {
68+
expect(
69+
isValidCslp("v2:content_type_uid.entry_.locale")
70+
).toBeFalsy();
71+
});
72+
73+
test("should return false for v2 format where entry_uid_variant_uid is missing entry_uid", () => {
74+
expect(
75+
isValidCslp("v2:content_type_uid._variant_uid.locale")
76+
).toBeFalsy();
77+
});
78+
79+
test("should return false for v2 format with empty parts", () => {
80+
expect(isValidCslp("v2:..locale")).toBeFalsy();
81+
});
82+
83+
test("should return false for v2 format with empty content_type_uid", () => {
84+
expect(isValidCslp("v2:.entry_uid_variant_uid.locale")).toBeFalsy();
85+
});
86+
87+
test("should return false for v2 format with empty locale", () => {
88+
expect(
89+
isValidCslp("v2:content_type_uid.entry_uid_variant_uid.")
90+
).toBeFalsy();
91+
});
92+
93+
test("should return false for v1 format with empty parts", () => {
94+
expect(isValidCslp("..locale")).toBeFalsy();
95+
});
96+
97+
test("should return false for v1 format with empty content_type_uid", () => {
98+
expect(isValidCslp(".entry_uid.locale")).toBeFalsy();
99+
});
100+
101+
test("should return false for v1 format with empty entry_uid", () => {
102+
expect(isValidCslp("content_type_uid..locale")).toBeFalsy();
103+
});
104+
105+
test("should return false for whitespace-only string", () => {
106+
expect(isValidCslp(" ")).toBeFalsy();
107+
});
108+
109+
test("should return false for tab and newline whitespace", () => {
110+
expect(isValidCslp("\t\n")).toBeFalsy();
111+
});
112+
113+
test("should return false for string with only dots", () => {
114+
expect(isValidCslp("...")).toBeFalsy();
115+
});
116+
117+
test("should return false for string with only two dots", () => {
118+
expect(isValidCslp("..")).toBeFalsy();
119+
});
120+
});
121+
});
2122

3123
describe("extractDetailsFromCslp", () => {
4124
test("should extract details from a CSLP value string with nested multiple field", () => {

src/cslp/cslpdata.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,76 @@ import Config from "../configManager/configManager";
88
import { DeepSignal } from "deepsignal";
99
import { cslpTagStyles } from "../livePreview/editButton/editButton.style";
1010

11+
/**
12+
* Validates that the required CSLP parts (content_type_uid, entry_uid/entry_uid_variant_uid, locale) are non-empty.
13+
* @param parts The array of parts from splitting the CSLP string by "."
14+
* @returns `true` if all required parts (first 3) are non-empty, `false` otherwise.
15+
*/
16+
function areRequiredPartsNonEmpty(parts: string[]): boolean {
17+
// Check that we have at least 3 parts
18+
if (parts.length < 3) {
19+
return false;
20+
}
21+
// Verify that content_type_uid (parts[0]), entry_uid/entry_uid_variant_uid (parts[1]), and locale (parts[2]) are all non-empty
22+
return parts[0].length > 0 && parts[1].length > 0 && parts[2].length > 0;
23+
}
24+
25+
/**
26+
* Validates if a CSLP value string is valid.
27+
*
28+
* Supports two formats:
29+
* - **v1 format**: `content_type_uid.entry_uid.locale[.field_path]` (requires at least 3 parts)
30+
* - **v2 format**: `v2:content_type_uid.entry_uid_variant_uid.locale[.field_path]`
31+
* (requires at least 3 parts, entry_uid_variant_uid must contain underscore separating entry_uid and variant_uid)
32+
*
33+
* @param cslpValue The CSLP value string to validate (can be null or undefined).
34+
* @returns Type predicate: `true` if the CSLP value is valid (narrows type to `string`), `false` otherwise.
35+
*
36+
* @example
37+
* Valid v1 format
38+
* isValidCslp("page.entry123.en-us") -> true
39+
* isValidCslp("page.entry123.en-us.title") -> true
40+
*
41+
* Valid v2 format
42+
* isValidCslp("v2:page.entry123_variant456.en-us") -> true
43+
* isValidCslp("v2:page.entry123_variant456.en-us.title") -> true
44+
*
45+
* Invalid cases
46+
* isValidCslp(null) -> false
47+
* isValidCslp("invalid") -> false (less than 3 parts)
48+
* isValidCslp("v2:page.entry123.en-us") -> false (missing underscore in entry_uid_variant_uid)
49+
*/
50+
export function isValidCslp(
51+
cslpValue: string | null | undefined
52+
): cslpValue is string {
53+
// Return false for null, undefined, or empty string
54+
if (!cslpValue) {
55+
return false;
56+
}
57+
58+
// Check for v2 format (starts with "v2:")
59+
if (cslpValue.startsWith("v2:")) {
60+
const dataAfterPrefix = cslpValue.substring(3); // Remove "v2:" prefix
61+
const parts = dataAfterPrefix.split(".");
62+
// v2 format requires at least 3 parts: content_type_uid.entry_uid_variant_uid.locale
63+
// Verify that content_type_uid, entry_uid_variant_uid, and locale are all non-empty
64+
if (!areRequiredPartsNonEmpty(parts)) {
65+
return false;
66+
}
67+
// Verify that entry_uid_variant_uid (second part) contains both entry_uid and variant_uid separated by at least one underscore
68+
const entryUidVariantUid = parts[1];
69+
const entryVariantParts = entryUidVariantUid.split("_");
70+
// Check that we have at least 2 parts (entry_uid and variant_uid) and all parts are non-empty
71+
return entryVariantParts.length >= 2 && entryVariantParts.every((part) => part.length > 0);
72+
}
73+
74+
// v1 format (default, no prefix)
75+
const parts = cslpValue.split(".");
76+
// v1 format requires at least 3 parts: content_type_uid.entry_uid.locale
77+
// Verify that content_type_uid, entry_uid, and locale are all non-empty
78+
return areRequiredPartsNonEmpty(parts);
79+
}
80+
1181
/**
1282
* Extracts details from a CSLP value string.
1383
* @param cslpValue The CSLP value string to extract details from.
@@ -163,7 +233,7 @@ export function addCslpOutline(
163233

164234
const cslpTag = element.getAttribute("data-cslp");
165235

166-
if (trigger && cslpTag) {
236+
if (trigger && isValidCslp(cslpTag)) {
167237
if (elements.highlightedElement)
168238
elements.highlightedElement.classList.remove(
169239
cslpTagStyles()["cslp-edit-mode"]

src/livePreview/editButton/editButton.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { effect } from "@preact/signals";
22
import { inIframe, isOpeningInNewTab } from "../../common/inIframe";
33
import Config from "../../configManager/configManager";
4-
import { addCslpOutline, extractDetailsFromCslp } from "../../cslp";
4+
import { addCslpOutline, extractDetailsFromCslp, isValidCslp } from "../../cslp";
55
import { cslpTagStyles } from "./editButton.style";
66
import { PublicLogger } from "../../logger/logger";
77
import {
@@ -439,7 +439,7 @@ export class LivePreviewEditButton {
439439

440440
const cslpTag = this.tooltip.getAttribute("current-data-cslp");
441441

442-
if (cslpTag) {
442+
if (isValidCslp(cslpTag)) {
443443
const {
444444
content_type_uid,
445445
entry_uid,

0 commit comments

Comments
 (0)