@embeddable.com/sdk-core 3.9.3 → 3.9.5

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/lib/push.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare const YAML_OR_JS_FILES: RegExp;
1
+ export declare const CUBE_FILES: RegExp;
2
+ export declare const PRESET_FILES: RegExp;
2
3
  declare const _default: () => Promise<void>;
3
4
  export default _default;
4
5
  export declare function archive(ctx: any, yamlFiles: [string, string][], isDev?: boolean): Promise<unknown>;
package/lib/utils.d.ts CHANGED
@@ -21,3 +21,4 @@ export declare const removeBuildSuccessFlag: () => Promise<void>;
21
21
  */
22
22
  export declare const checkBuildSuccess: () => Promise<boolean>;
23
23
  export declare const getSDKVersions: () => Record<string, string>;
24
+ export declare const hrtimeToISO8601: (hrtime: number[] | null | undefined) => String;
package/lib/validate.d.ts CHANGED
@@ -2,3 +2,4 @@ declare const _default: (ctx: any, exitIfInvalid?: boolean) => Promise<boolean>;
2
2
  export default _default;
3
3
  export declare function dataModelsValidation(filesList: [string, string][]): Promise<string[]>;
4
4
  export declare function securityContextValidation(filesList: [string, string][]): Promise<string[]>;
5
+ export declare function clientContextValidation(filesList: [string, string][]): Promise<string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddable.com/sdk-core",
3
- "version": "3.9.3",
3
+ "version": "3.9.5",
4
4
  "description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
5
5
  "keywords": [
6
6
  "embeddable",
package/src/build.ts CHANGED
@@ -18,6 +18,7 @@ export default async () => {
18
18
  const breadcrumbs: string[] = [];
19
19
 
20
20
  try {
21
+ const startTime = process.hrtime();
21
22
  checkNodeVersion();
22
23
  breadcrumbs.push("checkNodeVersion");
23
24
  removeBuildSuccessFlag();
@@ -44,6 +45,8 @@ export default async () => {
44
45
  // NOTE: likely this will be called inside the loop above if we decide to support clients with mixed frameworks simultaneously.
45
46
  breadcrumbs.push("generate");
46
47
  await generate(config, "sdk-react");
48
+ // Calculating build time in seconds
49
+ config.buildTime = process.hrtime(startTime);
47
50
  breadcrumbs.push("cleanup");
48
51
  await cleanup(config);
49
52
  await storeBuildSuccessFlag();
@@ -9,6 +9,7 @@ const ctx = {
9
9
  stencilBuild: "stencilBuild",
10
10
  buildDir: "buildDir",
11
11
  },
12
+ buildTime: [80, 525000],
12
13
  };
13
14
 
14
15
  vi.mock("node:fs/promises", () => ({
@@ -77,6 +78,9 @@ describe("cleanup", () => {
77
78
  sdkVersions: {},
78
79
  packageManager: "npm",
79
80
  packageManagerVersion: "10.7.0",
81
+ metrics: {
82
+ buildTime: "PT1M20.001S",
83
+ },
80
84
  },
81
85
  }),
82
86
  );
package/src/cleanup.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { findFiles } from "@embeddable.com/sdk-utils";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { getSDKVersions } from "./utils";
4
+ import { getSDKVersions, hrtimeToISO8601 } from "./utils";
5
5
 
6
6
  export default async (ctx: any) => {
7
7
  await extractBuild(ctx);
@@ -55,6 +55,9 @@ export async function createManifest({
55
55
  sdkVersions,
56
56
  packageManager,
57
57
  packageManagerVersion,
58
+ metrics: {
59
+ buildTime: hrtimeToISO8601(ctx.buildTime),
60
+ },
58
61
  },
59
62
  };
60
63
 
@@ -25,6 +25,7 @@ const baseConfigArgs = {
25
25
  rollbarAccessToken: "rollbarAccessToken",
26
26
  previewBaseUrl: "previewBaseUrl",
27
27
  modelsSrc: "modelsSrc",
28
+ presetsSrc: "presetsSrc",
28
29
  };
29
30
 
30
31
  describe("defineConfig", () => {
@@ -55,6 +56,7 @@ describe("defineConfig", () => {
55
56
  "errorFallbackComponent": "/embeddable-sdk/packages/core-sdk",
56
57
  "globalCss": "/embeddable-sdk/packages/core-sdk",
57
58
  "modelsSrc": "/embeddable-sdk/packages/core-sdk",
59
+ "presetsSrc": "/embeddable-sdk/packages/core-sdk",
58
60
  "rollupOptions": {},
59
61
  "rootDir": "/embeddable-sdk/packages/core-sdk",
60
62
  "srcDir": "/embeddable-sdk/packages/core-sdk",
@@ -19,6 +19,7 @@ export type EmbeddableConfig = {
19
19
  previewBaseUrl?: string;
20
20
  componentsSrc?: string;
21
21
  modelsSrc?: string;
22
+ presetsSrc?: string;
22
23
  globalCss?: string;
23
24
  viteConfig?: {
24
25
  resolve?: {
@@ -39,6 +40,7 @@ export default ({
39
40
  rollbarAccessToken,
40
41
  previewBaseUrl,
41
42
  modelsSrc = "src",
43
+ presetsSrc = "src",
42
44
  componentsSrc = "src",
43
45
  globalCss = "src/global.css",
44
46
  viteConfig = {},
@@ -65,6 +67,14 @@ export default ({
65
67
  }
66
68
  }
67
69
 
70
+ if (presetsSrc && !path.isAbsolute(presetsSrc)) {
71
+ presetsSrc = path.resolve(clientRoot, presetsSrc);
72
+
73
+ if (!existsSync(presetsSrc)) {
74
+ throw new Error(`presetsSrc directory ${presetsSrc} does not exist`);
75
+ }
76
+ }
77
+
68
78
  return {
69
79
  core: {
70
80
  rootDir: coreRoot,
@@ -75,6 +85,7 @@ export default ({
75
85
  rootDir: clientRoot,
76
86
  srcDir: path.resolve(clientRoot, componentsSrc),
77
87
  modelsSrc: modelsSrc ? path.resolve(clientRoot, modelsSrc) : undefined,
88
+ presetsSrc: presetsSrc ? path.resolve(clientRoot, presetsSrc) : undefined,
78
89
  buildDir: path.resolve(clientRoot, ".embeddable-build"),
79
90
  tmpDir: path.resolve(clientRoot, ".embeddable-tmp"),
80
91
  globalCss: path.resolve(clientRoot, globalCss),
package/src/dev.test.ts CHANGED
@@ -46,6 +46,7 @@ const mockConfig = {
46
46
  tmpDir: "/mock/root/.embeddable-dev-tmp",
47
47
  globalCss: "/mock/root/global.css",
48
48
  modelsSrc: "/mock/root/models",
49
+ presetsSrc: "/mock/root/presets",
49
50
  },
50
51
  plugins: [],
51
52
  previewBaseUrl: "http://preview.example.com",
package/src/dev.ts CHANGED
@@ -21,7 +21,7 @@ import { FSWatcher } from "chokidar";
21
21
  import { getToken, default as login } from "./login";
22
22
  import axios from "axios";
23
23
  import { findFiles } from "@embeddable.com/sdk-utils";
24
- import { archive, YAML_OR_JS_FILES, sendBuild } from "./push";
24
+ import { archive, PRESET_FILES, CUBE_FILES, sendBuild } from "./push";
25
25
  import validate from "./validate";
26
26
  import { checkNodeVersion } from "./utils";
27
27
  import { createManifest } from "./cleanup";
@@ -191,7 +191,7 @@ export default async () => {
191
191
  editorsMetaFileName: "embeddable-editors-meta.js",
192
192
  });
193
193
 
194
- await sendDataModelsAndSecurityContextsChanges(config);
194
+ await sendDataModelsAndContextsChanges(config);
195
195
 
196
196
  for (const getPlugin of config.plugins) {
197
197
  const plugin = getPlugin();
@@ -274,11 +274,14 @@ const onBundleBuildEnd = async (ctx: any) => {
274
274
 
275
275
  const dataModelAndSecurityContextWatcher = (ctx: any): FSWatcher => {
276
276
  const fsWatcher = chokidar.watch(
277
- [path.resolve(ctx.client.modelsSrc, "**/*.{cube,sc}.{yaml,yml,js}")],
277
+ [
278
+ path.resolve(ctx.client.modelsSrc, "**/*.{cube}.{yaml,yml,js}"),
279
+ path.resolve(ctx.client.presetsSrc, "**/*.{sc,cc}.{yaml,yml}"),
280
+ ],
278
281
  chokidarWatchOptions,
279
282
  );
280
283
  fsWatcher.on("all", async () => {
281
- await sendDataModelsAndSecurityContextsChanges(ctx);
284
+ await sendDataModelsAndContextsChanges(ctx);
282
285
  });
283
286
 
284
287
  return fsWatcher;
@@ -294,7 +297,7 @@ const globalCssWatcher = (ctx: any): FSWatcher => {
294
297
  return fsWatcher;
295
298
  };
296
299
 
297
- const sendDataModelsAndSecurityContextsChanges = async (ctx: any) => {
300
+ const sendDataModelsAndContextsChanges = async (ctx: any) => {
298
301
  sendMessage("dataModelsAndOrSecurityContextUpdateStart");
299
302
  const isValid = await validate(ctx, false);
300
303
  if (isValid) {
@@ -302,7 +305,13 @@ const sendDataModelsAndSecurityContextsChanges = async (ctx: any) => {
302
305
  const sending = ora(
303
306
  "Synchronising data models and/or security contexts...",
304
307
  ).start();
305
- const filesList = await findFiles(ctx.client.modelsSrc, YAML_OR_JS_FILES);
308
+ const cubeFilesList = await findFiles(ctx.client.modelsSrc, CUBE_FILES);
309
+ const contextFilesList = await findFiles(
310
+ ctx.client.presetsSrc,
311
+ PRESET_FILES,
312
+ );
313
+
314
+ const filesList = [...cubeFilesList, ...contextFilesList];
306
315
 
307
316
  // add manifest to the archive
308
317
  filesList.push([
package/src/push.ts CHANGED
@@ -15,8 +15,11 @@ import { getToken } from "./login";
15
15
  import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
16
16
  import { selectWorkspace } from "./workspaceUtils";
17
17
 
18
- // grab .cube.yml|js and .sc.yml|js files
19
- export const YAML_OR_JS_FILES = /^(.*)\.(cube|sc)\.(ya?ml|js)$/;
18
+ // grab cube files
19
+ export const CUBE_FILES = /^(.*)\.cube\.(ya?ml|js)$/;
20
+
21
+ // grab security context and client context files
22
+ export const PRESET_FILES = /^(.*)\.(sc|cc)\.ya?ml$/;
20
23
 
21
24
  let ora: any;
22
25
  export default async () => {
@@ -134,12 +137,17 @@ async function verify(ctx: any) {
134
137
  async function buildArchive(config: any) {
135
138
  const spinnerArchive = ora("Building...").start();
136
139
 
137
- const filesList = await findFiles(
140
+ const cubeFilesList = await findFiles(
138
141
  config.client.modelsSrc || config.client.srcDir,
139
- YAML_OR_JS_FILES,
142
+ CUBE_FILES,
143
+ );
144
+
145
+ const contextFilesList = await findFiles(
146
+ config.client.presetsSrc || config.client.srcDir,
147
+ PRESET_FILES,
140
148
  );
141
149
 
142
- await archive(config, filesList);
150
+ await archive(config, [...cubeFilesList, ...contextFilesList]);
143
151
  return spinnerArchive.succeed("Bundling completed");
144
152
  }
145
153
 
package/src/utils.test.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  storeBuildSuccessFlag,
6
6
  checkBuildSuccess,
7
7
  SUCCESS_FLAG_FILE,
8
+ hrtimeToISO8601,
8
9
  } from "./utils";
9
10
 
10
11
  const startMock = {
@@ -114,4 +115,19 @@ describe("utils", () => {
114
115
  expect(result).toBe(false);
115
116
  });
116
117
  });
118
+
119
+ describe("hrtimeToISO8601", () => {
120
+ test.each([
121
+ { input: [0, 0], expected: "PT0.000S" },
122
+ { input: [75, 0], expected: "PT1M15.000S" }, // 1 minute and 15 seconds
123
+ { input: [2, 500000000], expected: "PT2.500S" }, // 2.5 seconds
124
+ { input: [0, 123456789], expected: "PT0.123S" }, // 0.123 seconds
125
+ { input: [0, 500000000], expected: "PT0.500S" }, // 0.5 seconds
126
+ { input: [1, 0], expected: "PT1.000S" }, // 1 second
127
+ { input: null, expected: "" },
128
+ { input: undefined, expected: "" },
129
+ ])("converts hrtime $input to $expected", ({ input, expected }) => {
130
+ expect(hrtimeToISO8601(input)).toBe(expected);
131
+ });
132
+ });
117
133
  });
package/src/utils.ts CHANGED
@@ -125,3 +125,21 @@ export const getSDKVersions = () => {
125
125
 
126
126
  return sdkVersions;
127
127
  };
128
+
129
+ export const hrtimeToISO8601 = (
130
+ hrtime: number[] | null | undefined,
131
+ ): String => {
132
+ if (hrtime === null || hrtime === undefined) {
133
+ return "";
134
+ }
135
+ const seconds = hrtime[0];
136
+ const nanoseconds = hrtime[1];
137
+
138
+ // Convert time components
139
+ const totalSeconds = seconds + nanoseconds / 1e9;
140
+ const minutes = Math.floor(totalSeconds / 60);
141
+ const remainingSeconds = totalSeconds % 60;
142
+
143
+ // Format ISO 8601 duration without hours
144
+ return `PT${minutes > 0 ? minutes + "M" : ""}${remainingSeconds.toFixed(3)}S`;
145
+ };
@@ -1,4 +1,8 @@
1
- import { dataModelsValidation, securityContextValidation } from "./validate";
1
+ import {
2
+ clientContextValidation,
3
+ dataModelsValidation,
4
+ securityContextValidation,
5
+ } from "./validate";
2
6
  import * as fs from "node:fs/promises";
3
7
 
4
8
  const startMock = {
@@ -35,6 +39,11 @@ const securityContextYaml = `
35
39
  country: United States
36
40
  environment: default`;
37
41
 
42
+ const clientContextYaml = `
43
+ - name: blue
44
+ clientContext:
45
+ color: blue`;
46
+
38
47
  vi.mock("ora", () => ({
39
48
  default: () => ({
40
49
  start: vi.fn().mockImplementation(() => startMock),
@@ -121,4 +130,30 @@ describe("validate", () => {
121
130
  ]);
122
131
  });
123
132
  });
133
+
134
+ describe("clientContextValidation", () => {
135
+ it("should return an empty array if the client context is valid", async () => {
136
+ vi.mocked(fs.readFile).mockImplementation(async () => {
137
+ return clientContextYaml;
138
+ });
139
+ const filesList: [string, string][] = [
140
+ ["valid-client-context.json", "path/to/file"],
141
+ ];
142
+ const result = await clientContextValidation(filesList);
143
+ expect(result).toEqual([]);
144
+ });
145
+
146
+ it("should return an array of error messages if the client context is invalid", async () => {
147
+ vi.mocked(fs.readFile).mockImplementation(async () => {
148
+ return `${clientContextYaml} ${clientContextYaml}`;
149
+ });
150
+ const filesList: [string, string][] = [
151
+ ["invalid-client-context.json", "path/to/file"],
152
+ ];
153
+ const result = await clientContextValidation(filesList);
154
+ expect(result).toEqual([
155
+ 'path/to/file: client context with name "blue" already exists',
156
+ ]);
157
+ });
158
+ });
124
159
  });
package/src/validate.ts CHANGED
@@ -6,6 +6,7 @@ import { checkNodeVersion } from "./utils";
6
6
 
7
7
  const CUBE_YAML_FILE_REGEX = /^(.*)\.cube\.ya?ml$/;
8
8
  const SECURITY_CONTEXT_FILE_REGEX = /^(.*)\.sc\.ya?ml$/;
9
+ const CLIENT_CONTEXT_FILE_REGEX = /^(.*)\.cc\.ya?ml$/;
9
10
 
10
11
  export default async (ctx: any, exitIfInvalid = true) => {
11
12
  checkNodeVersion();
@@ -13,13 +14,20 @@ export default async (ctx: any, exitIfInvalid = true) => {
13
14
 
14
15
  const spinnerValidate = ora("Data model validation...").start();
15
16
 
16
- const filesList = await findFiles(ctx.client.srcDir, CUBE_YAML_FILE_REGEX);
17
- const securityContextFilesList = await findFiles(
17
+ const cubeFilesList = await findFiles(
18
18
  ctx.client.modelsSrc || ctx.client.srcDir,
19
+ CUBE_YAML_FILE_REGEX,
20
+ );
21
+ const securityContextFilesList = await findFiles(
22
+ ctx.client.presetsSrc || ctx.client.srcDir,
19
23
  SECURITY_CONTEXT_FILE_REGEX,
20
24
  );
25
+ const clientContextFilesList = await findFiles(
26
+ ctx.client.presetsSrc || ctx.client.srcDir,
27
+ CLIENT_CONTEXT_FILE_REGEX,
28
+ );
21
29
 
22
- const dataModelErrors = await dataModelsValidation(filesList);
30
+ const dataModelErrors = await dataModelsValidation(cubeFilesList);
23
31
 
24
32
  if (dataModelErrors.length) {
25
33
  spinnerValidate.fail("One or more cube.yaml files are invalid:");
@@ -39,6 +47,10 @@ export default async (ctx: any, exitIfInvalid = true) => {
39
47
  securityContextFilesList,
40
48
  );
41
49
 
50
+ const clientContextErrors = await clientContextValidation(
51
+ clientContextFilesList,
52
+ );
53
+
42
54
  if (securityContextErrors.length) {
43
55
  spinnerValidate.fail("One or more security context files are invalid:");
44
56
 
@@ -51,7 +63,23 @@ export default async (ctx: any, exitIfInvalid = true) => {
51
63
  }
52
64
  }
53
65
 
54
- return dataModelErrors.length === 0 && securityContextErrors.length === 0;
66
+ if (clientContextErrors.length) {
67
+ spinnerValidate.fail("One or more client context files are invalid:");
68
+
69
+ clientContextErrors.forEach((errorMessage) =>
70
+ spinnerValidate.info(errorMessage),
71
+ );
72
+
73
+ if (exitIfInvalid) {
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ return (
79
+ dataModelErrors.length === 0 &&
80
+ securityContextErrors.length === 0 &&
81
+ clientContextErrors.length === 0
82
+ );
55
83
  };
56
84
 
57
85
  export async function dataModelsValidation(filesList: [string, string][]) {
@@ -118,6 +146,36 @@ export async function securityContextValidation(filesList: [string, string][]) {
118
146
  return errors;
119
147
  }
120
148
 
149
+ export async function clientContextValidation(filesList: [string, string][]) {
150
+ const errors: string[] = [];
151
+
152
+ const nameSet = new Set<string>();
153
+ for (const [_, filePath] of filesList) {
154
+ const fileContentRaw = await fs.readFile(filePath, "utf8");
155
+
156
+ const cube = YAML.parse(fileContentRaw);
157
+
158
+ cube.forEach((item: { name: string }) => {
159
+ if (nameSet.has(item.name)) {
160
+ errors.push(
161
+ `${filePath}: client context with name "${item.name}" already exists`,
162
+ );
163
+ } else {
164
+ nameSet.add(item.name);
165
+ }
166
+ });
167
+
168
+ const safeParse = clientContextSchema.safeParse(cube);
169
+ if (!safeParse.success) {
170
+ errorFormatter(safeParse.error.issues).forEach((error) => {
171
+ errors.push(`${filePath}: ${error}`);
172
+ });
173
+ }
174
+ }
175
+
176
+ return errors;
177
+ }
178
+
121
179
  enum MeasureTypeEnum {
122
180
  string = "string",
123
181
  time = "time",
@@ -194,3 +252,10 @@ const securityContextSchema = z.array(
194
252
  securityContext: z.object({}), // can be any object
195
253
  }),
196
254
  );
255
+
256
+ const clientContextSchema = z.array(
257
+ z.object({
258
+ name: z.string(),
259
+ clientContext: z.object({}), // can be any object
260
+ }),
261
+ );
@@ -13,6 +13,7 @@ export class EmbeddableComponent {
13
13
 
14
14
  @Prop() componentName: string;
15
15
  @Prop() props: string = '{}';
16
+ @Prop() clientContext: string = '{}';
16
17
 
17
18
  @Event({
18
19
  eventName: 'componentLoaded',
@@ -21,6 +22,7 @@ export class EmbeddableComponent {
21
22
  bubbles: true,
22
23
  }) componentDidLoadEvent: EventEmitter<any>;
23
24
 
25
+ @Watch('clientContext')
24
26
  @Watch('componentName')
25
27
  watchComponentName() {
26
28
  this.handlePops();
@@ -77,7 +79,8 @@ export class EmbeddableComponent {
77
79
  render(
78
80
  this.rootElement,
79
81
  this.componentName,
80
- JSON.parse(this.props)
82
+ JSON.parse(this.props),
83
+ JSON.parse(this.clientContext)
81
84
  );
82
85
  }
83
86
  }