(element: T, id: string) => T;
/** Screeb context API */
export type ScreebContextValues = {
@@ -467,6 +470,9 @@ export type ScreebContextValues = {
sessionReplayStart: SessionReplayStartFunction;
sessionReplayStop: SessionReplayStopFunction;
targetingDebug: TargetingDebugFunction;
+ ScreebMaskText: ScreebMaskTextFunction;
+ ScreebNoCapture: ScreebNoCaptureFunction;
+ ScreebId: ScreebIdFunction;
};
/** Properties of Screeb provider */
diff --git a/packages/sdk-svelte/.eslintrc b/packages/sdk-svelte/.eslintrc
new file mode 100644
index 0000000..903ef04
--- /dev/null
+++ b/packages/sdk-svelte/.eslintrc
@@ -0,0 +1,8 @@
+{
+ "ignorePatterns": [
+ "dist"
+ ],
+ "extends": [
+ "@screeb/eslint-config"
+ ]
+}
diff --git a/packages/sdk-svelte/README.md b/packages/sdk-svelte/README.md
new file mode 100644
index 0000000..e29e7e4
--- /dev/null
+++ b/packages/sdk-svelte/README.md
@@ -0,0 +1,123 @@
+
+
+
+
+
+@screeb/sdk-svelte
+
+ Screeb's browser SDK, optimized for Svelte 4.
+
+ Continuous Product Discovery, Without the Time Sink.
+
+ Screeb is the only Continuous Product Discovery platform that lets you analyse users' behaviour, ask in-app questions, recruit people for interviews and analyse data in a blink with AI.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Installation
+
+This library is published in the NPM registry and can be installed using any compatible package manager.
+
+```sh
+npm install @screeb/sdk-svelte --save
+
+# For Yarn, use the command below.
+yarn add @screeb/sdk-svelte
+```
+
+## Package Size
+
+Current package size snapshot:
+
+- npm tarball: 7.1 KB
+- unpacked package: 42.3 KB
+
+## Usage
+
+Basic usage:
+
+Call `setScreebContext` once in the root component that wraps your app:
+
+```svelte
+
+
+
+
+```
+
+## Usage
+
+Use `useScreeb()` in any child component:
+
+```svelte
+
+
+ eventTrack("button-clicked")}>
+ Track event
+
+```
+
+## Identify after login
+
+```svelte
+
+
+```
+
+For a working example, see our [Screeb Svelte SDK example app](https://github.com/ScreebApp/sdk/tree/master/examples/example-svelte).
+
+## Documentation
+
+- Install guide: [developers.screeb.app/sdk-svelte/install](https://developers.screeb.app/sdk-svelte/install)
+- API reference: [developers.screeb.app/sdk-svelte/reference](https://developers.screeb.app/sdk-svelte/reference)
+
+## Support
+
+For any issues, please contact our support team at support@screeb.com.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
+
+## License
+
+Released under [MIT License](https://github.com/ScreebApp/sdk/blob/master/LICENSE).
diff --git a/packages/sdk-svelte/package.json b/packages/sdk-svelte/package.json
new file mode 100644
index 0000000..a639c49
--- /dev/null
+++ b/packages/sdk-svelte/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@screeb/sdk-svelte",
+ "version": "0.1.0",
+ "description": "Screeb's browser SDK, optimized for Svelte.",
+ "keywords": [
+ "product discovery",
+ "product management",
+ "survey",
+ "analytics",
+ "user feedback",
+ "user research",
+ "svelte"
+ ],
+ "homepage": "https://screeb.app",
+ "bugs": {
+ "url": "https://github.com/ScreebApp/sdk/issues",
+ "email": "support@screeb.app"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ScreebApp/sdk.git",
+ "directory": "packages/sdk-svelte"
+ },
+ "license": "MIT",
+ "author": "Screeb's frontend team",
+ "sideEffects": false,
+ "type": "module",
+ "module": "dist/es/index.mjs",
+ "main": "dist/cjs/index.cjs",
+ "types": "dist/es/index.d.ts",
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "rollup -c ../../node_modules/@screeb/typescript-config/src/rollup.config.js",
+ "clean": "rm -Rf dist",
+ "lint": "eslint .",
+ "release:check": "npm run build --workspace=@screeb/sdk-browser && npm run build && npm run lint"
+ },
+ "dependencies": {
+ "@screeb/sdk-browser": "^0.6.0"
+ },
+ "devDependencies": {
+ "@screeb/eslint-config": "^0.1.6",
+ "@screeb/typescript-config": "^0.1.10",
+ "@types/node": "^20.8.4",
+ "@typescript-eslint/eslint-plugin": "^6.7.5",
+ "eslint": "^8.51.0",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-prettier": "^5.0.1",
+ "prettier": "^3.0.3",
+ "rollup": "^4.0.2",
+ "svelte": ">=4.0.0",
+ "typescript": "^5.2.2"
+ },
+ "peerDependencies": {
+ "svelte": ">=4.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/sdk-svelte/readme/screeb-logo.svg b/packages/sdk-svelte/readme/screeb-logo.svg
new file mode 100644
index 0000000..b74bd6e
--- /dev/null
+++ b/packages/sdk-svelte/readme/screeb-logo.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/sdk-svelte/src/client.ts b/packages/sdk-svelte/src/client.ts
new file mode 100644
index 0000000..5002b51
--- /dev/null
+++ b/packages/sdk-svelte/src/client.ts
@@ -0,0 +1,166 @@
+import * as Screeb from "@screeb/sdk-browser";
+
+import CONSTANTS from "./constants";
+import * as logger from "./logger";
+import { ScreebClient, ScreebConfig } from "./types";
+
+export const createScreebClient = (config: ScreebConfig): ScreebClient => {
+ let isInitialized = false;
+
+ const ensureScreeb = async function ReturnType>(
+ functionName: string,
+ callback: T,
+ onlyLoaded = false,
+ ): Promise> {
+ const shouldLoad = config.shouldLoad ?? true;
+ if (!Screeb.isLoaded() && !shouldLoad) {
+ const message =
+ "Screeb instance is not loaded because `shouldLoad` is set to `false` in the Svelte provider";
+ logger.log("warn", message);
+ return Promise.reject(message);
+ }
+
+ if (!isInitialized && !onlyLoaded) {
+ const message = [
+ `"${functionName}" was called but Screeb has not been initialized yet. `,
+ `Please call 'init' before calling '${functionName}' or `,
+ "set 'autoInit' to true in the Svelte provider config.",
+ ].join("");
+ logger.log("warn", message);
+ return Promise.reject(message);
+ }
+
+ return Promise.resolve(callback());
+ };
+
+ const init = async (
+ websiteId: string,
+ userId?: string,
+ userProperties?: Screeb.PropertyRecord,
+ hooks?: Screeb.HooksInit,
+ language?: string,
+ ) => {
+ await ensureScreeb(
+ "init",
+ () => {
+ if (!isInitialized) {
+ Screeb.init(websiteId, userId, userProperties, hooks, language);
+ isInitialized = true;
+ }
+ },
+ true,
+ );
+ };
+
+ const load = async (options?: Screeb.ScreebOptions) => {
+ if (!Screeb.isLoaded()) {
+ Screeb.load({
+ sdkName: "sdk-svelte",
+ sdkVersion: CONSTANTS.version,
+ ...options,
+ });
+ }
+
+ if (config.autoInit && !isInitialized) {
+ if (config.websiteId) {
+ await init(
+ config.websiteId,
+ config.userId,
+ config.userProperties,
+ config.hooks,
+ config.language,
+ );
+ } else {
+ logger.log(
+ "warn",
+ "autoInit is set to true, but no websiteId have been provided.",
+ );
+ }
+ }
+ };
+
+ const close = async () => {
+ if (Screeb.isLoaded()) {
+ await Screeb.close();
+ isInitialized = false;
+ }
+ };
+
+ return {
+ ScreebId: Screeb.ScreebId,
+ ScreebMaskText: Screeb.ScreebMaskText,
+ ScreebNoCapture: Screeb.ScreebNoCapture,
+ close,
+ debug: async () => ensureScreeb("debug", () => Screeb.debug()),
+ eventTrack: async (eventName, eventProperties) =>
+ ensureScreeb("eventTrack", () =>
+ Screeb.eventTrack(eventName, eventProperties),
+ ),
+ identity: async (userId, userProperties) =>
+ ensureScreeb("identity", () => Screeb.identity(userId, userProperties)),
+ identityGet: async () =>
+ ensureScreeb("identityGet", () => Screeb.identityGet()),
+ identityGroupAssign: async (groupName, groupType, groupProperties) =>
+ ensureScreeb("identityGroupAssign", () =>
+ Screeb.identityGroupAssign(groupName, groupType, groupProperties),
+ ),
+ identityGroupUnassign: async (groupName, groupType) =>
+ ensureScreeb("identityGroupUnassign", () =>
+ Screeb.identityGroupUnassign(groupName, groupType),
+ ),
+ identityProperties: async (userProperties) =>
+ ensureScreeb("identityProperties", () =>
+ Screeb.identityProperties(userProperties),
+ ),
+ identityReset: async () =>
+ ensureScreeb("identityReset", () => Screeb.identityReset()),
+ init,
+ load,
+ messageClose: async () =>
+ ensureScreeb("messageClose", () => Screeb.messageClose()),
+ messageStart: async (
+ messageId,
+ allowMultipleResponses,
+ hiddenFields,
+ hooks,
+ language,
+ ) =>
+ ensureScreeb("messageStart", () =>
+ Screeb.messageStart(
+ messageId,
+ allowMultipleResponses,
+ hiddenFields,
+ hooks,
+ language,
+ ),
+ ),
+ sessionReplayStart: async () =>
+ ensureScreeb("sessionReplayStart", () => Screeb.sessionReplayStart()),
+ sessionReplayStop: async () =>
+ ensureScreeb("sessionReplayStop", () => Screeb.sessionReplayStop()),
+ surveyClose: async () =>
+ ensureScreeb("surveyClose", () => Screeb.surveyClose()),
+ surveyStart: async (
+ surveyId,
+ distributionId,
+ allowMultipleResponses,
+ hiddenFields,
+ hooks,
+ language,
+ selectors,
+ ) =>
+ ensureScreeb("surveyStart", () =>
+ Screeb.surveyStart(
+ surveyId,
+ distributionId,
+ allowMultipleResponses,
+ hiddenFields,
+ hooks,
+ language,
+ selectors,
+ ),
+ ),
+ targetingDebug: async () =>
+ ensureScreeb("targetingDebug", () => Screeb.targetingDebug()),
+ };
+};
diff --git a/packages/sdk-svelte/src/constants.ts b/packages/sdk-svelte/src/constants.ts
new file mode 100644
index 0000000..fe9cb36
--- /dev/null
+++ b/packages/sdk-svelte/src/constants.ts
@@ -0,0 +1,3 @@
+const CONSTANTS = { version: "0.1.0" };
+
+export default CONSTANTS;
diff --git a/packages/sdk-svelte/src/context.ts b/packages/sdk-svelte/src/context.ts
new file mode 100644
index 0000000..a538615
--- /dev/null
+++ b/packages/sdk-svelte/src/context.ts
@@ -0,0 +1,35 @@
+import { getContext, setContext } from "svelte";
+
+import { createScreebClient } from "./client";
+import * as logger from "./logger";
+import { ScreebClient, ScreebConfig } from "./types";
+import { isSSR } from "./utils";
+
+export const SCREEB_CONTEXT_KEY = Symbol("screeb");
+
+export function setScreebContext(config: ScreebConfig): ScreebClient {
+ const client = createScreebClient(config);
+
+ setContext(SCREEB_CONTEXT_KEY, client);
+
+ if (!isSSR && (config.shouldLoad ?? true)) {
+ void client.load(config.options);
+ }
+
+ return client;
+}
+
+export const provideScreeb = setScreebContext;
+
+export function useScreeb(): ScreebClient {
+ const context = getContext(SCREEB_CONTEXT_KEY);
+
+ if (!context) {
+ logger.log(
+ "warn",
+ "`useScreeb` must be called inside a component tree where `setScreebContext` or `provideScreeb` has been called.",
+ );
+ }
+
+ return context as ScreebClient;
+}
diff --git a/packages/sdk-svelte/src/index.ts b/packages/sdk-svelte/src/index.ts
new file mode 100644
index 0000000..eb6b921
--- /dev/null
+++ b/packages/sdk-svelte/src/index.ts
@@ -0,0 +1,8 @@
+export { createScreebClient } from "./client";
+export {
+ provideScreeb,
+ SCREEB_CONTEXT_KEY,
+ setScreebContext,
+ useScreeb,
+} from "./context";
+export * from "./types";
diff --git a/packages/sdk-svelte/src/logger.ts b/packages/sdk-svelte/src/logger.ts
new file mode 100644
index 0000000..698a24c
--- /dev/null
+++ b/packages/sdk-svelte/src/logger.ts
@@ -0,0 +1,8 @@
+type LogLevel = "warn";
+
+export const log = (level: LogLevel, message: string) => {
+ if (level === "warn") {
+ // eslint-disable-next-line no-console
+ console.warn(message);
+ }
+};
diff --git a/packages/sdk-svelte/src/types.ts b/packages/sdk-svelte/src/types.ts
new file mode 100644
index 0000000..d60547d
--- /dev/null
+++ b/packages/sdk-svelte/src/types.ts
@@ -0,0 +1,132 @@
+/* eslint-disable no-unused-vars */
+import {
+ HooksInit,
+ HooksMessageStart,
+ HooksSurveyStart,
+ PropertyRecord,
+ ScreebIdentityGetReturn,
+ ScreebOptions,
+} from "@screeb/sdk-browser";
+
+/** Configuration for the Svelte Screeb provider. */
+export type ScreebConfig = {
+ /** Your website/channel id. */
+ websiteId: string;
+ /** The unique identifier of your user. */
+ userId?: string;
+ /** The properties of your user. */
+ userProperties?: PropertyRecord;
+ /** Hooks to define callbacks for Screeb events. */
+ hooks?: HooksInit;
+ /** Force a specific language (for example, "en"). Defaults to the browser language. */
+ language?: string;
+ /**
+ * Indicates if Screeb should be automatically loaded.
+ * Set to false to prevent the SDK from loading, for example during SSR or tests.
+ * @default true
+ */
+ shouldLoad?: boolean;
+ /**
+ * Indicates if Screeb should be automatically initialized after load.
+ * @default false
+ */
+ autoInit?: boolean;
+ /** Screeb tag loading options. */
+ options?: ScreebOptions;
+};
+
+export type CloseFunction = () => Promise;
+export type DebugFunction = () => Promise;
+
+export type EventTrackFunction = (
+ eventName: string,
+ eventProperties?: PropertyRecord,
+) => Promise;
+
+export type IdentityFunction = (
+ userId: string,
+ userProperties?: PropertyRecord,
+) => Promise;
+
+export type IdentityGetFunction = () => Promise;
+
+export type IdentityGroupAssignFunction = (
+ groupName: string,
+ groupType?: string,
+ groupProperties?: PropertyRecord,
+) => Promise;
+
+export type IdentityGroupUnassignFunction = (
+ groupName: string,
+ groupType?: string,
+) => Promise;
+
+export type IdentityPropertiesFunction = (
+ userProperties: PropertyRecord,
+) => Promise;
+
+export type IdentityResetFunction = () => Promise;
+
+export type InitFunction = (
+ websiteId: string,
+ userId?: string,
+ userProperties?: PropertyRecord,
+ hooks?: HooksInit,
+ language?: string,
+) => Promise;
+
+export type LoadFunction = (options?: ScreebOptions) => Promise;
+
+export type SurveyCloseFunction = () => Promise;
+
+export type SurveyStartFunction = (
+ surveyId: string,
+ distributionId?: string,
+ allowMultipleResponses?: boolean,
+ hiddenFields?: PropertyRecord,
+ hooks?: HooksSurveyStart,
+ language?: string,
+ selectors?: string | string[],
+) => Promise;
+
+export type MessageCloseFunction = () => Promise;
+
+export type MessageStartFunction = (
+ messageId: string,
+ allowMultipleResponses?: boolean,
+ hiddenFields?: PropertyRecord,
+ hooks?: HooksMessageStart,
+ language?: string,
+) => Promise;
+
+export type SessionReplayStopFunction = () => Promise;
+export type SessionReplayStartFunction = () => Promise;
+export type TargetingDebugFunction = () => Promise;
+export type ScreebMaskTextFunction = (element: T) => T;
+export type ScreebNoCaptureFunction = (element: T) => T;
+export type ScreebIdFunction = (element: T, id: string) => T;
+
+/** All Screeb methods provided by `useScreeb()`. */
+export type ScreebClient = {
+ close: CloseFunction;
+ debug: DebugFunction;
+ eventTrack: EventTrackFunction;
+ identity: IdentityFunction;
+ identityGet: IdentityGetFunction;
+ identityGroupAssign: IdentityGroupAssignFunction;
+ identityGroupUnassign: IdentityGroupUnassignFunction;
+ identityProperties: IdentityPropertiesFunction;
+ identityReset: IdentityResetFunction;
+ init: InitFunction;
+ load: LoadFunction;
+ messageClose: MessageCloseFunction;
+ messageStart: MessageStartFunction;
+ sessionReplayStart: SessionReplayStartFunction;
+ sessionReplayStop: SessionReplayStopFunction;
+ surveyClose: SurveyCloseFunction;
+ surveyStart: SurveyStartFunction;
+ targetingDebug: TargetingDebugFunction;
+ ScreebMaskText: ScreebMaskTextFunction;
+ ScreebNoCapture: ScreebNoCaptureFunction;
+ ScreebId: ScreebIdFunction;
+};
diff --git a/packages/sdk-svelte/src/utils.ts b/packages/sdk-svelte/src/utils.ts
new file mode 100644
index 0000000..19c0976
--- /dev/null
+++ b/packages/sdk-svelte/src/utils.ts
@@ -0,0 +1 @@
+export const isSSR = typeof window === "undefined";
diff --git a/packages/sdk-svelte/tsconfig.json b/packages/sdk-svelte/tsconfig.json
new file mode 100644
index 0000000..5c272f8
--- /dev/null
+++ b/packages/sdk-svelte/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@screeb/typescript-config/src/tsconfig.json",
+ "include": ["src"],
+ "exclude": ["dist", "**/*.test.ts"],
+ "compilerOptions": {
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "."
+ }
+}
diff --git a/packages/sdk-vue/docs/README.md b/packages/sdk-vue/docs/README.md
index c60574e..90115a1 100644
--- a/packages/sdk-vue/docs/README.md
+++ b/packages/sdk-vue/docs/README.md
@@ -21,6 +21,9 @@
- [MessageStartFunction](README.md#messagestartfunction)
- [ScreebConfig](README.md#screebconfig)
- [ScreebContextValues](README.md#screebcontextvalues)
+- [ScreebIdFunction](README.md#screebidfunction)
+- [ScreebMaskTextFunction](README.md#screebmasktextfunction)
+- [ScreebNoCaptureFunction](README.md#screebnocapturefunction)
- [SessionReplayStartFunction](README.md#sessionreplaystartfunction)
- [SessionReplayStopFunction](README.md#sessionreplaystopfunction)
- [SurveyCloseFunction](README.md#surveyclosefunction)
@@ -312,6 +315,9 @@ All Screeb methods provided via `useScreeb()`
| Name | Type |
| :------ | :------ |
+| `ScreebId` | [`ScreebIdFunction`](README.md#screebidfunction) |
+| `ScreebMaskText` | [`ScreebMaskTextFunction`](README.md#screebmasktextfunction) |
+| `ScreebNoCapture` | [`ScreebNoCaptureFunction`](README.md#screebnocapturefunction) |
| `close` | [`CloseFunction`](README.md#closefunction) |
| `debug` | [`DebugFunction`](README.md#debugfunction) |
| `eventTrack` | [`EventTrackFunction`](README.md#eventtrackfunction) |
@@ -333,6 +339,85 @@ All Screeb methods provided via `useScreeb()`
___
+### ScreebIdFunction
+
+Ƭ **ScreebIdFunction**: \(`element`: `T`, `id`: `string`) => `T`
+
+#### Type declaration
+
+â–¸ \<`T`\>(`element`, `id`): `T`
+
+##### Type parameters
+
+| Name | Type |
+| :------ | :------ |
+| `T` | extends `Element` |
+
+##### Parameters
+
+| Name | Type |
+| :------ | :------ |
+| `element` | `T` |
+| `id` | `string` |
+
+##### Returns
+
+`T`
+
+___
+
+### ScreebMaskTextFunction
+
+Ƭ **ScreebMaskTextFunction**: \(`element`: `T`) => `T`
+
+#### Type declaration
+
+â–¸ \<`T`\>(`element`): `T`
+
+##### Type parameters
+
+| Name | Type |
+| :------ | :------ |
+| `T` | extends `Element` |
+
+##### Parameters
+
+| Name | Type |
+| :------ | :------ |
+| `element` | `T` |
+
+##### Returns
+
+`T`
+
+___
+
+### ScreebNoCaptureFunction
+
+Ƭ **ScreebNoCaptureFunction**: \(`element`: `T`) => `T`
+
+#### Type declaration
+
+â–¸ \<`T`\>(`element`): `T`
+
+##### Type parameters
+
+| Name | Type |
+| :------ | :------ |
+| `T` | extends `Element` |
+
+##### Parameters
+
+| Name | Type |
+| :------ | :------ |
+| `element` | `T` |
+
+##### Returns
+
+`T`
+
+___
+
### SessionReplayStartFunction
Ƭ **SessionReplayStartFunction**: () => `Promise`\<`unknown`\>
diff --git a/packages/sdk-vue/src/plugin.ts b/packages/sdk-vue/src/plugin.ts
index 4f0eaed..2531c58 100644
--- a/packages/sdk-vue/src/plugin.ts
+++ b/packages/sdk-vue/src/plugin.ts
@@ -231,6 +231,9 @@ export const ScreebPlugin: Plugin = {
);
const context: ScreebContextValues = {
+ ScreebId: Screeb.ScreebId,
+ ScreebMaskText: Screeb.ScreebMaskText,
+ ScreebNoCapture: Screeb.ScreebNoCapture,
close,
debug,
eventTrack,
@@ -242,12 +245,12 @@ export const ScreebPlugin: Plugin = {
identityReset,
init,
load,
- surveyClose,
- surveyStart,
messageClose,
messageStart,
- sessionReplayStop,
sessionReplayStart,
+ sessionReplayStop,
+ surveyClose,
+ surveyStart,
targetingDebug,
};
diff --git a/packages/sdk-vue/src/types.ts b/packages/sdk-vue/src/types.ts
index 9d5c3fa..ded1330 100644
--- a/packages/sdk-vue/src/types.ts
+++ b/packages/sdk-vue/src/types.ts
@@ -103,6 +103,9 @@ export type MessageStartFunction = (
export type SessionReplayStopFunction = () => Promise;
export type SessionReplayStartFunction = () => Promise;
export type TargetingDebugFunction = () => Promise;
+export type ScreebMaskTextFunction = (element: T) => T;
+export type ScreebNoCaptureFunction = (element: T) => T;
+export type ScreebIdFunction = (element: T, id: string) => T;
/** All Screeb methods provided via `useScreeb()` */
export type ScreebContextValues = {
@@ -124,4 +127,7 @@ export type ScreebContextValues = {
sessionReplayStart: SessionReplayStartFunction;
sessionReplayStop: SessionReplayStopFunction;
targetingDebug: TargetingDebugFunction;
+ ScreebMaskText: ScreebMaskTextFunction;
+ ScreebNoCapture: ScreebNoCaptureFunction;
+ ScreebId: ScreebIdFunction;
};
diff --git a/scripts/sync-sdk-versions.mjs b/scripts/sync-sdk-versions.mjs
new file mode 100755
index 0000000..3a08b65
--- /dev/null
+++ b/scripts/sync-sdk-versions.mjs
@@ -0,0 +1,178 @@
+#!/usr/bin/env node
+
+import { readFileSync, writeFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const manifestPath = resolve(root, "sdk-versions.json");
+const semverPattern = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
+
+function readManifest() {
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
+}
+
+function writeJson(path, value) {
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+function setByPath(object, dottedPath, value) {
+ const parts = dottedPath.split(".");
+ let current = object;
+ for (const part of parts.slice(0, -1)) {
+ if (!current[part] || typeof current[part] !== "object") {
+ throw new Error(`Unknown version path: ${dottedPath}`);
+ }
+ current = current[part];
+ }
+ const leaf = parts.at(-1);
+ if (!leaf || !(leaf in current)) {
+ throw new Error(`Unknown version path: ${dottedPath}`);
+ }
+ current[leaf] = value;
+}
+
+function replaceInFile(file, pattern, replacement) {
+ const path = resolve(root, file);
+ const before = readFileSync(path, "utf8");
+ if (!pattern.test(before)) {
+ throw new Error(`No version match found in ${file}`);
+ }
+ const after = before.replace(pattern, replacement);
+ writeIfNeeded(file, before, after);
+ if (!check && after !== before) {
+ console.log(`${file}`);
+ }
+}
+
+function replacePackageVersion(file, version) {
+ const path = resolve(root, file);
+ const before = readFileSync(path, "utf8");
+ const json = JSON.parse(before);
+ json.version = version;
+ const after = `${JSON.stringify(json, null, 2)}\n`;
+ writeIfNeeded(file, before, after);
+ if (!check && after !== before) {
+ console.log(`${file}`);
+ }
+}
+
+function parseArgs(args) {
+ const sets = [];
+ let check = false;
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+ if (arg === "--check") {
+ check = true;
+ continue;
+ }
+ if (arg === "--set") {
+ const path = args[index + 1];
+ const version = args[index + 2];
+ if (!path || !version) {
+ throw new Error("Usage: node scripts/sync-sdk-versions.mjs --set ");
+ }
+ sets.push([path, version]);
+ index += 2;
+ continue;
+ }
+ throw new Error(`Unknown argument: ${arg}`);
+ }
+
+ return { check, sets };
+}
+
+const { check, sets } = parseArgs(process.argv.slice(2));
+const manifest = readManifest();
+const changedFiles = [];
+
+function writeIfNeeded(file, before, after) {
+ if (after === before) {
+ return;
+ }
+ if (check) {
+ changedFiles.push(file);
+ return;
+ }
+ writeFileSync(resolve(root, file), after);
+}
+
+for (const [path, version] of sets) {
+ if (!semverPattern.test(version)) {
+ throw new Error(`Invalid semantic version for ${path}: ${version}`);
+ }
+ setByPath(manifest, path, version);
+}
+
+if (sets.length > 0 && !check) {
+ writeJson(manifestPath, manifest);
+ console.log("sdk-versions.json");
+}
+
+const androidVersion = manifest.native.android;
+const iosVersion = manifest.native.ios;
+const flutterVersion = manifest.wrappers.flutter;
+const kmpVersion = manifest.wrappers.kmp;
+const mauiVersion = manifest.wrappers.maui;
+const reactNativeVersion = manifest.wrappers.reactNative;
+const svelteVersion = manifest.wrappers.svelte;
+
+replacePackageVersion("packages/sdk-reactnative/package.json", reactNativeVersion);
+replacePackageVersion("packages/sdk-svelte/package.json", svelteVersion);
+replaceInFile("packages/sdk-flutter/pubspec.yaml", /^version: .+$/m, `version: ${flutterVersion}`);
+replaceInFile("packages/sdk-flutter/android/build.gradle", /^version '.+'$/m, `version '${flutterVersion}'`);
+replaceInFile("packages/sdk-flutter/android/build.gradle", /api "app\.screeb\.sdk:survey:[^"]+"/, `api "app.screeb.sdk:survey:${androidVersion}"`);
+replaceInFile("packages/sdk-flutter/ios/plugin_screeb.podspec", /s\.version\s+=\s+'[^']+'/, `s.version = '${flutterVersion}'`);
+replaceInFile("packages/sdk-flutter/ios/plugin_screeb.podspec", /s\.dependency 'Screeb', '[^']+'/, `s.dependency 'Screeb', '${iosVersion}'`);
+replaceInFile("packages/sdk-reactnative/android/build.gradle", /api "app\.screeb\.sdk:survey:[^"]+"/, `api "app.screeb.sdk:survey:${androidVersion}"`);
+replaceInFile("packages/sdk-reactnative/ScreebReactNative.podspec", /s\.dependency "Screeb", '~> [^']+'/, `s.dependency "Screeb", '~> ${iosVersion}'`);
+replaceInFile(
+ "packages/sdk-reactnative/android/src/main/java/app/screeb/reactnative/ScreebReactNativeModule.kt",
+ /Screeb\.setSecondarySDK\("react-native", "[^"]+"\)/,
+ `Screeb.setSecondarySDK("react-native", "${reactNativeVersion}")`,
+);
+replaceInFile(
+ "packages/sdk-reactnative/ios/ScreebReactNative.swift",
+ /Screeb\.setSecondarySDK\(name: "react-native", version: "[^"]+"\)/,
+ `Screeb.setSecondarySDK(name: "react-native", version: "${reactNativeVersion}")`,
+);
+replaceInFile(
+ "packages/sdk-flutter/android/src/main/kotlin/app/screeb/plugin_screeb/PluginScreebPlugin.kt",
+ /Screeb\.setSecondarySDK\("flutter", "[^"]+"\)/,
+ `Screeb.setSecondarySDK("flutter", "${flutterVersion}")`,
+);
+replaceInFile(
+ "packages/sdk-flutter/ios/Classes/SwiftPluginScreebPlugin.swift",
+ /Screeb\.setSecondarySDK\(name: "flutter", version: "[^"]+"\)/,
+ `Screeb.setSecondarySDK(name: "flutter", version: "${flutterVersion}")`,
+);
+replaceInFile("packages/sdk-kmp/gradle.properties", /^VERSION_NAME=.*$/m, `VERSION_NAME=${kmpVersion}`);
+replaceInFile("packages/sdk-kmp/gradle.properties", /^SCREEB_ANDROID_SDK_VERSION=.*$/m, `SCREEB_ANDROID_SDK_VERSION=${androidVersion}`);
+replaceInFile("packages/sdk-kmp/gradle.properties", /^SCREEB_IOS_SDK_VERSION=.*$/m, `SCREEB_IOS_SDK_VERSION=${iosVersion}`);
+replaceInFile(
+ "packages/sdk-kmp/src/commonMain/kotlin/app/screeb/sdk/kmp/SdkVersion.kt",
+ /internal const val SDK_VERSION = "[^"]+"/,
+ `internal const val SDK_VERSION = "${kmpVersion}"`,
+);
+replaceInFile(
+ "packages/sdk-svelte/src/constants.ts",
+ /const CONSTANTS = \{ version: "[^"]+" \};/,
+ `const CONSTANTS = { version: "${svelteVersion}" };`,
+);
+replaceInFile("packages/sdk-maui/ScreebMaui.csproj", /()[^<]+(<\/Version>)/, `$1${mauiVersion}$2`);
+replaceInFile(
+ "packages/sdk-maui/ScreebMaui.csproj",
+ /(Include="app\.screeb\.sdk:survey"\s*\n\s*Version=")[^"]+(")/,
+ `$1${androidVersion}$2`,
+);
+replaceInFile("examples/example-android/app/build.gradle", /implementation 'app\.screeb\.sdk:survey:[^']+'/, `implementation 'app.screeb.sdk:survey:${androidVersion}'`);
+
+if (check && changedFiles.length > 0) {
+ console.error(`Version files are not synced:\n${changedFiles.map((file) => `- ${file}`).join("\n")}`);
+ process.exit(1);
+}
+
+if (check) {
+ console.log("Version files are synced.");
+}
diff --git a/sdk-versions.json b/sdk-versions.json
new file mode 100644
index 0000000..98b1edb
--- /dev/null
+++ b/sdk-versions.json
@@ -0,0 +1,13 @@
+{
+ "native": {
+ "android": "4.0.0",
+ "ios": "4.0.0"
+ },
+ "wrappers": {
+ "flutter": "3.1.0",
+ "kmp": "0.1.0",
+ "maui": "0.1.0",
+ "reactNative": "3.3.1",
+ "svelte": "0.1.0"
+ }
+}