@embeddable.com/sdk-core 3.9.1 → 3.9.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/lib/index.esm.js +219 -98
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +219 -98
- package/lib/index.js.map +1 -1
- package/lib/logger.d.ts +9 -0
- package/lib/utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/build.test.ts +6 -0
- package/src/build.ts +11 -0
- package/src/cleanup.ts +2 -19
- package/src/dev.test.ts +25 -0
- package/src/dev.ts +125 -102
- package/src/logger.test.ts +98 -0
- package/src/logger.ts +116 -0
- package/src/login.test.ts +5 -0
- package/src/login.ts +7 -1
- package/src/push.test.ts +2 -0
- package/src/push.ts +11 -6
- package/src/utils.ts +24 -1
package/lib/logger.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const ERROR_LOG_FILE: string;
|
|
2
|
+
interface LogErrorParams {
|
|
3
|
+
command: string;
|
|
4
|
+
breadcrumbs: string[];
|
|
5
|
+
error: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function initLogger(command: string): Promise<void>;
|
|
8
|
+
export declare function logError({ command, breadcrumbs, error, }: LogErrorParams): Promise<void>;
|
|
9
|
+
export {};
|
package/lib/utils.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export declare const removeBuildSuccessFlag: () => Promise<void>;
|
|
|
20
20
|
* Check if the build was successful
|
|
21
21
|
*/
|
|
22
22
|
export declare const checkBuildSuccess: () => Promise<boolean>;
|
|
23
|
-
export declare const
|
|
23
|
+
export declare const getSDKVersions: () => Record<string, string>;
|
package/package.json
CHANGED
package/src/build.test.ts
CHANGED
|
@@ -5,11 +5,17 @@ import buildTypes from "./buildTypes";
|
|
|
5
5
|
import provideConfig from "./provideConfig";
|
|
6
6
|
import generate from "./generate";
|
|
7
7
|
import cleanup from "./cleanup";
|
|
8
|
+
import { initLogger, logError } from "./logger";
|
|
8
9
|
|
|
9
10
|
// @ts-ignore
|
|
10
11
|
import reportErrorToRollbar from "./rollbar.mjs";
|
|
11
12
|
import { storeBuildSuccessFlag } from "./utils";
|
|
12
13
|
|
|
14
|
+
vi.mock("./logger", () => ({
|
|
15
|
+
initLogger: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
logError: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
}));
|
|
18
|
+
|
|
13
19
|
const mockPlugin = {
|
|
14
20
|
validate: vi.fn(),
|
|
15
21
|
build: vi.fn(),
|
package/src/build.ts
CHANGED
|
@@ -11,10 +11,15 @@ import {
|
|
|
11
11
|
removeBuildSuccessFlag,
|
|
12
12
|
storeBuildSuccessFlag,
|
|
13
13
|
} from "./utils";
|
|
14
|
+
import { initLogger, logError } from "./logger";
|
|
14
15
|
|
|
15
16
|
export default async () => {
|
|
17
|
+
await initLogger("build");
|
|
18
|
+
const breadcrumbs: string[] = [];
|
|
19
|
+
|
|
16
20
|
try {
|
|
17
21
|
checkNodeVersion();
|
|
22
|
+
breadcrumbs.push("checkNodeVersion");
|
|
18
23
|
removeBuildSuccessFlag();
|
|
19
24
|
|
|
20
25
|
const config = await provideConfig();
|
|
@@ -28,16 +33,22 @@ export default async () => {
|
|
|
28
33
|
for (const getPlugin of config.plugins) {
|
|
29
34
|
const plugin = getPlugin();
|
|
30
35
|
|
|
36
|
+
breadcrumbs.push(`${plugin.pluginName}: validate`);
|
|
31
37
|
await plugin.validate(config);
|
|
38
|
+
breadcrumbs.push(`${plugin.pluginName}: build`);
|
|
32
39
|
await plugin.build(config);
|
|
40
|
+
breadcrumbs.push(`${plugin.pluginName}: cleanup`);
|
|
33
41
|
await plugin.cleanup(config);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
// NOTE: likely this will be called inside the loop above if we decide to support clients with mixed frameworks simultaneously.
|
|
45
|
+
breadcrumbs.push("generate");
|
|
37
46
|
await generate(config, "sdk-react");
|
|
47
|
+
breadcrumbs.push("cleanup");
|
|
38
48
|
await cleanup(config);
|
|
39
49
|
await storeBuildSuccessFlag();
|
|
40
50
|
} catch (error: any) {
|
|
51
|
+
await logError({ command: "build", breadcrumbs, error });
|
|
41
52
|
await reportErrorToRollbar(error);
|
|
42
53
|
console.log(error);
|
|
43
54
|
process.exit(1);
|
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 {
|
|
4
|
+
import { getSDKVersions } from "./utils";
|
|
5
5
|
|
|
6
6
|
export default async (ctx: any) => {
|
|
7
7
|
await extractBuild(ctx);
|
|
@@ -26,24 +26,7 @@ export async function createManifest({
|
|
|
26
26
|
editorsMetaFileName,
|
|
27
27
|
stencilWrapperFileName,
|
|
28
28
|
}: ManifestArgs) {
|
|
29
|
-
const
|
|
30
|
-
"@embeddable.com/core",
|
|
31
|
-
"@embeddable.com/react",
|
|
32
|
-
"@embeddable.com/sdk-core",
|
|
33
|
-
"@embeddable.com/sdk-react",
|
|
34
|
-
"@embeddable.com/sdk-utils",
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const sdkVersions = packageNames.reduce<Record<string, string>>(
|
|
38
|
-
(acc, packageName) => {
|
|
39
|
-
const version = getPackageVersion(packageName);
|
|
40
|
-
if (version) {
|
|
41
|
-
acc[packageName] = version;
|
|
42
|
-
}
|
|
43
|
-
return acc;
|
|
44
|
-
},
|
|
45
|
-
{},
|
|
46
|
-
);
|
|
29
|
+
const sdkVersions = getSDKVersions();
|
|
47
30
|
// identify user's package manager and its version
|
|
48
31
|
let packageManager = "npm";
|
|
49
32
|
if (process.env.npm_config_user_agent?.includes("yarn")) {
|
package/src/dev.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { checkNodeVersion } from "./utils";
|
|
|
9
9
|
import { createManifest } from "./cleanup";
|
|
10
10
|
import prepare from "./prepare";
|
|
11
11
|
import { WebSocketServer } from "ws";
|
|
12
|
+
import { initLogger, logError } from "./logger";
|
|
12
13
|
|
|
13
14
|
// Mock dependencies
|
|
14
15
|
vi.mock("./buildTypes", () => ({ default: vi.fn() }));
|
|
@@ -31,6 +32,10 @@ vi.mock("./cleanup", () => ({ createManifest: vi.fn() }));
|
|
|
31
32
|
vi.mock("node:http", () => ({
|
|
32
33
|
createServer: vi.fn(() => ({ listen: vi.fn() })),
|
|
33
34
|
}));
|
|
35
|
+
vi.mock("./logger", () => ({
|
|
36
|
+
initLogger: vi.fn(),
|
|
37
|
+
logError: vi.fn(),
|
|
38
|
+
}));
|
|
34
39
|
|
|
35
40
|
const mockConfig = {
|
|
36
41
|
client: {
|
|
@@ -100,4 +105,24 @@ describe("dev command", () => {
|
|
|
100
105
|
|
|
101
106
|
await expect.poll(() => chokidar.watch).toBeCalledTimes(2);
|
|
102
107
|
});
|
|
108
|
+
|
|
109
|
+
it("should log errors and exit on failure", async () => {
|
|
110
|
+
const error = new Error("Test error");
|
|
111
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
112
|
+
throw new Error("process.exit");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
vi.mocked(checkNodeVersion).mockImplementation(() => {
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await expect(dev()).rejects.toThrow("process.exit");
|
|
120
|
+
|
|
121
|
+
expect(logError).toHaveBeenCalledWith({
|
|
122
|
+
command: "dev",
|
|
123
|
+
breadcrumbs: ["run dev"],
|
|
124
|
+
error,
|
|
125
|
+
});
|
|
126
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
127
|
+
});
|
|
103
128
|
});
|
package/src/dev.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { createManifest } from "./cleanup";
|
|
|
28
28
|
import { selectWorkspace } from "./workspaceUtils";
|
|
29
29
|
import * as fs from "fs";
|
|
30
30
|
const minimist = require("minimist");
|
|
31
|
+
import { initLogger, logError } from "./logger";
|
|
31
32
|
|
|
32
33
|
const oraP = import("ora");
|
|
33
34
|
let wss: WSServer;
|
|
@@ -69,130 +70,152 @@ const chokidarWatchOptions = {
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
export default async () => {
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
await initLogger("dev");
|
|
74
|
+
const breadcrumbs: string[] = [];
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
try {
|
|
77
|
+
breadcrumbs.push("run dev");
|
|
78
|
+
checkNodeVersion();
|
|
79
|
+
addToGitingore();
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
const sys = createNodeSys({ process });
|
|
81
|
+
ora = (await oraP).default;
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
process.on("warning", (e) => console.warn(e.stack));
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
const logger = createNodeLogger();
|
|
86
|
+
const sys = createNodeSys({ process });
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
...defaultConfig,
|
|
88
|
-
dev: {
|
|
89
|
-
watch: true,
|
|
90
|
-
logger,
|
|
91
|
-
sys,
|
|
92
|
-
},
|
|
93
|
-
client: {
|
|
94
|
-
...defaultConfig.client,
|
|
95
|
-
buildDir,
|
|
96
|
-
componentDir: path.resolve(buildDir, "component"),
|
|
97
|
-
stencilBuild: path.resolve(buildDir, "dist", "embeddable-wrapper"),
|
|
98
|
-
tmpDir: path.resolve(defaultConfig.client.rootDir, ".embeddable-dev-tmp"),
|
|
99
|
-
},
|
|
100
|
-
};
|
|
88
|
+
const defaultConfig = await provideConfig();
|
|
101
89
|
|
|
102
|
-
|
|
90
|
+
const buildDir = path.resolve(defaultConfig.client.rootDir, BUILD_DEV_DIR);
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
const config = {
|
|
93
|
+
...defaultConfig,
|
|
94
|
+
dev: {
|
|
95
|
+
watch: true,
|
|
96
|
+
logger,
|
|
97
|
+
sys,
|
|
98
|
+
},
|
|
99
|
+
client: {
|
|
100
|
+
...defaultConfig.client,
|
|
101
|
+
buildDir,
|
|
102
|
+
componentDir: path.resolve(buildDir, "component"),
|
|
103
|
+
stencilBuild: path.resolve(buildDir, "dist", "embeddable-wrapper"),
|
|
104
|
+
tmpDir: path.resolve(
|
|
105
|
+
defaultConfig.client.rootDir,
|
|
106
|
+
".embeddable-dev-tmp",
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
breadcrumbs.push("prepare config");
|
|
112
|
+
await prepare(config);
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
const finalhandler = require("finalhandler");
|
|
115
|
+
const serveStatic = require("serve-static");
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
previewWorkspace = await getPreviewWorkspace(workspacePreparation, config);
|
|
113
|
-
} catch (e: any) {
|
|
114
|
-
workspacePreparation.fail(
|
|
115
|
-
e.response?.data?.errorMessage || "Unknown error: " + e.message,
|
|
116
|
-
);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
117
|
+
const serve = serveStatic(config.client.buildDir);
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
const workspacePreparation = ora("Preparing workspace...").start();
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"GET, POST, PUT, DELETE, OPTIONS",
|
|
121
|
+
breadcrumbs.push("get preview workspace");
|
|
122
|
+
try {
|
|
123
|
+
previewWorkspace = await getPreviewWorkspace(
|
|
124
|
+
workspacePreparation,
|
|
125
|
+
config,
|
|
128
126
|
);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
if (request.method === "OPTIONS") {
|
|
135
|
-
// Respond to OPTIONS requests with just the CORS headers and a 200 status code
|
|
136
|
-
res.writeHead(200);
|
|
137
|
-
res.end();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const done = finalhandler(request, res);
|
|
142
|
-
|
|
143
|
-
if (request.url?.endsWith(GLOBAL_CSS)) {
|
|
144
|
-
res.writeHead(200, { "Content-Type": "text/css" });
|
|
145
|
-
res.end(fs.readFileSync(config.client.globalCss));
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
serve(request, res, done);
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
wss = new WebSocketServer({ server });
|
|
154
|
-
|
|
155
|
-
server.listen(SERVER_PORT, async () => {
|
|
156
|
-
const watchers: Array<RollupWatcher | FSWatcher> = [];
|
|
157
|
-
if (sys?.onProcessInterrupt) {
|
|
158
|
-
sys.onProcessInterrupt(
|
|
159
|
-
async () => await onClose(server, sys, watchers, config),
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
workspacePreparation.fail(
|
|
129
|
+
e.response?.data?.errorMessage || "Unknown error: " + e.message,
|
|
160
130
|
);
|
|
131
|
+
process.exit(1);
|
|
161
132
|
}
|
|
162
133
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
134
|
+
workspacePreparation.succeed("Workspace is ready");
|
|
135
|
+
|
|
136
|
+
const server = http.createServer(
|
|
137
|
+
(request: IncomingMessage, res: ServerResponse) => {
|
|
138
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
139
|
+
res.setHeader(
|
|
140
|
+
"Access-Control-Allow-Methods",
|
|
141
|
+
"GET, POST, PUT, DELETE, OPTIONS",
|
|
142
|
+
);
|
|
143
|
+
res.setHeader(
|
|
144
|
+
"Access-Control-Allow-Headers",
|
|
145
|
+
"Content-Type, Authorization",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (request.method === "OPTIONS") {
|
|
149
|
+
// Respond to OPTIONS requests with just the CORS headers and a 200 status code
|
|
150
|
+
res.writeHead(200);
|
|
151
|
+
res.end();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const done = finalhandler(request, res);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (request.url?.endsWith(GLOBAL_CSS)) {
|
|
159
|
+
res.writeHead(200, { "Content-Type": "text/css" });
|
|
160
|
+
res.end(fs.readFileSync(config.client.globalCss));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
|
|
165
|
+
serve(request, res, done);
|
|
170
166
|
},
|
|
171
|
-
|
|
172
|
-
stencilWrapperFileName: "embeddable-wrapper.js",
|
|
173
|
-
metaFileName: "embeddable-components-meta.js",
|
|
174
|
-
editorsMetaFileName: "embeddable-editors-meta.js",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
await sendDataModelsAndSecurityContextsChanges(config);
|
|
167
|
+
);
|
|
178
168
|
|
|
179
|
-
|
|
180
|
-
const plugin = getPlugin();
|
|
169
|
+
wss = new WebSocketServer({ server });
|
|
181
170
|
|
|
182
|
-
|
|
183
|
-
const
|
|
171
|
+
server.listen(SERVER_PORT, async () => {
|
|
172
|
+
const watchers: Array<RollupWatcher | FSWatcher> = [];
|
|
173
|
+
if (sys?.onProcessInterrupt) {
|
|
174
|
+
sys.onProcessInterrupt(
|
|
175
|
+
async () => await onClose(server, sys, watchers, config),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
184
178
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
breadcrumbs.push("create manifest");
|
|
180
|
+
await createManifest({
|
|
181
|
+
ctx: {
|
|
182
|
+
...config,
|
|
183
|
+
client: {
|
|
184
|
+
...config.client,
|
|
185
|
+
tmpDir: buildDir,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
typesFileName: "embeddable-types.js",
|
|
189
|
+
stencilWrapperFileName: "embeddable-wrapper.js",
|
|
190
|
+
metaFileName: "embeddable-components-meta.js",
|
|
191
|
+
editorsMetaFileName: "embeddable-editors-meta.js",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await sendDataModelsAndSecurityContextsChanges(config);
|
|
195
|
+
|
|
196
|
+
for (const getPlugin of config.plugins) {
|
|
197
|
+
const plugin = getPlugin();
|
|
198
|
+
|
|
199
|
+
breadcrumbs.push("validate plugin");
|
|
200
|
+
await plugin.validate(config);
|
|
201
|
+
breadcrumbs.push("build plugin");
|
|
202
|
+
const watcher = await plugin.build(config);
|
|
203
|
+
breadcrumbs.push("configure watcher");
|
|
204
|
+
await configureWatcher(watcher, config);
|
|
205
|
+
watchers.push(watcher);
|
|
206
|
+
}
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
const dataModelAndSecurityContextWatch =
|
|
209
|
+
dataModelAndSecurityContextWatcher(config);
|
|
191
210
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
const customGlobalCssWatch = globalCssWatcher(config);
|
|
212
|
+
watchers.push(dataModelAndSecurityContextWatch);
|
|
213
|
+
watchers.push(customGlobalCssWatch);
|
|
214
|
+
});
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
await logError({ command: "dev", breadcrumbs, error });
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
196
219
|
};
|
|
197
220
|
|
|
198
221
|
const configureWatcher = async (watcher: RollupWatcher, ctx: any) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import { initLogger, logError, ERROR_LOG_FILE } from "./logger";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
vi.mock("fs/promises", () => ({
|
|
6
|
+
readFile: vi.fn(),
|
|
7
|
+
appendFile: vi.fn(),
|
|
8
|
+
access: vi.fn(),
|
|
9
|
+
mkdir: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("Logger", () => {
|
|
13
|
+
describe("initLogger", () => {
|
|
14
|
+
it("should create log directory if it does not exist", async () => {
|
|
15
|
+
await initLogger("test");
|
|
16
|
+
expect(fs.mkdir).toHaveBeenCalledWith(
|
|
17
|
+
expect.stringContaining(".embeddable/logs"),
|
|
18
|
+
{ recursive: true },
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle errors when creating log directory", async () => {
|
|
23
|
+
const consoleErrorSpy = vi
|
|
24
|
+
.spyOn(console, "error")
|
|
25
|
+
.mockImplementation(() => {});
|
|
26
|
+
vi.mocked(fs.mkdir).mockRejectedValueOnce(
|
|
27
|
+
new Error("Failed to create directory"),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await initLogger("test");
|
|
31
|
+
|
|
32
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
33
|
+
"Failed to create log directory:",
|
|
34
|
+
expect.any(Error),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("logError", () => {
|
|
40
|
+
it("should append error message to log file", async () => {
|
|
41
|
+
const error = new Error("Test error");
|
|
42
|
+
await logError({
|
|
43
|
+
command: "test",
|
|
44
|
+
breadcrumbs: ["step1", "step2"],
|
|
45
|
+
error,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(fs.appendFile).toHaveBeenCalledWith(
|
|
49
|
+
ERROR_LOG_FILE,
|
|
50
|
+
expect.stringContaining(
|
|
51
|
+
"Command: test\nBreadcrumbs: step1 > step2\nError: Error: Test error",
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle non-Error objects", async () => {
|
|
57
|
+
await logError({
|
|
58
|
+
command: "test",
|
|
59
|
+
breadcrumbs: ["step1"],
|
|
60
|
+
error: "String error",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(fs.appendFile).toHaveBeenCalledWith(
|
|
64
|
+
ERROR_LOG_FILE,
|
|
65
|
+
expect.stringContaining(
|
|
66
|
+
"Command: test\nBreadcrumbs: step1\nError: String error",
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should log to console when error occurs", async () => {
|
|
72
|
+
const consoleErrorSpy = vi
|
|
73
|
+
.spyOn(console, "error")
|
|
74
|
+
.mockImplementation(() => {});
|
|
75
|
+
await logError({ command: "test", breadcrumbs: [], error: "Test error" });
|
|
76
|
+
|
|
77
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
78
|
+
expect.stringContaining("An error occurred during test"),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle errors when writing to log file", async () => {
|
|
83
|
+
const consoleErrorSpy = vi
|
|
84
|
+
.spyOn(console, "error")
|
|
85
|
+
.mockImplementation(() => {});
|
|
86
|
+
vi.mocked(fs.appendFile).mockRejectedValueOnce(
|
|
87
|
+
new Error("Failed to write"),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await logError({ command: "test", breadcrumbs: [], error: "Test error" });
|
|
91
|
+
|
|
92
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
93
|
+
"Failed to write to log file:",
|
|
94
|
+
expect.any(Error),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getSDKVersions } from "./utils";
|
|
4
|
+
|
|
5
|
+
const LOG_DIR = path.join(process.cwd(), ".embeddable", "logs");
|
|
6
|
+
export const ERROR_LOG_FILE = path.join(LOG_DIR, "error.log");
|
|
7
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
8
|
+
const MAX_LOG_FILES = 5;
|
|
9
|
+
|
|
10
|
+
interface LogEntry {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
command: string;
|
|
13
|
+
breadcrumbs: string[];
|
|
14
|
+
error: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LogErrorParams {
|
|
18
|
+
command: string;
|
|
19
|
+
breadcrumbs: string[];
|
|
20
|
+
error: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function initLogger(command: string) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.mkdir(LOG_DIR, { recursive: true });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Failed to create log directory:", error);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setupGlobalErrorHandlers(command);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function logError({
|
|
34
|
+
command,
|
|
35
|
+
breadcrumbs,
|
|
36
|
+
error,
|
|
37
|
+
}: LogErrorParams) {
|
|
38
|
+
const sdkVersions = getSDKVersions();
|
|
39
|
+
const logEntry: LogEntry = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
command,
|
|
42
|
+
breadcrumbs,
|
|
43
|
+
error:
|
|
44
|
+
error instanceof Error
|
|
45
|
+
? `${error.name}: ${error.message}\n${error.stack}`
|
|
46
|
+
: String(error),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const logMessage = `
|
|
50
|
+
[${logEntry.timestamp}] Command: ${logEntry.command}
|
|
51
|
+
Breadcrumbs: ${logEntry.breadcrumbs.join(" > ")}
|
|
52
|
+
Error: ${logEntry.error}
|
|
53
|
+
OS: ${process.platform}
|
|
54
|
+
Node: ${process.version}
|
|
55
|
+
SDK Versions: ${JSON.stringify(sdkVersions, null, 2)}
|
|
56
|
+
----------------------------------------
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await rotateLogIfNeeded();
|
|
61
|
+
await fs.appendFile(ERROR_LOG_FILE, logMessage);
|
|
62
|
+
console.error(
|
|
63
|
+
`An error occurred during ${command}. Check the log file for details: ${ERROR_LOG_FILE}`,
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Failed to write to log file:", error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function rotateLogIfNeeded() {
|
|
71
|
+
try {
|
|
72
|
+
const stats = await fs.stat(ERROR_LOG_FILE);
|
|
73
|
+
if (stats.size < MAX_LOG_SIZE) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let i = MAX_LOG_FILES - 1; i > 0; i--) {
|
|
78
|
+
const oldFile = `${ERROR_LOG_FILE}.${i}`;
|
|
79
|
+
const newFile = `${ERROR_LOG_FILE}.${i + 1}`;
|
|
80
|
+
try {
|
|
81
|
+
await fs.rename(oldFile, newFile);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Ignore error if file doesn't exist
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await fs.rename(ERROR_LOG_FILE, `${ERROR_LOG_FILE}.1`);
|
|
88
|
+
await fs.writeFile(ERROR_LOG_FILE, ""); // Create a new empty log file
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
if (error.code !== "ENOENT") {
|
|
91
|
+
console.error("Error rotating log file:", error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setupGlobalErrorHandlers(command: string) {
|
|
97
|
+
process.on("uncaughtException", async (error) => {
|
|
98
|
+
await logError({ command, breadcrumbs: ["uncaughtException"], error });
|
|
99
|
+
console.error(
|
|
100
|
+
"An uncaught error occurred. Check the log file for details.",
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
process.on("unhandledRejection", async (reason) => {
|
|
106
|
+
await logError({
|
|
107
|
+
command,
|
|
108
|
+
breadcrumbs: ["unhandledRejection"],
|
|
109
|
+
error: reason as Error | string,
|
|
110
|
+
});
|
|
111
|
+
console.error(
|
|
112
|
+
"An unhandled rejection occurred. Check the log file for details.",
|
|
113
|
+
);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
}
|
package/src/login.test.ts
CHANGED
|
@@ -8,6 +8,11 @@ import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials";
|
|
|
8
8
|
import { http, HttpResponse } from "msw";
|
|
9
9
|
import { AxiosError } from "axios";
|
|
10
10
|
|
|
11
|
+
vi.mock("./logger", () => ({
|
|
12
|
+
initLogger: vi.fn(),
|
|
13
|
+
logError: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
11
16
|
vi.mock("fs/promises", () => ({
|
|
12
17
|
readFile: vi.fn(),
|
|
13
18
|
writeFile: vi.fn(),
|
package/src/login.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import provideConfig from "./provideConfig";
|
|
4
|
+
import { initLogger, logError } from "./logger";
|
|
4
5
|
|
|
5
6
|
// @ts-ignore
|
|
6
7
|
import reportErrorToRollbar from "./rollbar.mjs";
|
|
@@ -9,14 +10,18 @@ const oraP = import("ora");
|
|
|
9
10
|
const openP = import("open");
|
|
10
11
|
|
|
11
12
|
export default async () => {
|
|
13
|
+
await initLogger("login");
|
|
14
|
+
const breadcrumbs: string[] = [];
|
|
12
15
|
const ora = (await oraP).default;
|
|
13
16
|
const authenticationSpinner = ora("Waiting for code verification...").start();
|
|
14
17
|
|
|
15
18
|
try {
|
|
16
19
|
const open = (await openP).default;
|
|
17
20
|
const config = await provideConfig();
|
|
21
|
+
breadcrumbs.push("provideConfig");
|
|
18
22
|
|
|
19
23
|
await resolveFiles();
|
|
24
|
+
breadcrumbs.push("resolveFiles");
|
|
20
25
|
|
|
21
26
|
const deviceCodePayload = {
|
|
22
27
|
client_id: config.authClientId,
|
|
@@ -70,8 +75,9 @@ export default async () => {
|
|
|
70
75
|
await sleep(deviceCodeResponse.data["interval"] * 1000);
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
|
-
} catch (error) {
|
|
78
|
+
} catch (error: unknown) {
|
|
74
79
|
authenticationSpinner.fail("Authentication failed. Please try again.");
|
|
80
|
+
await logError({ command: "login", breadcrumbs, error });
|
|
75
81
|
await reportErrorToRollbar(error);
|
|
76
82
|
console.log(error);
|
|
77
83
|
|