@embeddable.com/sdk-core 3.8.0-next.0 → 3.8.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.
@@ -0,0 +1,2 @@
1
+ export declare function getWorkspaces(ctx: any, token: string, workspaceSpinner: any): Promise<any>;
2
+ export declare function selectWorkspace(ora: any, ctx: any, token: string): Promise<any>;
@@ -59,8 +59,6 @@ const currentDir = dirname(fileURLToPath(import.meta.url));
59
59
 
60
60
  const isWindows = process.platform === "win32";
61
61
 
62
- const moduleCache = new Map();
63
-
64
62
  export const resolve = async (specifier, context, nextResolve) => {
65
63
  if (NON_JS_TS_EXTENSIONS.test(specifier)) {
66
64
  const mockModulePath = pathToFileURL(
@@ -74,10 +72,6 @@ export const resolve = async (specifier, context, nextResolve) => {
74
72
  };
75
73
  }
76
74
 
77
- if (moduleCache.has(specifier)) {
78
- return moduleCache.get(specifier);
79
- }
80
-
81
75
  const isTS = EXTENSIONS.some((ext) => specifier.endsWith(ext));
82
76
 
83
77
  // Entry point
@@ -88,15 +82,11 @@ export const resolve = async (specifier, context, nextResolve) => {
88
82
  ? pathToFileURL(specifier.replace(`${processCwd}/file:/`, "")).href
89
83
  : specifier;
90
84
 
91
- const resolved = {
85
+ return {
92
86
  format: isTS ? "ts" : undefined,
93
87
  url: entryPointPath,
94
88
  shortCircuit: true,
95
89
  };
96
-
97
- moduleCache.set(specifier, resolved);
98
-
99
- return resolved;
100
90
  }
101
91
 
102
92
  // Determine if this is a package import
@@ -131,17 +121,13 @@ export const resolve = async (specifier, context, nextResolve) => {
131
121
  !resolvedModule.resolvedFileName.includes("/node_modules/") &&
132
122
  EXTENSIONS.includes(resolvedModule.extension)
133
123
  ) {
134
- const resolved = {
124
+ return {
135
125
  format: "ts",
136
126
  url:
137
127
  pathToFileURL(resolvedModule.resolvedFileName).href +
138
128
  `?update=${Date.now()}`,
139
129
  shortCircuit: true,
140
130
  };
141
-
142
- moduleCache.set(specifier, resolved);
143
-
144
- return resolved;
145
131
  }
146
132
 
147
133
  // Fallback for non-TS files or unresolved modules
@@ -153,13 +139,7 @@ export const resolve = async (specifier, context, nextResolve) => {
153
139
  return nextResolve(specifierPathOrUrl);
154
140
  };
155
141
 
156
- const compiledModuleCache = new Map();
157
-
158
142
  export const load = async (url, context, nextLoad) => {
159
- if (compiledModuleCache.has(url)) {
160
- return compiledModuleCache.get(url);
161
- }
162
-
163
143
  if (context.format === "ts") {
164
144
  const { source } = await nextLoad(url, context);
165
145
  const code =
@@ -167,15 +147,11 @@ export const load = async (url, context, nextLoad) => {
167
147
 
168
148
  // Compile the code using @swc-node with the customer's tsconfig
169
149
  const compiled = await compile(code, fileURLToPath(url), tsconfig, true);
170
- const resolved = {
150
+ return {
171
151
  format: "module",
172
152
  source: compiled,
173
153
  shortCircuit: true,
174
154
  };
175
-
176
- compiledModuleCache.set(url, resolved);
177
-
178
- return resolved;
179
155
  } else {
180
156
  if (
181
157
  isWindows &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddable.com/sdk-core",
3
- "version": "3.8.0-next.0",
3
+ "version": "3.8.0",
4
4
  "description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
5
5
  "keywords": [
6
6
  "embeddable",
@@ -34,14 +34,14 @@
34
34
  "embeddable": "bin/embeddable"
35
35
  },
36
36
  "engines": {
37
- "node": ">=18.0.0"
37
+ "node": ">=20.0.0"
38
38
  },
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
41
  "@embeddable.com/sdk-utils": "*",
42
- "@inquirer/select": "^1.3.0",
43
- "@stencil/core": "^4.18.2",
44
- "@swc-node/register": "^1.10.9",
42
+ "@inquirer/prompts": "^7.0.0",
43
+ "@stencil/core": "^4.22.0",
44
+ "@swc-node/register": "^1.9.0",
45
45
  "archiver": "^5.3.1",
46
46
  "axios": "^1.7.2",
47
47
  "chokidar": "^3.6.0",
@@ -52,7 +52,7 @@
52
52
  "ora": "^8.0.1",
53
53
  "serve-static": "^1.15.0",
54
54
  "sorcery": "^0.11.0",
55
- "vite": "^5.3.2",
55
+ "vite": "^5.4.8",
56
56
  "ws": "^8.17.0",
57
57
  "yaml": "^2.3.3"
58
58
  },
package/src/dev.ts CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  createNodeSys,
12
12
  } from "@stencil/core/sys/node";
13
13
  import { RollupWatcher } from "rollup";
14
- import * as http from "node:http";
15
14
  import { IncomingMessage, Server, ServerResponse } from "http";
16
15
  import { ChildProcess } from "node:child_process";
17
16
  import { WebSocketServer, Server as WSServer } from "ws";
@@ -25,6 +24,7 @@ import { archive, YAML_OR_JS_FILES, sendBuild } from "./push";
25
24
  import validate from "./validate";
26
25
  import { checkNodeVersion } from "./utils";
27
26
  import { createManifest } from "./cleanup";
27
+ import { selectWorkspace } from "./workspaceUtils";
28
28
  const minimist = require("minimist");
29
29
 
30
30
  const oraP = import("ora");
@@ -57,24 +57,16 @@ const addToGitingore = async () => {
57
57
  }
58
58
  };
59
59
 
60
- const chokidarWatchOptions = {
61
- ignoreInitial: true,
62
- usePolling: false, // Ensure polling is disabled
63
- awaitWriteFinish: {
64
- stabilityThreshold: 200,
65
- pollInterval: 100,
66
- },
67
- };
68
-
69
60
  export default async () => {
70
61
  checkNodeVersion();
71
62
  addToGitingore();
72
63
 
64
+ const http = require("http");
73
65
  ora = (await oraP).default;
74
66
 
75
67
  process.on("warning", (e) => console.warn(e.stack));
76
68
 
77
- const logger = createNodeLogger();
69
+ const logger = createNodeLogger({ process });
78
70
  const sys = createNodeSys({ process });
79
71
 
80
72
  const defaultConfig = await provideConfig();
@@ -108,9 +100,11 @@ export default async () => {
108
100
  const workspacePreparation = ora("Preparing workspace...").start();
109
101
 
110
102
  try {
111
- previewWorkspace = await getPreviewWorkspace(config);
103
+ previewWorkspace = await getPreviewWorkspace(workspacePreparation, config);
112
104
  } catch (e: any) {
113
- workspacePreparation.fail(e.response.data?.errorMessage);
105
+ workspacePreparation.fail(
106
+ e.response?.data?.errorMessage || "Unknown error: " + e.message,
107
+ );
114
108
  process.exit(1);
115
109
  }
116
110
 
@@ -182,7 +176,6 @@ export default async () => {
182
176
 
183
177
  await plugin.validate(config);
184
178
  const watcher = await plugin.build(config);
185
-
186
179
  await configureWatcher(watcher, config);
187
180
  watchers.push(watcher);
188
181
  }
@@ -253,7 +246,9 @@ const onBundleBuildEnd = async (ctx: any) => {
253
246
  const dataModelAndSecurityContextWatcher = (ctx: any): FSWatcher => {
254
247
  const fsWatcher = chokidar.watch(
255
248
  [path.resolve(ctx.client.modelsSrc, "**/*.{cube,sc}.{yaml,yml,js}")],
256
- chokidarWatchOptions,
249
+ {
250
+ ignoreInitial: true,
251
+ },
257
252
  );
258
253
  fsWatcher.on("all", async () => {
259
254
  await sendDataModelsAndSecurityContextsChanges(ctx);
@@ -268,7 +263,9 @@ const customSelfServeWatcher = (ctx: any): FSWatcher => {
268
263
  path.resolve(ctx.client.selfServeCustomizationDir, "style.css"),
269
264
  path.resolve(ctx.client.selfServeCustomizationDir, "*.svg"),
270
265
  ],
271
- chokidarWatchOptions,
266
+ {
267
+ ignoreInitial: true,
268
+ },
272
269
  );
273
270
 
274
271
  fsWatcher.on("all", async () => {
@@ -326,11 +323,21 @@ const onClose = async (
326
323
  process.exit(0);
327
324
  };
328
325
 
329
- const getPreviewWorkspace = async (ctx: any): Promise<string> => {
326
+ const getPreviewWorkspace = async (
327
+ startedOra: any,
328
+ ctx: any,
329
+ ): Promise<string> => {
330
330
  const token = await getToken();
331
331
 
332
332
  const params = minimist(process.argv.slice(2));
333
- const primaryWorkspace = params.w || params.workspace;
333
+ let primaryWorkspace = params.w || params.workspace;
334
+
335
+ if (!primaryWorkspace) {
336
+ startedOra.stop(); // Stop current Ora, otherwise the last option will get hidden by it.
337
+ const { workspaceId } = await selectWorkspace(ora, ctx, token);
338
+ primaryWorkspace = workspaceId;
339
+ startedOra.start();
340
+ }
334
341
 
335
342
  try {
336
343
  const response = await axios.get(
@@ -347,7 +354,7 @@ const getPreviewWorkspace = async (ctx: any): Promise<string> => {
347
354
  if (e.response.status === 401) {
348
355
  // login and retry
349
356
  await login();
350
- return await getPreviewWorkspace(ctx);
357
+ return await getPreviewWorkspace(startedOra, ctx);
351
358
  } else {
352
359
  throw e;
353
360
  }
@@ -79,9 +79,7 @@ describe("generate", () => {
79
79
  config: {},
80
80
  } as any);
81
81
  vi.mocked(createCompiler).mockResolvedValue({
82
- build: vi.fn().mockResolvedValue({
83
- hasError: false,
84
- }),
82
+ build: vi.fn(),
85
83
  destroy: vi.fn(),
86
84
  } as any);
87
85
 
@@ -114,51 +112,4 @@ describe("generate", () => {
114
112
  "stencilBuild/embeddable-wrapper.esm-hash.js",
115
113
  );
116
114
  });
117
-
118
- it("should generate bundle in dev mode", async () => {
119
- const ctx = {
120
- ...config,
121
- dev: {
122
- logger: vi.fn(),
123
- sys: vi.fn(),
124
- },
125
- };
126
-
127
- vi.mocked(fs.readFile).mockResolvedValue(
128
- "replace-this-with-component-name",
129
- );
130
- await generate(ctx, "sdk-react");
131
-
132
- expect(createCompiler).toHaveBeenCalled();
133
-
134
- expect(fs.writeFile).toHaveBeenCalledWith(
135
- "componentDir/component.tsx",
136
- expect.stringContaining("embeddable-component"),
137
- );
138
-
139
- expect(loadConfig).toHaveBeenCalledWith({
140
- config: {
141
- configPath: "buildDir/stencil.config.ts",
142
- devMode: true,
143
- enableCache: true,
144
- maxConcurrentWorkers: 8,
145
- minifyCss: false,
146
- minifyJs: false,
147
- namespace: "embeddable-wrapper",
148
- outputTargets: [
149
- {
150
- type: "dist",
151
- },
152
- ],
153
- rootDir: "buildDir",
154
- sourceMap: false,
155
- srcDir: "buildDir/component",
156
- tsconfig: "buildDir/tsconfig.json",
157
- watchDirs: ["buildDir/buildName"],
158
- },
159
- initTsConfig: true,
160
- logger: expect.any(Function),
161
- sys: expect.any(Function),
162
- });
163
- });
164
115
  });
package/src/generate.ts CHANGED
@@ -96,8 +96,8 @@ async function addComponentTagName(filePath: string, bundleHash: string) {
96
96
  ]);
97
97
  }
98
98
 
99
- async function runStencil(ctx: any): Promise<void> {
100
- const logger = ctx.dev?.logger || createNodeLogger();
99
+ async function runStencil(ctx: any) {
100
+ const logger = ctx.dev?.logger || createNodeLogger({ process });
101
101
  const sys = ctx.dev?.sys || createNodeSys({ process });
102
102
  const devMode = !!ctx.dev;
103
103
 
@@ -109,56 +109,23 @@ async function runStencil(ctx: any): Promise<void> {
109
109
  sys,
110
110
  config: {
111
111
  devMode,
112
- enableCache: true,
113
112
  maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows
114
113
  rootDir: ctx.client.buildDir,
115
114
  configPath: path.resolve(ctx.client.buildDir, "stencil.config.ts"),
116
115
  tsconfig: path.resolve(ctx.client.buildDir, "tsconfig.json"),
117
116
  namespace: "embeddable-wrapper",
118
117
  srcDir: path.resolve(ctx.client.buildDir, "component"),
119
- sourceMap: !devMode,
120
- minifyJs: !devMode,
121
- minifyCss: !devMode,
122
118
  outputTargets: [
123
119
  {
124
120
  type: "dist",
125
121
  },
126
122
  ],
127
- watchDirs: [
128
- path.resolve(
129
- ctx.client.buildDir,
130
- ctx["sdk-react"].outputOptions.buildName,
131
- ),
132
- ],
133
123
  },
134
124
  });
135
125
 
136
126
  const compiler = await createCompiler(validated.config);
137
- const buildResults = await compiler.build();
138
-
139
- if (devMode) {
140
- // Handle process exit to clean up resources
141
- const cleanUp = async () => {
142
- await compiler.destroy();
143
- process.exit(0);
144
- };
145
-
146
- process.on("SIGINT", cleanUp);
147
- process.on("SIGTERM", cleanUp);
148
- } else {
149
- if (buildResults.hasError) {
150
- console.error("Stencil build error:", buildResults.diagnostics);
151
- throw new Error("Stencil build error");
152
- } else {
153
- await handleStencilBuildOutput(ctx);
154
- }
155
- await compiler.destroy();
156
- }
157
127
 
158
- process.chdir(ctx.client.rootDir);
159
- }
160
-
161
- async function handleStencilBuildOutput(ctx: any) {
128
+ await compiler.build();
162
129
  const entryFilePath = path.resolve(
163
130
  ctx.client.stencilBuild,
164
131
  "embeddable-wrapper.esm.js",
@@ -182,6 +149,9 @@ async function handleStencilBuildOutput(ctx: any) {
182
149
  entryFilePath,
183
150
  path.resolve(ctx.client.stencilBuild, fileName),
184
151
  );
152
+
153
+ await compiler.destroy();
154
+ process.chdir(ctx.client.rootDir);
185
155
  }
186
156
 
187
157
  async function generateSourceMap(ctx: any, pluginName: string) {
package/src/push.test.ts CHANGED
@@ -74,8 +74,8 @@ const config = {
74
74
  buildDir: "buildDir",
75
75
  archiveFile: "embeddable-build.zip",
76
76
  },
77
- pushBaseUrl: "pushBaseUrl",
78
- previewBaseUrl: "previewBaseUrl",
77
+ pushBaseUrl: "http://localhost:3000",
78
+ previewBaseUrl: "http://localhost:3000",
79
79
  };
80
80
 
81
81
  describe("push", () => {
@@ -130,10 +130,10 @@ describe("push", () => {
130
130
  expect(fs.rm).toHaveBeenCalledWith(config.client.archiveFile);
131
131
 
132
132
  expect(infoMock.info).toHaveBeenCalledWith(
133
- "Publishing to mocked-workspace-name using previewBaseUrl/workspace/mocked-workspace-id...",
133
+ "Publishing to mocked-workspace-name using http://localhost:3000/workspace/mocked-workspace-id...",
134
134
  );
135
135
  expect(infoMock.succeed).toHaveBeenCalledWith(
136
- "Published to mocked-workspace-name using previewBaseUrl/workspace/mocked-workspace-id",
136
+ "Published to mocked-workspace-name using http://localhost:3000/workspace/mocked-workspace-id",
137
137
  );
138
138
  });
139
139
 
package/src/push.ts CHANGED
@@ -3,7 +3,7 @@ import * as fsSync from "node:fs";
3
3
  import * as archiver from "archiver";
4
4
  import axios from "axios";
5
5
  const oraP = import("ora");
6
- const inquirerSelect = import("@inquirer/select");
6
+
7
7
  import provideConfig from "./provideConfig";
8
8
  // @ts-ignore
9
9
  import reportErrorToRollbar from "./rollbar.mjs";
@@ -11,6 +11,7 @@ import reportErrorToRollbar from "./rollbar.mjs";
11
11
  import { findFiles } from "@embeddable.com/sdk-utils";
12
12
  import { getToken } from "./login";
13
13
  import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
14
+ import { selectWorkspace } from "./workspaceUtils";
14
15
 
15
16
  // grab .cube.yml|js and .sc.yml|js files
16
17
  export const YAML_OR_JS_FILES = /^(.*)\.(cube|sc)\.(ya?ml|js)$/;
@@ -53,6 +54,7 @@ export default async () => {
53
54
  .info("No API Key provided. Standard login will be used.");
54
55
 
55
56
  const { workspaceId, name: workspaceName } = await selectWorkspace(
57
+ ora,
56
58
  config,
57
59
  token,
58
60
  );
@@ -110,44 +112,6 @@ async function pushByApiKey(config: any, spinner: any) {
110
112
  });
111
113
  }
112
114
 
113
- async function selectWorkspace(ctx: any, token: string) {
114
- const workspaceSpinner = ora({
115
- text: `Fetching workspaces using ${ctx.pushBaseUrl}...`,
116
- color: "green",
117
- discardStdin: false,
118
- }).start();
119
-
120
- const availableWorkspaces = await getWorkspaces(ctx, token, workspaceSpinner);
121
-
122
- let selectedWorkspace;
123
-
124
- if (availableWorkspaces.length === 0) {
125
- workspaceSpinner.fail("No workspaces found");
126
- process.exit(1);
127
- }
128
-
129
- workspaceSpinner.info(`Found ${availableWorkspaces.length} workspace(s)`);
130
-
131
- if (availableWorkspaces.length === 1) {
132
- selectedWorkspace = availableWorkspaces[0];
133
- } else {
134
- const select = (await inquirerSelect).default;
135
- selectedWorkspace = await select({
136
- message: "Select workspace to push changes",
137
- choices: availableWorkspaces.map((workspace: any) => ({
138
- name: `${workspace.name} (${workspace.workspaceId})`,
139
- value: workspace,
140
- })),
141
- });
142
- }
143
-
144
- workspaceSpinner.succeed(
145
- `Workspace: ${selectedWorkspace.name} (${selectedWorkspace.workspaceId})`,
146
- );
147
-
148
- return selectedWorkspace;
149
- }
150
-
151
115
  async function verify(ctx: any) {
152
116
  try {
153
117
  await fs.access(ctx.client.buildDir);
@@ -297,24 +261,3 @@ async function uploadFile(formData: any, url: string, token: string) {
297
261
  maxBodyLength: Infinity,
298
262
  });
299
263
  }
300
-
301
- async function getWorkspaces(ctx: any, token: string, workspaceSpinner: any) {
302
- try {
303
- const response = await axios.get(`${ctx.pushBaseUrl}/workspace`, {
304
- headers: {
305
- Authorization: `Bearer ${token}`,
306
- },
307
- });
308
-
309
- return response.data?.filter((w: any) => !w.devWorkspace);
310
- } catch (e: any) {
311
- if (e.response.status === 401) {
312
- workspaceSpinner.fail(
313
- 'Unauthorized. Please login using "embeddable login" command.',
314
- );
315
- } else {
316
- workspaceSpinner.fail("Failed to fetch workspaces");
317
- }
318
- process.exit(1);
319
- }
320
- }
package/src/utils.test.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  const startMock = {
11
11
  succeed: vi.fn(),
12
12
  fail: vi.fn(),
13
+ stop: vi.fn(),
13
14
  };
14
15
 
15
16
  const failMock = vi.fn();
@@ -58,11 +59,9 @@ describe("utils", () => {
58
59
  vi.spyOn(process, "exit").mockImplementation(() => null as never);
59
60
 
60
61
  const result = await checkNodeVersion();
61
-
62
- expect(failMock).toHaveBeenCalledWith({
63
- text: "Node version 16.0 or higher is required. You are running 14.0.",
64
- color: "red",
65
- });
62
+ expect(startMock.fail).toHaveBeenCalledWith(
63
+ "Node version 16.0 or higher is required. You are running 14.0.",
64
+ );
66
65
 
67
66
  expect(result).toBe(undefined);
68
67
  });
package/src/utils.ts CHANGED
@@ -3,10 +3,9 @@ import * as fs from "node:fs/promises";
3
3
  import { CREDENTIALS_DIR } from "./credentials";
4
4
  import path from "node:path";
5
5
 
6
- let ora: any;
7
6
  export const checkNodeVersion = async () => {
8
- ora = (await oraP).default;
9
- const spinner = ora("Checking node version...");
7
+ const ora = (await oraP).default;
8
+ const spinner = ora("Checking node version...").start();
10
9
  const [major, minor] = process.versions.node.split(".").map(Number);
11
10
 
12
11
  const packageJson = await import("../package.json");
@@ -20,13 +19,13 @@ export const checkNodeVersion = async () => {
20
19
  .map(Number);
21
20
 
22
21
  if (major < minMajor || (major === minMajor && minor < minMinor)) {
23
- spinner.fail({
24
- text: `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
25
- color: "red",
26
- });
22
+ spinner.fail(
23
+ `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
24
+ );
27
25
 
28
26
  process.exit(1);
29
27
  } else {
28
+ spinner.stop();
30
29
  return true;
31
30
  }
32
31
  };
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import axios from "axios";
3
+ import { getWorkspaces, selectWorkspace } from "./workspaceUtils";
4
+ import { select } from "@inquirer/prompts";
5
+
6
+ vi.mock("axios");
7
+
8
+ vi.mock("@inquirer/prompts", () => ({
9
+ select: vi.fn(),
10
+ }));
11
+
12
+ describe("workspaceUtils", () => {
13
+ describe("getWorkspaces", () => {
14
+ it("should fetch workspaces and filter out dev workspaces", async () => {
15
+ const ctx = { pushBaseUrl: "http://example.com" };
16
+ const token = "test-token";
17
+ const workspaceSpinner = { fail: vi.fn() };
18
+
19
+ const workspaces = [
20
+ { name: "Workspace 1", workspaceId: "1", devWorkspace: false },
21
+ { name: "Workspace 2", workspaceId: "2", devWorkspace: true },
22
+ ];
23
+
24
+ (axios.get as any).mockResolvedValue({ data: workspaces });
25
+
26
+ const result = await getWorkspaces(ctx, token, workspaceSpinner);
27
+
28
+ expect(result).toEqual([
29
+ { name: "Workspace 1", workspaceId: "1", devWorkspace: false },
30
+ ]);
31
+ });
32
+
33
+ it("should handle unauthorized error", async () => {
34
+ const ctx = { pushBaseUrl: "http://example.com" };
35
+ const token = "test-token";
36
+ const workspaceSpinner = { fail: vi.fn() };
37
+
38
+ (axios.get as any).mockRejectedValue({ response: { status: 401 } });
39
+
40
+ await expect(
41
+ getWorkspaces(ctx, token, workspaceSpinner),
42
+ ).rejects.toThrow();
43
+ expect(workspaceSpinner.fail).toHaveBeenCalledWith(
44
+ 'Unauthorized. Please login using "embeddable login" command.',
45
+ );
46
+ });
47
+
48
+ it("should handle other errors", async () => {
49
+ const ctx = { pushBaseUrl: "http://example.com" };
50
+ const token = "test-token";
51
+ const workspaceSpinner = { fail: vi.fn() };
52
+
53
+ (axios.get as any).mockRejectedValue({ response: { status: 500 } });
54
+
55
+ await expect(
56
+ getWorkspaces(ctx, token, workspaceSpinner),
57
+ ).rejects.toThrow();
58
+ expect(workspaceSpinner.fail).toHaveBeenCalledWith(
59
+ "Failed to fetch workspaces",
60
+ );
61
+ });
62
+ });
63
+
64
+ describe("selectWorkspace", () => {
65
+ beforeEach(() => {
66
+ vi.mocked(select).mockResolvedValue({
67
+ name: "Workspace 2",
68
+ workspaceId: "2",
69
+ devWorkspace: false,
70
+ });
71
+ });
72
+
73
+ it("should select a workspace when only one is available", async () => {
74
+ const ora = vi.fn().mockReturnValue({
75
+ start: vi.fn().mockReturnThis(),
76
+ fail: vi.fn(),
77
+ info: vi.fn(),
78
+ succeed: vi.fn(),
79
+ });
80
+
81
+ const ctx = { pushBaseUrl: "http://example.com" };
82
+ const token = "test-token";
83
+
84
+ const workspaces = [
85
+ { name: "Workspace 1", workspaceId: "1", devWorkspace: false },
86
+ ];
87
+
88
+ (axios.get as any).mockResolvedValue({ data: workspaces });
89
+
90
+ const selectedWorkspace = await selectWorkspace(ora, ctx, token);
91
+
92
+ expect(selectedWorkspace).toEqual(workspaces[0]);
93
+ expect(ora().succeed).toHaveBeenCalledWith(`Workspace: Workspace 1 (1)`);
94
+ });
95
+
96
+ it("should prompt user to select a workspace when multiple are available", async () => {
97
+ const ora = vi.fn().mockReturnValue({
98
+ start: vi.fn().mockReturnThis(),
99
+ fail: vi.fn(),
100
+ info: vi.fn(),
101
+ succeed: vi.fn(),
102
+ });
103
+ const ctx = { pushBaseUrl: "http://example.com" };
104
+ const token = "test-token";
105
+
106
+ const workspaces = [
107
+ { name: "Workspace 1", workspaceId: "1", devWorkspace: false },
108
+ { name: "Workspace 2", workspaceId: "2", devWorkspace: false },
109
+ ];
110
+
111
+ (axios.get as any).mockResolvedValue({ data: workspaces });
112
+
113
+ const selectedWorkspace = await selectWorkspace(ora, ctx, token);
114
+
115
+ expect(selectedWorkspace).toEqual(workspaces[1]);
116
+ expect(ora().succeed).toHaveBeenCalledWith(`Workspace: Workspace 2 (2)`);
117
+ });
118
+
119
+ it("should handle no workspaces found", async () => {
120
+ const ora = vi.fn().mockReturnValue({
121
+ start: vi.fn().mockReturnThis(),
122
+ fail: vi.fn(),
123
+ info: vi.fn(),
124
+ succeed: vi.fn(),
125
+ });
126
+ const ctx = { pushBaseUrl: "http://example.com" };
127
+ const token = "test-token";
128
+
129
+ (axios.get as any).mockResolvedValue({ data: [] });
130
+
131
+ await expect(selectWorkspace(ora, ctx, token)).rejects.toThrow();
132
+ expect(ora().fail).toHaveBeenCalledWith("No workspaces found");
133
+ });
134
+ });
135
+ });