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

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",
3
+ "version": "4.4.0-next.1",
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;
@@ -107,9 +109,14 @@ export type ResolvedEmbeddableConfig = {
107
109
  buildName: string;
108
110
  componentsEntryPointFilename: string;
109
111
  };
112
+ pluginFlags?: PluginFlags;
110
113
  };
111
114
  };
112
115
 
116
+ export type PluginFlags = {
117
+ supportsOnComponentReadyHook: boolean;
118
+ }
119
+
113
120
  const REGION_CONFIGS = {
114
121
  EU: {
115
122
  pushBaseUrl: "https://api.eu.embeddable.com",
@@ -152,6 +159,7 @@ export const embeddableConfigSchema = z
152
159
  ).optional(),
153
160
  pushModels: z.boolean().optional(),
154
161
  pushComponents: z.boolean().optional(),
162
+ pushEmbeddables: z.boolean().optional(),
155
163
  pushBaseUrl: z.string().optional(),
156
164
  audienceUrl: z.string().optional(),
157
165
  authDomain: z.string().optional(),
@@ -202,6 +210,7 @@ export default (config: EmbeddableConfig) => {
202
210
  pushModels = true,
203
211
  starterEmbeddables,
204
212
  pushComponents = true,
213
+ pushEmbeddables = true,
205
214
  pushBaseUrl,
206
215
  audienceUrl,
207
216
  authDomain,
@@ -308,6 +317,7 @@ export default (config: EmbeddableConfig) => {
308
317
  starterEmbeddables,
309
318
  pushModels,
310
319
  pushComponents,
320
+ pushEmbeddables,
311
321
  pushBaseUrl: pushBaseUrl ?? regionConfig.pushBaseUrl,
312
322
  audienceUrl: audienceUrl ?? regionConfig.audienceUrl,
313
323
  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({
@@ -984,6 +1103,34 @@ describe("dev command", () => {
984
1103
 
985
1104
  process.argv = originalArgv;
986
1105
  });
1106
+
1107
+ it("should include pushEmbeddables in the dev-workspace request body when true", async () => {
1108
+ vi.mocked(provideConfig).mockResolvedValue({
1109
+ ...mockConfig,
1110
+ pushEmbeddables: true,
1111
+ } as unknown as ResolvedEmbeddableConfig);
1112
+
1113
+ await dev();
1114
+
1115
+ const devWorkspaceCall = vi.mocked(axios.post).mock.calls.find(
1116
+ (call) => String(call[0]).includes("dev-workspace"),
1117
+ );
1118
+ expect(devWorkspaceCall?.[1]).toMatchObject({ pushEmbeddables: true });
1119
+ });
1120
+
1121
+ it("should include pushEmbeddables in the dev-workspace request body when false", async () => {
1122
+ vi.mocked(provideConfig).mockResolvedValue({
1123
+ ...mockConfig,
1124
+ pushEmbeddables: false,
1125
+ } as unknown as ResolvedEmbeddableConfig);
1126
+
1127
+ await dev();
1128
+
1129
+ const devWorkspaceCall = vi.mocked(axios.post).mock.calls.find(
1130
+ (call) => String(call[0]).includes("dev-workspace"),
1131
+ );
1132
+ expect(devWorkspaceCall?.[1]).toMatchObject({ pushEmbeddables: false });
1133
+ });
987
1134
  });
988
1135
 
989
1136
  describe("addToGitignore", () => {
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
 
@@ -637,6 +661,7 @@ const getPreviewWorkspace = async (
637
661
  instanceUrl,
638
662
  pushModels: ctx.pushModels,
639
663
  pushComponents: ctx.pushComponents,
664
+ pushEmbeddables: ctx.pushEmbeddables,
640
665
  },
641
666
  {
642
667
  headers: {
@@ -254,9 +254,9 @@ describe("generateDTS", () => {
254
254
  "embeddable-wrapper.esm.js",
255
255
  ] as any);
256
256
  vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
257
- // Template contains both tokens so we can verify replacement
257
+ // Template contains all tokens so we can verify replacement
258
258
  vi.mocked(fs.readFile).mockResolvedValue(
259
- "replace-this-with-component-name {{RENDER_IMPORT}}",
259
+ "replace-this-with-component-name {{RENDER_IMPORT}} {{PLUGIN_FLAGS}}",
260
260
  );
261
261
  vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
262
262
  vi.mocked(createCompiler).mockResolvedValue({
@@ -290,6 +290,19 @@ describe("generateDTS", () => {
290
290
  );
291
291
  });
292
292
 
293
+ it("should replace {{PLUGIN_FLAGS}} token with empty pluginFlags", async () => {
294
+ await generateDTS(config as unknown as ResolvedEmbeddableConfig);
295
+
296
+ expect(fs.writeFile).toHaveBeenCalledWith(
297
+ "componentDir/component.tsx",
298
+ expect.stringContaining("const pluginFlags: Partial<PluginFlags> = {}"),
299
+ );
300
+ expect(fs.writeFile).toHaveBeenCalledWith(
301
+ "componentDir/component.tsx",
302
+ expect.not.stringContaining("{{PLUGIN_FLAGS}}"),
303
+ );
304
+ });
305
+
293
306
  it("should call loadConfig with devMode=false and sourceMap=false", async () => {
294
307
  await generateDTS(config as unknown as ResolvedEmbeddableConfig);
295
308
 
@@ -333,7 +346,7 @@ describe("injectBundleRender cross-platform paths", () => {
333
346
 
334
347
  beforeEach(() => {
335
348
  vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
336
- vi.mocked(fs.readFile).mockResolvedValue("{{RENDER_IMPORT}}");
349
+ vi.mocked(fs.readFile).mockResolvedValue("{{RENDER_IMPORT}}\n{{PLUGIN_FLAGS}}");
337
350
  });
338
351
 
339
352
  it("should use forward slashes in import when path.relative returns unix path", async () => {
@@ -363,6 +376,55 @@ describe("injectBundleRender cross-platform paths", () => {
363
376
  expect.stringContaining("import render from '../../buildDir/buildName/render.js'"),
364
377
  );
365
378
  });
379
+
380
+ it("should inject pluginFlags from config into component.tsx", async () => {
381
+ vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
382
+ const ctxWithPluginFlags = {
383
+ ...ctxWithFileName,
384
+ "sdk-react": {
385
+ ...ctxWithFileName["sdk-react"],
386
+ pluginFlags: { supportsOnComponentReadyHook: true },
387
+ },
388
+ };
389
+
390
+ await injectBundleRender(
391
+ ctxWithPluginFlags as unknown as ResolvedEmbeddableConfig,
392
+ "sdk-react",
393
+ );
394
+
395
+ expect(fs.writeFile).toHaveBeenCalledWith(
396
+ expect.any(String),
397
+ expect.stringContaining('const pluginFlags: Partial<PluginFlags> = {"supportsOnComponentReadyHook":true}'),
398
+ );
399
+ });
400
+
401
+ it("should inject empty pluginFlags when not present in config", async () => {
402
+ vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
403
+
404
+ await injectBundleRender(
405
+ ctxWithFileName as unknown as ResolvedEmbeddableConfig,
406
+ "sdk-react",
407
+ );
408
+
409
+ expect(fs.writeFile).toHaveBeenCalledWith(
410
+ expect.any(String),
411
+ expect.stringContaining("const pluginFlags: Partial<PluginFlags> = {}"),
412
+ );
413
+ });
414
+
415
+ it("should not leave {{PLUGIN_FLAGS}} token in output", async () => {
416
+ vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
417
+
418
+ await injectBundleRender(
419
+ ctxWithFileName as unknown as ResolvedEmbeddableConfig,
420
+ "sdk-react",
421
+ );
422
+
423
+ expect(fs.writeFile).toHaveBeenCalledWith(
424
+ expect.any(String),
425
+ expect.not.stringContaining("{{PLUGIN_FLAGS}}"),
426
+ );
427
+ });
366
428
  });
367
429
 
368
430
  describe("injectCSS cross-platform paths", () => {
package/src/generate.ts CHANGED
@@ -18,6 +18,7 @@ import type { Logger } from "@stencil/core/internal";
18
18
 
19
19
  const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}";
20
20
  const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";
21
+ const PLUGIN_FLAGS_TOKEN = "{{PLUGIN_FLAGS}}";
21
22
 
22
23
  // stencil doesn't support dynamic component tag name, so we need to replace it manually
23
24
  const COMPONENT_TAG_TOKEN = "replace-this-with-component-name";
@@ -166,6 +167,8 @@ export async function injectBundleRender(
166
167
  )
167
168
  .replaceAll("\\", "/");
168
169
  const importStr = `import render from '${importFilePath}/${ctx[pluginName].outputOptions.fileName}';`;
170
+ const pluginFlags = ctx[pluginName].pluginFlags ?? {};
171
+ const pluginFlagsStr = `const pluginFlags: Partial<PluginFlags> = ${JSON.stringify(pluginFlags)}`;
169
172
 
170
173
  let content = await fs.readFile(
171
174
  path.resolve(ctx.core.templatesDir, "component.tsx.template"),
@@ -178,7 +181,7 @@ export async function injectBundleRender(
178
181
 
179
182
  await fs.writeFile(
180
183
  path.resolve(ctx.client.componentDir, "component.tsx"),
181
- content.replace(RENDER_IMPORT_TOKEN, importStr),
184
+ content.replace(RENDER_IMPORT_TOKEN, importStr).replace(PLUGIN_FLAGS_TOKEN, pluginFlagsStr),
182
185
  );
183
186
  }
184
187
 
@@ -199,7 +202,7 @@ async function injectBundleRenderStub(
199
202
  content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component");
200
203
  await fs.writeFile(
201
204
  path.resolve(ctx.client.componentDir, "component.tsx"),
202
- content.replace(RENDER_IMPORT_TOKEN, stubStr),
205
+ content.replace(RENDER_IMPORT_TOKEN, stubStr).replace(PLUGIN_FLAGS_TOKEN, "const pluginFlags: Partial<PluginFlags> = {}"),
203
206
  );
204
207
  }
205
208
 
package/src/index.ts CHANGED
@@ -4,4 +4,4 @@ export { default as push } from "./push";
4
4
  export { default as dev } from "./dev";
5
5
  export { default as defineConfig } from "./defineConfig";
6
6
  export { default as buildPackage } from "./buildPackage";
7
- export type { ResolvedEmbeddableConfig } from "./defineConfig";
7
+ export type { ResolvedEmbeddableConfig, PluginFlags } from "./defineConfig";
package/src/push.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import push, { buildArchive } from "./push";
1
+ import push, { buildArchive, EMBEDDABLE_FILES, sendBuild, sendBuildByApiKey } 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,18 +222,19 @@ 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
  });
@@ -633,5 +634,164 @@ describe("push", () => {
633
634
  expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
634
635
  expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
635
636
  });
637
+
638
+ it("should include embeddable files when pushEmbeddables is true", async () => {
639
+ vi.mocked(findFiles)
640
+ .mockResolvedValueOnce([]) // cube files
641
+ .mockResolvedValueOnce([]) // security context files
642
+ .mockResolvedValueOnce([]) // client context files
643
+ .mockResolvedValueOnce([
644
+ ["dashboard.embeddable.yaml", "/src/dashboard.embeddable.yaml"],
645
+ ["report.embeddable.yml", "/src/report.embeddable.yml"],
646
+ ]); // embeddable files
647
+
648
+ const testConfig = {
649
+ ...config,
650
+ pushModels: true,
651
+ pushComponents: true,
652
+ pushEmbeddables: true,
653
+ client: {
654
+ ...config.client,
655
+ srcDir: "/src",
656
+ },
657
+ } as ResolvedEmbeddableConfig;
658
+
659
+ await buildArchive(testConfig);
660
+
661
+ expect(findFiles).toHaveBeenCalledWith("/src", EMBEDDABLE_FILES);
662
+ expect(mockZipLocal.addFile).toHaveBeenCalledWith(
663
+ "/src/dashboard.embeddable.yaml",
664
+ "dashboard.embeddable.yaml",
665
+ expect.objectContaining({ compress: true }),
666
+ );
667
+ expect(mockZipLocal.addFile).toHaveBeenCalledWith(
668
+ "/src/report.embeddable.yml",
669
+ "report.embeddable.yml",
670
+ expect.objectContaining({ compress: true }),
671
+ );
672
+ });
673
+
674
+ it("should not search for embeddable files when pushEmbeddables is false", async () => {
675
+ // Reset call history to avoid interference from previous tests
676
+ vi.mocked(findFiles).mockClear();
677
+
678
+ const testConfig = {
679
+ ...config,
680
+ pushModels: true,
681
+ pushComponents: true,
682
+ pushEmbeddables: false,
683
+ client: {
684
+ ...config.client,
685
+ srcDir: "/src",
686
+ },
687
+ } as ResolvedEmbeddableConfig;
688
+
689
+ await buildArchive(testConfig);
690
+
691
+ // findFiles should have been called for models and components but NOT for embeddable files
692
+ const embeddableFilesCall = vi
693
+ .mocked(findFiles)
694
+ .mock.calls.find((call) => call[1] === EMBEDDABLE_FILES);
695
+ expect(embeddableFilesCall).toBeUndefined();
696
+ });
697
+
698
+ it("should exit when all three push flags are disabled", async () => {
699
+ vi.spyOn(process, "exit").mockImplementation(() => null as never);
700
+
701
+ const testConfig = {
702
+ ...config,
703
+ pushModels: false,
704
+ pushComponents: false,
705
+ pushEmbeddables: false,
706
+ client: {
707
+ ...config.client,
708
+ srcDir: "/src",
709
+ },
710
+ } as ResolvedEmbeddableConfig;
711
+
712
+ await buildArchive(testConfig);
713
+
714
+ expect(process.exit).toHaveBeenCalledWith(1);
715
+ });
716
+ });
717
+
718
+ describe("EMBEDDABLE_FILES", () => {
719
+ it("should match .embeddable.yaml files", () => {
720
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yaml")).toBe(true);
721
+ expect(EMBEDDABLE_FILES.test("src/dashboards/sales.embeddable.yaml")).toBe(
722
+ true,
723
+ );
724
+ });
725
+
726
+ it("should match .embeddable.yml files", () => {
727
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yml")).toBe(true);
728
+ expect(EMBEDDABLE_FILES.test("src/widgets/chart.embeddable.yml")).toBe(
729
+ true,
730
+ );
731
+ });
732
+
733
+ it("should not match unrelated yaml files", () => {
734
+ expect(EMBEDDABLE_FILES.test("config.yaml")).toBe(false);
735
+ expect(EMBEDDABLE_FILES.test("my-dashboard.cc.yaml")).toBe(false);
736
+ expect(EMBEDDABLE_FILES.test("my-dashboard.sc.yaml")).toBe(false);
737
+ });
738
+
739
+ it("should not match cube files", () => {
740
+ expect(EMBEDDABLE_FILES.test("my-model.cube.yaml")).toBe(false);
741
+ expect(EMBEDDABLE_FILES.test("my-model.cube.yml")).toBe(false);
742
+ });
743
+
744
+ it("should not match non-yaml extensions", () => {
745
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.json")).toBe(false);
746
+ expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.ts")).toBe(false);
747
+ });
748
+ });
749
+
750
+ describe.each([
751
+ {
752
+ label: "sendBuild",
753
+ endpoint: "**/bundle/:workspaceId/upload",
754
+ invoke: (testConfig: ResolvedEmbeddableConfig) =>
755
+ sendBuild(testConfig, { workspaceId: "test-workspace", token: "test-token" }),
756
+ },
757
+ {
758
+ label: "sendBuildByApiKey",
759
+ endpoint: "**/bundle/upload",
760
+ invoke: (testConfig: ResolvedEmbeddableConfig) =>
761
+ sendBuildByApiKey(testConfig, { apiKey: "test-api-key", email: "test@example.com" }),
762
+ },
763
+ ])("$label", ({ endpoint, invoke }) => {
764
+ async function captureMetadata(pushEmbeddables: boolean) {
765
+ let capturedMetadata: Record<string, any> | undefined;
766
+
767
+ server.use(
768
+ http.post(endpoint, async ({ request }) => {
769
+ const formData = await request.formData();
770
+ const requestBlob = formData.get("request") as Blob;
771
+ capturedMetadata = JSON.parse(await requestBlob.text());
772
+ return HttpResponse.json({ bundleId: "mocked-bundle-id" });
773
+ }),
774
+ );
775
+
776
+ const testConfig = {
777
+ ...config,
778
+ pushEmbeddables,
779
+ region: "us",
780
+ starterEmbeddables: {},
781
+ } as unknown as ResolvedEmbeddableConfig;
782
+
783
+ await invoke(testConfig);
784
+ return capturedMetadata;
785
+ }
786
+
787
+ it("should include pushEmbeddables in form data metadata when true", async () => {
788
+ const metadata = await captureMetadata(true);
789
+ expect(metadata?.pushEmbeddables).toBe(true);
790
+ });
791
+
792
+ it("should include pushEmbeddables in form data metadata when false", async () => {
793
+ const metadata = await captureMetadata(false);
794
+ expect(metadata?.pushEmbeddables).toBe(false);
795
+ });
636
796
  });
637
797
  });