@embeddable.com/sdk-core 3.13.0-next.1 → 3.13.0-next.3

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/src/push.test.ts CHANGED
@@ -1,14 +1,17 @@
1
- import push from "./push";
1
+ import push, { buildArchive } from "./push";
2
2
  import provideConfig from "./provideConfig";
3
3
  import { fileFromPath } from "formdata-node/file-from-path";
4
4
  import archiver from "archiver";
5
5
  import * as fs from "node:fs/promises";
6
6
  import * as fsSync from "node:fs";
7
7
  import { findFiles } from "@embeddable.com/sdk-utils";
8
+ import { ResolvedEmbeddableConfig } from "./defineConfig";
9
+ import { vi, describe, it, expect, beforeEach } from "vitest";
10
+ import { getArgumentByKey } from "./utils";
8
11
 
9
12
  // @ts-ignore
10
13
  import reportErrorToRollbar from "./rollbar.mjs";
11
- import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
14
+ import { checkBuildSuccess, checkNodeVersion } from "./utils";
12
15
  import { server } from "../../../mocks/server";
13
16
  import { http, HttpResponse } from "msw";
14
17
 
@@ -85,9 +88,11 @@ const config = {
85
88
  rootDir: "rootDir",
86
89
  buildDir: "buildDir",
87
90
  archiveFile: "embeddable-build.zip",
91
+ globalCss: "src/global.css",
88
92
  },
89
93
  pushBaseUrl: "http://localhost:3000",
90
94
  previewBaseUrl: "http://localhost:3000",
95
+ pushComponents: true,
91
96
  };
92
97
 
93
98
  describe("push", () => {
@@ -110,7 +115,6 @@ describe("push", () => {
110
115
  vi.mocked(fs.stat).mockResolvedValue({
111
116
  size: 100,
112
117
  } as any);
113
-
114
118
  vi.mocked(findFiles).mockResolvedValue([["fileName", "filePath"]]);
115
119
  vi.mocked(fileFromPath).mockReturnValue(
116
120
  new Blob([new ArrayBuffer(8)]) as any,
@@ -144,8 +148,8 @@ describe("push", () => {
144
148
  );
145
149
 
146
150
  expect(archiveMock.pipe).toHaveBeenCalled();
147
- expect(archiveMock.file).toHaveBeenCalledWith("filePath", {
148
- name: "filePath",
151
+ expect(archiveMock.file).toHaveBeenCalledWith("src/global.css", {
152
+ name: "global.css",
149
153
  });
150
154
  expect(archiveMock.directory).toHaveBeenCalledWith("buildDir", false);
151
155
  expect(archiveMock.finalize).toHaveBeenCalled();
@@ -205,4 +209,405 @@ describe("push", () => {
205
209
 
206
210
  expect(startMock.succeed).toHaveBeenCalledWith("Published using API key");
207
211
  });
212
+
213
+ describe("push configuration", () => {
214
+ it("should fail if both pushModels and pushComponents are disabled", async () => {
215
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
216
+ vi.mocked(provideConfig).mockResolvedValue({
217
+ ...config,
218
+ pushModels: false,
219
+ pushComponents: false,
220
+ });
221
+
222
+ await push();
223
+
224
+ expect(startMock.fail).toHaveBeenCalledWith(
225
+ "Cannot push: both pushModels and pushComponents are disabled",
226
+ );
227
+ expect(process.exit).toHaveBeenCalledWith(1);
228
+ });
229
+
230
+ it("should only include model files when pushComponents is false", async () => {
231
+ const mockArchiver = {
232
+ finalize: vi.fn(),
233
+ pipe: vi.fn(),
234
+ directory: vi.fn(),
235
+ file: vi.fn(),
236
+ };
237
+ vi.mocked(archiver.create).mockReturnValue(mockArchiver as any);
238
+
239
+ vi.mocked(provideConfig).mockResolvedValue({
240
+ ...config,
241
+ pushModels: true,
242
+ pushComponents: false,
243
+ });
244
+
245
+ await push();
246
+
247
+ // Should not include component build directory
248
+ expect(mockArchiver.directory).not.toHaveBeenCalled();
249
+ // Should include model files
250
+ expect(mockArchiver.file).toHaveBeenCalled();
251
+ });
252
+
253
+ it("should only include component files when pushModels is false", async () => {
254
+ const mockArchiver = {
255
+ finalize: vi.fn(),
256
+ pipe: vi.fn(),
257
+ directory: vi.fn(),
258
+ file: vi.fn(),
259
+ };
260
+ vi.mocked(archiver.create).mockReturnValue(mockArchiver as any);
261
+
262
+ vi.mocked(provideConfig).mockResolvedValue({
263
+ ...config,
264
+ pushModels: false,
265
+ pushComponents: true,
266
+ });
267
+
268
+ await push();
269
+
270
+ // Should include component build directory
271
+ expect(mockArchiver.directory).toHaveBeenCalled();
272
+ // Should not include model files (except global.css which is part of components)
273
+ expect(mockArchiver.file).toHaveBeenCalledTimes(1);
274
+ expect(mockArchiver.file).toHaveBeenCalledWith(expect.anything(), {
275
+ name: "global.css",
276
+ });
277
+ });
278
+ });
279
+
280
+ describe("API key validation", () => {
281
+ it("should fail if API key is not provided", async () => {
282
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
283
+ Object.defineProperties(process, {
284
+ argv: {
285
+ value: ["--api-key"],
286
+ },
287
+ });
288
+ vi.mocked(getArgumentByKey).mockReturnValue(undefined);
289
+
290
+ await push();
291
+
292
+ expect(startMock.fail).toHaveBeenCalledWith("No API key provided");
293
+ expect(process.exit).toHaveBeenCalledWith(1);
294
+ });
295
+
296
+ it("should fail if email is not provided with API key", async () => {
297
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
298
+ Object.defineProperties(process, {
299
+ argv: {
300
+ value: ["--api-key", "some-key"],
301
+ },
302
+ });
303
+ vi.mocked(getArgumentByKey)
304
+ .mockReturnValueOnce("some-key") // API key
305
+ .mockReturnValueOnce(undefined); // Email
306
+
307
+ await push();
308
+
309
+ expect(startMock.fail).toHaveBeenCalledWith(
310
+ "Invalid email provided. Please provide a valid email using --email (-e) flag",
311
+ );
312
+ expect(process.exit).toHaveBeenCalledWith(1);
313
+ });
314
+
315
+ it("should fail if email is invalid", async () => {
316
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
317
+ Object.defineProperties(process, {
318
+ argv: {
319
+ value: ["--api-key", "some-key", "--email", "invalid-email"],
320
+ },
321
+ });
322
+ vi.mocked(getArgumentByKey)
323
+ .mockReturnValueOnce("some-key") // API key
324
+ .mockReturnValueOnce("invalid-email"); // Invalid email
325
+
326
+ await push();
327
+
328
+ expect(startMock.fail).toHaveBeenCalledWith(
329
+ "Invalid email provided. Please provide a valid email using --email (-e) flag",
330
+ );
331
+ expect(process.exit).toHaveBeenCalledWith(1);
332
+ });
333
+
334
+ it("should accept optional message parameter", async () => {
335
+ Object.defineProperties(process, {
336
+ argv: {
337
+ value: [
338
+ "--api-key",
339
+ "some-key",
340
+ "--email",
341
+ "valid@email.com",
342
+ "--message",
343
+ "test message",
344
+ ],
345
+ },
346
+ });
347
+ vi.mocked(getArgumentByKey)
348
+ .mockReturnValueOnce("some-key") // API key
349
+ .mockReturnValueOnce("valid@email.com") // Email
350
+ .mockReturnValueOnce("test message"); // Message
351
+
352
+ await push();
353
+
354
+ expect(startMock.succeed).toHaveBeenCalledWith("Published using API key");
355
+ });
356
+ });
357
+
358
+ describe("error handling", () => {
359
+ beforeEach(() => {
360
+ // Reset all mocks to their default state
361
+ vi.mocked(getArgumentByKey).mockReturnValue(undefined);
362
+ Object.defineProperties(process, {
363
+ argv: {
364
+ value: [],
365
+ },
366
+ });
367
+ });
368
+
369
+ it("should fail if build directory does not exist", async () => {
370
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
371
+ vi.mocked(fs.access).mockRejectedValue(new Error("No such directory"));
372
+ vi.mocked(provideConfig).mockResolvedValue(config);
373
+
374
+ await push();
375
+
376
+ expect(console.error).toHaveBeenCalledWith(
377
+ "No embeddable build was produced.",
378
+ );
379
+ expect(process.exit).toHaveBeenCalledWith(1);
380
+ });
381
+
382
+ it("should fail if token is not available", async () => {
383
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
384
+ vi.mocked(fs.access).mockResolvedValue(undefined);
385
+ vi.mocked(fs.readFile).mockResolvedValue(Buffer.from("{}"));
386
+ vi.mocked(provideConfig).mockResolvedValue(config);
387
+
388
+ await push();
389
+
390
+ expect(console.error).toHaveBeenCalledWith(
391
+ "Expired token. Please login again.",
392
+ );
393
+ expect(process.exit).toHaveBeenCalledWith(1);
394
+ });
395
+
396
+ it("should handle and report errors during push", async () => {
397
+ const testError = new Error("Test error");
398
+ vi.mocked(provideConfig).mockRejectedValue(testError);
399
+ vi.mocked(fs.access).mockResolvedValue(undefined);
400
+ vi.mocked(fs.readFile).mockImplementation(async () =>
401
+ Buffer.from(`{"access_token":"mocked-token"}`),
402
+ );
403
+
404
+ await push();
405
+
406
+ expect(reportErrorToRollbar).toHaveBeenCalledWith(testError);
407
+ expect(process.exit).toHaveBeenCalledWith(1);
408
+ });
409
+ });
410
+
411
+ describe("buildArchive", () => {
412
+ type MockArchiver = {
413
+ finalize: ReturnType<typeof vi.fn>;
414
+ pipe: ReturnType<typeof vi.fn>;
415
+ directory: ReturnType<typeof vi.fn>;
416
+ file: ReturnType<typeof vi.fn>;
417
+ };
418
+
419
+ let mockArchiver: MockArchiver;
420
+ let mockOra: {
421
+ start: ReturnType<typeof vi.fn>;
422
+ succeed: ReturnType<typeof vi.fn>;
423
+ fail: ReturnType<typeof vi.fn>;
424
+ };
425
+
426
+ beforeEach(() => {
427
+ mockArchiver = {
428
+ finalize: vi.fn(),
429
+ pipe: vi.fn(),
430
+ directory: vi.fn(),
431
+ file: vi.fn(),
432
+ };
433
+
434
+ vi.mocked(archiver.create).mockReturnValue(mockArchiver as any);
435
+ vi.mocked(findFiles).mockResolvedValue([]);
436
+ });
437
+
438
+ it("should include all file types when both flags are true", async () => {
439
+ vi.mocked(findFiles)
440
+ .mockResolvedValueOnce([
441
+ ["model1.cube.yml", "/path/to/model1.cube.yml"],
442
+ ["model2.cube.yaml", "/path/to/model2.cube.yaml"],
443
+ ])
444
+ .mockResolvedValueOnce([
445
+ ["context1.sc.yml", "/path/to/context1.sc.yml"],
446
+ ["context2.cc.yml", "/path/to/context2.cc.yml"],
447
+ ]);
448
+
449
+ const testConfig = {
450
+ ...config,
451
+ pushModels: true,
452
+ pushComponents: true,
453
+ client: {
454
+ ...config.client,
455
+ srcDir: "/src",
456
+ },
457
+ } as ResolvedEmbeddableConfig;
458
+
459
+ await buildArchive(testConfig);
460
+
461
+ // Should include component build directory
462
+ expect(mockArchiver.directory).toHaveBeenCalledWith(
463
+ testConfig.client.buildDir,
464
+ false,
465
+ );
466
+ // Should include global.css
467
+ expect(mockArchiver.file).toHaveBeenCalledWith(
468
+ testConfig.client.globalCss,
469
+ {
470
+ name: "global.css",
471
+ },
472
+ );
473
+ // Should include all model files
474
+ expect(mockArchiver.file).toHaveBeenCalledWith(
475
+ "/path/to/model1.cube.yml",
476
+ {
477
+ name: "model1.cube.yml",
478
+ },
479
+ );
480
+ expect(mockArchiver.file).toHaveBeenCalledWith(
481
+ "/path/to/model2.cube.yaml",
482
+ {
483
+ name: "model2.cube.yaml",
484
+ },
485
+ );
486
+ // Should include all preset files
487
+ expect(mockArchiver.file).toHaveBeenCalledWith(
488
+ "/path/to/context1.sc.yml",
489
+ {
490
+ name: "context1.sc.yml",
491
+ },
492
+ );
493
+ expect(mockArchiver.file).toHaveBeenCalledWith(
494
+ "/path/to/context2.cc.yml",
495
+ {
496
+ name: "context2.cc.yml",
497
+ },
498
+ );
499
+ });
500
+
501
+ it("should only include model files when pushComponents is false", async () => {
502
+ vi.mocked(findFiles)
503
+ .mockResolvedValueOnce([["model.cube.yml", "/path/to/model.cube.yml"]])
504
+ .mockResolvedValueOnce([["context.sc.yml", "/path/to/context.sc.yml"]]);
505
+
506
+ const testConfig = {
507
+ ...config,
508
+ pushModels: true,
509
+ pushComponents: false,
510
+ client: {
511
+ ...config.client,
512
+ srcDir: "/src",
513
+ },
514
+ } as ResolvedEmbeddableConfig;
515
+
516
+ await buildArchive(testConfig);
517
+
518
+ // Should not include component build directory
519
+ expect(mockArchiver.directory).not.toHaveBeenCalled();
520
+ // Should not include global.css
521
+ expect(mockArchiver.file).not.toHaveBeenCalledWith(
522
+ expect.anything(),
523
+ expect.objectContaining({ name: "global.css" }),
524
+ );
525
+ // Should include model files
526
+ expect(mockArchiver.file).toHaveBeenCalledWith(
527
+ "/path/to/model.cube.yml",
528
+ {
529
+ name: "model.cube.yml",
530
+ },
531
+ );
532
+ expect(mockArchiver.file).toHaveBeenCalledWith(
533
+ "/path/to/context.sc.yml",
534
+ {
535
+ name: "context.sc.yml",
536
+ },
537
+ );
538
+ });
539
+
540
+ it("should only include component files when pushModels is false", async () => {
541
+ const testConfig = {
542
+ ...config,
543
+ pushModels: false,
544
+ pushComponents: true,
545
+ client: {
546
+ ...config.client,
547
+ srcDir: "/src",
548
+ },
549
+ } as ResolvedEmbeddableConfig;
550
+
551
+ await buildArchive(testConfig);
552
+
553
+ // Should include component build directory
554
+ expect(mockArchiver.directory).toHaveBeenCalledWith(
555
+ testConfig.client.buildDir,
556
+ false,
557
+ );
558
+ // Should include global.css
559
+ expect(mockArchiver.file).toHaveBeenCalledWith(
560
+ testConfig.client.globalCss,
561
+ {
562
+ name: "global.css",
563
+ },
564
+ );
565
+ // Should not include any model files
566
+ expect(findFiles).not.toHaveBeenCalled();
567
+ });
568
+
569
+ it("should search in custom directories for model files", async () => {
570
+ const testConfig = {
571
+ ...config,
572
+ pushModels: true,
573
+ pushComponents: true,
574
+ client: {
575
+ ...config.client,
576
+ srcDir: "/src",
577
+ modelsSrc: "/custom/models/path",
578
+ presetsSrc: "/custom/presets/path",
579
+ },
580
+ } as ResolvedEmbeddableConfig;
581
+
582
+ await buildArchive(testConfig);
583
+
584
+ expect(findFiles).toHaveBeenCalledWith(
585
+ "/custom/models/path",
586
+ expect.any(RegExp),
587
+ );
588
+ expect(findFiles).toHaveBeenCalledWith(
589
+ "/custom/presets/path",
590
+ expect.any(RegExp),
591
+ );
592
+ });
593
+
594
+ it("should use srcDir as fallback when modelsSrc/presetsSrc are not defined", async () => {
595
+ const testConfig = {
596
+ ...config,
597
+ pushModels: true,
598
+ pushComponents: true,
599
+ client: {
600
+ ...config.client,
601
+ srcDir: "/src",
602
+ modelsSrc: undefined,
603
+ presetsSrc: undefined,
604
+ },
605
+ } as ResolvedEmbeddableConfig;
606
+
607
+ await buildArchive(testConfig);
608
+
609
+ expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
610
+ expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
611
+ });
612
+ });
208
613
  });
package/src/push.ts CHANGED
@@ -14,6 +14,7 @@ import { findFiles } from "@embeddable.com/sdk-utils";
14
14
  import { getToken } from "./login";
15
15
  import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
16
16
  import { selectWorkspace } from "./workspaceUtils";
17
+ import { ResolvedEmbeddableConfig } from "./defineConfig";
17
18
 
18
19
  // grab cube files
19
20
  export const CUBE_FILES = /^(.*)\.cube\.(ya?ml|js)$/;
@@ -84,7 +85,7 @@ export default async () => {
84
85
  }
85
86
  };
86
87
 
87
- async function pushByApiKey(config: any, spinner: any) {
88
+ async function pushByApiKey(config: ResolvedEmbeddableConfig, spinner: any) {
88
89
  const apiKey = getArgumentByKey(["--api-key", "-k"]);
89
90
 
90
91
  if (!apiKey) {
@@ -113,7 +114,7 @@ async function pushByApiKey(config: any, spinner: any) {
113
114
  });
114
115
  }
115
116
 
116
- async function verify(ctx: any) {
117
+ async function verify(ctx: ResolvedEmbeddableConfig) {
117
118
  try {
118
119
  await fs.access(ctx.client.buildDir);
119
120
  } catch (_e) {
@@ -132,33 +133,55 @@ async function verify(ctx: any) {
132
133
  return token;
133
134
  }
134
135
 
135
- async function buildArchive(config: any) {
136
+ export async function buildArchive(config: ResolvedEmbeddableConfig) {
136
137
  const spinnerArchive = ora("Building...").start();
137
138
 
138
- const cubeFilesList = await findFiles(
139
- config.client.modelsSrc || config.client.srcDir,
140
- CUBE_FILES,
141
- );
139
+ if (!config.pushModels && !config.pushComponents) {
140
+ spinnerArchive.fail(
141
+ "Cannot push: both pushModels and pushComponents are disabled",
142
+ );
143
+ process.exit(1);
144
+ }
142
145
 
143
- const contextFilesList = await findFiles(
144
- config.client.presetsSrc || config.client.srcDir,
145
- PRESET_FILES,
146
- );
146
+ const filesList: [string, string][] = [];
147
147
 
148
- // Map the files to include their full filenames
149
- const filesList = [...cubeFilesList, ...contextFilesList].map(
150
- (entry): [string, string] => [path.basename(entry[1]), entry[1]],
151
- );
148
+ if (config.pushModels) {
149
+ const cubeFilesList = await findFiles(
150
+ config.client.modelsSrc || config.client.srcDir,
151
+ CUBE_FILES,
152
+ );
153
+ const contextFilesList = await findFiles(
154
+ config.client.presetsSrc || config.client.srcDir,
155
+ PRESET_FILES,
156
+ );
157
+ filesList.push(
158
+ ...cubeFilesList.map((entry): [string, string] => [
159
+ path.basename(entry[1]),
160
+ entry[1],
161
+ ]),
162
+ ...contextFilesList.map((entry): [string, string] => [
163
+ path.basename(entry[1]),
164
+ entry[1],
165
+ ]),
166
+ );
167
+ }
152
168
 
153
- await archive(config, filesList);
169
+ await archive({
170
+ ctx: config,
171
+ filesList,
172
+ isDev: false,
173
+ includeComponents: config.pushComponents,
174
+ });
154
175
  return spinnerArchive.succeed("Bundling completed");
155
176
  }
156
177
 
157
- export async function archive(
158
- ctx: any,
159
- yamlFiles: [string, string][],
160
- isDev: boolean = false,
161
- ) {
178
+ export async function archive(args: {
179
+ ctx: ResolvedEmbeddableConfig;
180
+ filesList: [string, string][];
181
+ isDev: boolean;
182
+ includeComponents: boolean;
183
+ }) {
184
+ const { ctx, filesList, isDev, includeComponents } = args;
162
185
  const output = fsSync.createWriteStream(ctx.client.archiveFile);
163
186
 
164
187
  const archive = archiver.create("zip", {
@@ -166,14 +189,14 @@ export async function archive(
166
189
  });
167
190
 
168
191
  archive.pipe(output);
169
- if (!isDev) {
192
+ if (!isDev && includeComponents) {
170
193
  archive.directory(ctx.client.buildDir, false);
171
194
  archive.file(ctx.client.globalCss, {
172
195
  name: "global.css",
173
196
  });
174
197
  }
175
198
 
176
- for (const fileData of yamlFiles) {
199
+ for (const fileData of filesList) {
177
200
  archive.file(fileData[1], {
178
201
  name: fileData[0],
179
202
  });
@@ -187,8 +210,12 @@ export async function archive(
187
210
  }
188
211
 
189
212
  export async function sendBuildByApiKey(
190
- ctx: any,
191
- { apiKey, email, message }: any,
213
+ ctx: ResolvedEmbeddableConfig,
214
+ {
215
+ apiKey,
216
+ email,
217
+ message,
218
+ }: { apiKey: string; email: string; message?: string },
192
219
  ) {
193
220
  const { FormData, Blob } = await import("formdata-node");
194
221
  const { fileFromPath } = await import("formdata-node/file-from-path");
@@ -218,7 +245,7 @@ export async function sendBuildByApiKey(
218
245
  }
219
246
 
220
247
  export async function sendBuild(
221
- ctx: any,
248
+ ctx: ResolvedEmbeddableConfig,
222
249
  { workspaceId, token }: { workspaceId: string; token: string },
223
250
  ) {
224
251
  const { FormData } = await import("formdata-node");