@embeddable.com/sdk-core 4.3.3-next.0 → 4.4.0-next.0

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
@@ -3,6 +3,7 @@ import { ResolvedEmbeddableConfig } from "./defineConfig";
3
3
  export declare const CUBE_FILES: RegExp;
4
4
  export declare const CLIENT_CONTEXT_FILES: RegExp;
5
5
  export declare const SECURITY_CONTEXT_FILES: RegExp;
6
+ export declare const EMBEDDABLE_FILES: RegExp;
6
7
  declare const _default: () => Promise<void>;
7
8
  export default _default;
8
9
  export declare function buildArchive(config: ResolvedEmbeddableConfig): Promise<Ora>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddable.com/sdk-core",
3
- "version": "4.3.3-next.0",
3
+ "version": "4.4.0-next.0",
4
4
  "description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
5
5
  "keywords": [
6
6
  "embeddable",
@@ -89,6 +89,7 @@ describe("defineConfig", () => {
89
89
  "previewBaseUrl": "previewBaseUrl",
90
90
  "pushBaseUrl": "pushBaseUrl",
91
91
  "pushComponents": true,
92
+ "pushEmbeddables": true,
92
93
  "pushModels": true,
93
94
  "region": "legacy-US",
94
95
  "rollbarAccessToken": "rollbarAccessToken",
@@ -19,6 +19,7 @@ export type EmbeddableConfig = {
19
19
  })[];
20
20
  pushModels?: boolean;
21
21
  pushComponents?: boolean;
22
+ pushEmbeddables?: boolean;
22
23
  pushBaseUrl?: string;
23
24
  audienceUrl?: string;
24
25
  authDomain?: string;
@@ -83,6 +84,7 @@ export type ResolvedEmbeddableConfig = {
83
84
  };
84
85
  pushModels: boolean;
85
86
  pushComponents: boolean;
87
+ pushEmbeddables: boolean;
86
88
  pushBaseUrl: string;
87
89
  audienceUrl: string;
88
90
  previewBaseUrl: string;
@@ -152,6 +154,7 @@ export const embeddableConfigSchema = z
152
154
  ).optional(),
153
155
  pushModels: z.boolean().optional(),
154
156
  pushComponents: z.boolean().optional(),
157
+ pushEmbeddables: z.boolean().optional(),
155
158
  pushBaseUrl: z.string().optional(),
156
159
  audienceUrl: z.string().optional(),
157
160
  authDomain: z.string().optional(),
@@ -202,6 +205,7 @@ export default (config: EmbeddableConfig) => {
202
205
  pushModels = true,
203
206
  starterEmbeddables,
204
207
  pushComponents = true,
208
+ pushEmbeddables = true,
205
209
  pushBaseUrl,
206
210
  audienceUrl,
207
211
  authDomain,
@@ -308,6 +312,7 @@ export default (config: EmbeddableConfig) => {
308
312
  starterEmbeddables,
309
313
  pushModels,
310
314
  pushComponents,
315
+ pushEmbeddables,
311
316
  pushBaseUrl: pushBaseUrl ?? regionConfig.pushBaseUrl,
312
317
  audienceUrl: audienceUrl ?? regionConfig.audienceUrl,
313
318
  previewBaseUrl: previewBaseUrl ?? regionConfig.previewBaseUrl,
package/src/dev.test.ts CHANGED
@@ -29,7 +29,8 @@ import { logError } from "./logger";
29
29
  import { ResolvedEmbeddableConfig } from "./defineConfig";
30
30
  import { RollupWatcher } from "rollup";
31
31
  import ora from "ora";
32
- import { archive } from "./push";
32
+ import { archive, EMBEDDABLE_FILES } from "./push";
33
+ import fg from "fast-glob";
33
34
  import { selectWorkspace } from "./workspaceUtils";
34
35
  import serveStatic from "serve-static";
35
36
  import { createNodeSys } from "@stencil/core/sys/node";
@@ -107,6 +108,8 @@ vi.mock("serve-static", () => ({
107
108
  default: vi.fn(() => vi.fn()),
108
109
  }));
109
110
 
111
+ vi.mock("fast-glob", () => ({ default: vi.fn() }));
112
+
110
113
  vi.mock("./dev", async (importOriginal) => {
111
114
  const actual = await importOriginal<typeof dev>();
112
115
  return {
@@ -221,6 +224,8 @@ describe("dev command", () => {
221
224
  ["mock-model.json", "/mock/root/models/mock-model.json"],
222
225
  ]);
223
226
 
227
+ vi.mocked(fg).mockResolvedValue([]);
228
+
224
229
  // Mock fs functions
225
230
  vi.mocked(fs.readFile).mockResolvedValue("default content");
226
231
  vi.mocked(fs.appendFile).mockResolvedValue(undefined);
@@ -417,6 +422,49 @@ describe("dev command", () => {
417
422
  isDev: true,
418
423
  });
419
424
  });
425
+
426
+ it("should include embeddable files in archive when pushEmbeddables is true", async () => {
427
+ const embeddableConfig = {
428
+ ...mockConfig,
429
+ pushEmbeddables: true,
430
+ client: { ...mockConfig.client, srcDir: "/mock/src" },
431
+ } as unknown as ResolvedEmbeddableConfig;
432
+
433
+ vi.mocked(findFiles).mockImplementation(async (_dir, pattern) => {
434
+ if (pattern === EMBEDDABLE_FILES) {
435
+ return [["dashboard.embeddable.yaml", "/mock/src/dashboard.embeddable.yaml"]];
436
+ }
437
+ return [["mock-model.json", "/mock/root/models/mock-model.json"]];
438
+ });
439
+
440
+ await sendBuildChanges(embeddableConfig);
441
+
442
+ expect(findFiles).toHaveBeenCalledWith("/mock/src", EMBEDDABLE_FILES);
443
+ expect(archive).toHaveBeenCalledWith(
444
+ expect.objectContaining({
445
+ filesList: expect.arrayContaining([
446
+ ["dashboard.embeddable.yaml", "/mock/src/dashboard.embeddable.yaml"],
447
+ ]),
448
+ }),
449
+ );
450
+ });
451
+
452
+ it("should not call findFiles with EMBEDDABLE_FILES when pushEmbeddables is false", async () => {
453
+ const embeddableConfig = {
454
+ ...mockConfig,
455
+ pushEmbeddables: false,
456
+ client: { ...mockConfig.client, srcDir: "/mock/src" },
457
+ } as unknown as ResolvedEmbeddableConfig;
458
+
459
+ vi.mocked(findFiles).mockClear();
460
+
461
+ await sendBuildChanges(embeddableConfig);
462
+
463
+ const embeddableFilesCall = vi.mocked(findFiles).mock.calls.find(
464
+ (call) => call[1] === EMBEDDABLE_FILES,
465
+ );
466
+ expect(embeddableFilesCall).toBeUndefined();
467
+ });
420
468
  });
421
469
 
422
470
  describe("sendBuildChanges error handling", () => {
@@ -439,7 +487,7 @@ describe("dev command", () => {
439
487
  await sendBuildChanges(mockConfig as unknown as ResolvedEmbeddableConfig);
440
488
 
441
489
  expect(ora().fail).toHaveBeenCalledWith(
442
- `Data models and/or security context synchronization failed with error: ${error.message}`,
490
+ `Data models and/or security context and/or embeddables synchronization failed with error: ${error.message}`,
443
491
  );
444
492
  expect(mockWss.clients[0].send).toHaveBeenCalledWith(
445
493
  JSON.stringify({ type: "dataModelsAndOrSecurityContextUpdateError", error: error.message }),
@@ -447,6 +495,77 @@ describe("dev command", () => {
447
495
  });
448
496
  });
449
497
 
498
+ describe("cubeSecurityContextAndClientContextWatcher (pushEmbeddables)", () => {
499
+ it("should call fg with embeddable pattern and pass results to chokidar.watch when pushEmbeddables is true", async () => {
500
+ const mockEmbeddableFiles = [
501
+ "/mock/src/dashboard.embeddable.yaml",
502
+ "/mock/src/report.embeddable.yml",
503
+ ];
504
+
505
+ vi.mocked(fg).mockImplementation(async (pattern: any) => {
506
+ if (pattern === "**/*.embeddable.{yaml,yml}") {
507
+ return mockEmbeddableFiles as any;
508
+ }
509
+ return [];
510
+ });
511
+
512
+ vi.mocked(provideConfig).mockResolvedValue({
513
+ ...mockConfig,
514
+ pushEmbeddables: true,
515
+ client: { ...mockConfig.client, srcDir: "/mock/src" },
516
+ } as unknown as ResolvedEmbeddableConfig);
517
+
518
+ await dev();
519
+ await listenMock.mock.calls[0][1]();
520
+
521
+ expect(vi.mocked(fg)).toHaveBeenCalledWith(
522
+ "**/*.embeddable.{yaml,yml}",
523
+ expect.objectContaining({ cwd: "/mock/src", absolute: true }),
524
+ );
525
+
526
+ expect(chokidar.watch).toHaveBeenCalledWith(
527
+ expect.arrayContaining(mockEmbeddableFiles),
528
+ expect.anything(),
529
+ );
530
+ });
531
+
532
+ it("should not call fg with embeddable pattern when pushEmbeddables is false", async () => {
533
+ vi.mocked(provideConfig).mockResolvedValue({
534
+ ...mockConfig,
535
+ pushEmbeddables: false,
536
+ client: { ...mockConfig.client, srcDir: "/mock/src" },
537
+ } as unknown as ResolvedEmbeddableConfig);
538
+
539
+ vi.mocked(fg).mockClear();
540
+
541
+ await dev();
542
+ await listenMock.mock.calls[0][1]();
543
+
544
+ const embeddableCall = vi.mocked(fg).mock.calls.find(
545
+ (call) => call[0] === "**/*.embeddable.{yaml,yml}",
546
+ );
547
+ expect(embeddableCall).toBeUndefined();
548
+ });
549
+
550
+ it("should not call fg with embeddable pattern when pushEmbeddables is undefined", async () => {
551
+ vi.mocked(provideConfig).mockResolvedValue({
552
+ ...mockConfig,
553
+ // pushEmbeddables not set → undefined → falsy
554
+ client: { ...mockConfig.client, srcDir: "/mock/src" },
555
+ } as unknown as ResolvedEmbeddableConfig);
556
+
557
+ vi.mocked(fg).mockClear();
558
+
559
+ await dev();
560
+ await listenMock.mock.calls[0][1]();
561
+
562
+ const embeddableCall = vi.mocked(fg).mock.calls.find(
563
+ (call) => call[0] === "**/*.embeddable.{yaml,yml}",
564
+ );
565
+ expect(embeddableCall).toBeUndefined();
566
+ });
567
+ });
568
+
450
569
  describe("Plugin build coordination", () => {
451
570
  it("should handle configs with no plugins when pushComponents is true", async () => {
452
571
  vi.mocked(provideConfig).mockResolvedValue({
package/src/dev.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  sendBuild,
29
29
  SECURITY_CONTEXT_FILES,
30
30
  CLIENT_CONTEXT_FILES,
31
+ EMBEDDABLE_FILES,
31
32
  } from "./push";
32
33
  import validate from "./validate";
33
34
  import { checkNodeVersion } from "./utils";
@@ -494,6 +495,14 @@ const cubeSecurityContextAndClientContextWatcher = async (
494
495
  filesToWatch = [...filesToWatch, ...cubeFiles, ...securityContextFiles];
495
496
  }
496
497
 
498
+ if (ctx.pushEmbeddables) {
499
+ const embeddableFiles = await fg("**/*.embeddable.{yaml,yml}", {
500
+ cwd: ctx.client.srcDir,
501
+ absolute: true,
502
+ });
503
+ filesToWatch = [...filesToWatch, ...embeddableFiles];
504
+ }
505
+
497
506
  const fsWatcher = chokidar.watch(filesToWatch, chokidarWatchOptions);
498
507
 
499
508
  fsWatcher.on("all", () => sendBuildChanges(ctx));
@@ -526,7 +535,7 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
526
535
  sendMessage("dataModelsAndOrSecurityContextUpdateStart");
527
536
 
528
537
  const sending = ora(
529
- "Synchronising data models and/or security contexts...",
538
+ "Synchronising data models and/or security contexts and/or embeddables...",
530
539
  ).start();
531
540
 
532
541
  let filesList: [string, string][] = [];
@@ -567,6 +576,21 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
567
576
  filesList = [...filesList, ...cubeAndSecurityContextFileList];
568
577
  }
569
578
 
579
+ if (ctx.pushEmbeddables) {
580
+ const embeddableFilesList = await findFiles(
581
+ ctx.client.srcDir,
582
+ EMBEDDABLE_FILES,
583
+ );
584
+
585
+ filesList = [
586
+ ...filesList,
587
+ ...embeddableFilesList.map((entry): [string, string] => [
588
+ path.basename(entry[1]),
589
+ entry[1],
590
+ ]),
591
+ ];
592
+ }
593
+
570
594
  try {
571
595
  const token = await getToken();
572
596
  await archive({
@@ -578,12 +602,12 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
578
602
  } catch (e: any) {
579
603
  const errorMessage = e.response?.data?.errorMessage ?? e.message ?? "Unknown error";
580
604
  sending.fail(
581
- `Data models and/or security context synchronization failed with error: ${errorMessage}`,
605
+ `Data models and/or security context and/or embeddables synchronization failed with error: ${errorMessage}`,
582
606
  );
583
607
  return sendMessage("dataModelsAndOrSecurityContextUpdateError", { error: errorMessage });
584
608
  }
585
609
 
586
- sending.succeed(`Data models and/or security context synchronized`);
610
+ sending.succeed(`Data models and/or security context and/or embeddables synchronized`);
587
611
  sendMessage("dataModelsAndOrSecurityContextUpdateSuccess");
588
612
  };
589
613
 
package/src/push.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import push, { buildArchive } from "./push";
1
+ import push, { buildArchive, EMBEDDABLE_FILES } from "./push";
2
2
  import provideConfig from "./provideConfig";
3
3
  import { fileFromPath } from "formdata-node/file-from-path";
4
4
  import * as path from "path";
@@ -222,22 +222,24 @@ describe("push", () => {
222
222
  });
223
223
 
224
224
  describe("push configuration", () => {
225
- it("should fail if both pushModels and pushComponents are disabled", async () => {
225
+ it("should fail if pushModels, pushComponents, and pushEmbeddables are all disabled", async () => {
226
226
  vi.spyOn(console, "error").mockImplementation(() => undefined);
227
227
  vi.mocked(provideConfig).mockResolvedValue({
228
228
  ...config,
229
229
  pushModels: false,
230
230
  pushComponents: false,
231
+ pushEmbeddables: false,
231
232
  } as ResolvedEmbeddableConfig);
232
233
 
233
234
  await push();
234
235
 
235
236
  expect(startMock.fail).toHaveBeenCalledWith(
236
- "Cannot push: both pushModels and pushComponents are disabled"
237
+ "Cannot push: pushModels, pushComponents, and pushEmbeddables are all disabled"
237
238
  );
238
239
  expect(process.exit).toHaveBeenCalledWith(1);
239
240
  });
240
241
 
242
+
241
243
  it("should only include component files when pushModels is false", async () => {
242
244
  const localZipMock = {
243
245
  addFile: vi.fn(),
@@ -633,5 +635,116 @@ describe("push", () => {
633
635
  expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
634
636
  expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
635
637
  });
638
+
639
+ it("should include embeddable files when pushEmbeddables is true", async () => {
640
+ vi.mocked(findFiles)
641
+ .mockResolvedValueOnce([]) // cube files
642
+ .mockResolvedValueOnce([]) // security context files
643
+ .mockResolvedValueOnce([]) // client context files
644
+ .mockResolvedValueOnce([
645
+ ["dashboard.embeddable.yaml", "/src/dashboard.embeddable.yaml"],
646
+ ["report.embeddable.yml", "/src/report.embeddable.yml"],
647
+ ]); // embeddable files
648
+
649
+ const testConfig = {
650
+ ...config,
651
+ pushModels: true,
652
+ pushComponents: true,
653
+ pushEmbeddables: true,
654
+ client: {
655
+ ...config.client,
656
+ srcDir: "/src",
657
+ },
658
+ } as ResolvedEmbeddableConfig;
659
+
660
+ await buildArchive(testConfig);
661
+
662
+ expect(findFiles).toHaveBeenCalledWith("/src", EMBEDDABLE_FILES);
663
+ expect(mockZipLocal.addFile).toHaveBeenCalledWith(
664
+ "/src/dashboard.embeddable.yaml",
665
+ "dashboard.embeddable.yaml",
666
+ expect.objectContaining({ compress: true }),
667
+ );
668
+ expect(mockZipLocal.addFile).toHaveBeenCalledWith(
669
+ "/src/report.embeddable.yml",
670
+ "report.embeddable.yml",
671
+ expect.objectContaining({ compress: true }),
672
+ );
673
+ });
674
+
675
+ it("should not search for embeddable files when pushEmbeddables is false", async () => {
676
+ // Reset call history to avoid interference from previous tests
677
+ vi.mocked(findFiles).mockClear();
678
+
679
+ const testConfig = {
680
+ ...config,
681
+ pushModels: true,
682
+ pushComponents: true,
683
+ pushEmbeddables: false,
684
+ client: {
685
+ ...config.client,
686
+ srcDir: "/src",
687
+ },
688
+ } as ResolvedEmbeddableConfig;
689
+
690
+ await buildArchive(testConfig);
691
+
692
+ // findFiles should have been called for models and components but NOT for embeddable files
693
+ const embeddableFilesCall = vi
694
+ .mocked(findFiles)
695
+ .mock.calls.find((call) => call[1] === EMBEDDABLE_FILES);
696
+ expect(embeddableFilesCall).toBeUndefined();
697
+ });
698
+
699
+ it("should exit when all three push flags are disabled", async () => {
700
+ vi.spyOn(process, "exit").mockImplementation(() => null as never);
701
+
702
+ const testConfig = {
703
+ ...config,
704
+ pushModels: false,
705
+ pushComponents: false,
706
+ pushEmbeddables: false,
707
+ client: {
708
+ ...config.client,
709
+ srcDir: "/src",
710
+ },
711
+ } as ResolvedEmbeddableConfig;
712
+
713
+ await buildArchive(testConfig);
714
+
715
+ expect(process.exit).toHaveBeenCalledWith(1);
716
+ });
717
+ });
718
+
719
+ describe("EMBEDDABLE_FILES", () => {
720
+ it("should match .embeddable.yaml files", () => {
721
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yaml")).toBe(true);
722
+ expect(EMBEDDABLE_FILES.test("src/dashboards/sales.embeddable.yaml")).toBe(
723
+ true,
724
+ );
725
+ });
726
+
727
+ it("should match .embeddable.yml files", () => {
728
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yml")).toBe(true);
729
+ expect(EMBEDDABLE_FILES.test("src/widgets/chart.embeddable.yml")).toBe(
730
+ true,
731
+ );
732
+ });
733
+
734
+ it("should not match unrelated yaml files", () => {
735
+ expect(EMBEDDABLE_FILES.test("config.yaml")).toBe(false);
736
+ expect(EMBEDDABLE_FILES.test("my-dashboard.cc.yaml")).toBe(false);
737
+ expect(EMBEDDABLE_FILES.test("my-dashboard.sc.yaml")).toBe(false);
738
+ });
739
+
740
+ it("should not match cube files", () => {
741
+ expect(EMBEDDABLE_FILES.test("my-model.cube.yaml")).toBe(false);
742
+ expect(EMBEDDABLE_FILES.test("my-model.cube.yml")).toBe(false);
743
+ });
744
+
745
+ it("should not match non-yaml extensions", () => {
746
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.json")).toBe(false);
747
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.ts")).toBe(false);
748
+ });
636
749
  });
637
750
  });
package/src/push.ts CHANGED
@@ -21,6 +21,7 @@ export const CUBE_FILES = /^(.*)\.cube\.(ya?ml|js)$/;
21
21
 
22
22
  export const CLIENT_CONTEXT_FILES = /^(.*)\.cc\.ya?ml$/;
23
23
  export const SECURITY_CONTEXT_FILES = /^(.*)\.sc\.ya?ml$/;
24
+ export const EMBEDDABLE_FILES = /^(.*)\.embeddable\.ya?ml$/;
24
25
 
25
26
  export default async () => {
26
27
  await initLogger("push");
@@ -110,6 +111,7 @@ const publishedSectionFeedback = (
110
111
  ) => {
111
112
  config.pushModels && spinnerPushing.succeed("Models published");
112
113
  config.pushComponents && spinnerPushing.succeed("Components published");
114
+ config.pushEmbeddables && spinnerPushing.succeed("Embeddables published");
113
115
  };
114
116
 
115
117
  async function pushByApiKey(
@@ -170,9 +172,9 @@ async function verify(ctx: ResolvedEmbeddableConfig) {
170
172
  export async function buildArchive(config: ResolvedEmbeddableConfig) {
171
173
  const spinnerArchive = ora("Building...").start();
172
174
 
173
- if (!config.pushModels && !config.pushComponents) {
175
+ if (!config.pushModels && !config.pushComponents && !config.pushEmbeddables) {
174
176
  spinnerArchive.fail(
175
- "Cannot push: both pushModels and pushComponents are disabled",
177
+ "Cannot push: pushModels, pushComponents, and pushEmbeddables are all disabled",
176
178
  );
177
179
  process.exit(1);
178
180
  }
@@ -217,6 +219,20 @@ export async function buildArchive(config: ResolvedEmbeddableConfig) {
217
219
  );
218
220
  }
219
221
 
222
+ if (config.pushEmbeddables) {
223
+ const embeddableFilesList = await findFiles(
224
+ config.client.srcDir,
225
+ EMBEDDABLE_FILES,
226
+ );
227
+
228
+ filesList.push(
229
+ ...embeddableFilesList.map((entry): [string, string] => [
230
+ path.basename(entry[1]),
231
+ entry[1],
232
+ ]),
233
+ );
234
+ }
235
+
220
236
  await archive({
221
237
  ctx: config,
222
238
  filesList,