@embeddable.com/sdk-core 3.9.2 → 3.9.4
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 +237 -100
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +237 -100
- package/lib/index.js.map +1 -1
- package/lib/logger.d.ts +9 -0
- package/lib/utils.d.ts +2 -1
- package/package.json +1 -1
- package/src/build.test.ts +6 -0
- package/src/build.ts +14 -0
- package/src/cleanup.test.ts +4 -0
- package/src/cleanup.ts +5 -19
- package/src/dev.test.ts +25 -0
- package/src/dev.ts +121 -100
- 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.test.ts +16 -0
- package/src/utils.ts +42 -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,5 @@ 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>;
|
|
24
|
+
export declare const hrtimeToISO8601: (hrtime: number[] | null | undefined) => 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,16 @@ 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 {
|
|
21
|
+
const startTime = process.hrtime();
|
|
17
22
|
checkNodeVersion();
|
|
23
|
+
breadcrumbs.push("checkNodeVersion");
|
|
18
24
|
removeBuildSuccessFlag();
|
|
19
25
|
|
|
20
26
|
const config = await provideConfig();
|
|
@@ -28,16 +34,24 @@ export default async () => {
|
|
|
28
34
|
for (const getPlugin of config.plugins) {
|
|
29
35
|
const plugin = getPlugin();
|
|
30
36
|
|
|
37
|
+
breadcrumbs.push(`${plugin.pluginName}: validate`);
|
|
31
38
|
await plugin.validate(config);
|
|
39
|
+
breadcrumbs.push(`${plugin.pluginName}: build`);
|
|
32
40
|
await plugin.build(config);
|
|
41
|
+
breadcrumbs.push(`${plugin.pluginName}: cleanup`);
|
|
33
42
|
await plugin.cleanup(config);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// NOTE: likely this will be called inside the loop above if we decide to support clients with mixed frameworks simultaneously.
|
|
46
|
+
breadcrumbs.push("generate");
|
|
37
47
|
await generate(config, "sdk-react");
|
|
48
|
+
// Calculating build time in seconds
|
|
49
|
+
config.buildTime = process.hrtime(startTime);
|
|
50
|
+
breadcrumbs.push("cleanup");
|
|
38
51
|
await cleanup(config);
|
|
39
52
|
await storeBuildSuccessFlag();
|
|
40
53
|
} catch (error: any) {
|
|
54
|
+
await logError({ command: "build", breadcrumbs, error });
|
|
41
55
|
await reportErrorToRollbar(error);
|
|
42
56
|
console.log(error);
|
|
43
57
|
process.exit(1);
|
package/src/cleanup.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ const ctx = {
|
|
|
9
9
|
stencilBuild: "stencilBuild",
|
|
10
10
|
buildDir: "buildDir",
|
|
11
11
|
},
|
|
12
|
+
buildTime: [80, 525000],
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
vi.mock("node:fs/promises", () => ({
|
|
@@ -77,6 +78,9 @@ describe("cleanup", () => {
|
|
|
77
78
|
sdkVersions: {},
|
|
78
79
|
packageManager: "npm",
|
|
79
80
|
packageManagerVersion: "10.7.0",
|
|
81
|
+
metrics: {
|
|
82
|
+
buildTime: "PT1M20.001S",
|
|
83
|
+
},
|
|
80
84
|
},
|
|
81
85
|
}),
|
|
82
86
|
);
|
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, hrtimeToISO8601 } 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")) {
|
|
@@ -72,6 +55,9 @@ export async function createManifest({
|
|
|
72
55
|
sdkVersions,
|
|
73
56
|
packageManager,
|
|
74
57
|
packageManagerVersion,
|
|
58
|
+
metrics: {
|
|
59
|
+
buildTime: hrtimeToISO8601(ctx.buildTime),
|
|
60
|
+
},
|
|
75
61
|
},
|
|
76
62
|
};
|
|
77
63
|
|
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,132 +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
|
+
try {
|
|
77
|
+
breadcrumbs.push("run dev");
|
|
78
|
+
checkNodeVersion();
|
|
79
|
+
addToGitingore();
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
ora = (await oraP).default;
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
const sys = createNodeSys({ process });
|
|
83
|
+
process.on("warning", (e) => console.warn(e.stack));
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
const logger = createNodeLogger();
|
|
86
|
+
const sys = createNodeSys({ process });
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
const defaultConfig = await provideConfig();
|
|
85
89
|
|
|
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
|
-
};
|
|
90
|
+
const buildDir = path.resolve(defaultConfig.client.rootDir, BUILD_DEV_DIR);
|
|
101
91
|
|
|
102
|
-
|
|
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
|
+
};
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
breadcrumbs.push("prepare config");
|
|
112
|
+
await prepare(config);
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
const finalhandler = require("finalhandler");
|
|
115
|
+
const serveStatic = require("serve-static");
|
|
108
116
|
|
|
109
|
-
|
|
117
|
+
const serve = serveStatic(config.client.buildDir);
|
|
110
118
|
|
|
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
|
-
}
|
|
119
|
+
const workspacePreparation = ora("Preparing workspace...").start();
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
res.setHeader(
|
|
126
|
-
"Access-Control-Allow-Methods",
|
|
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
|
-
"
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
workspacePreparation.fail(
|
|
129
|
+
e.response?.data?.errorMessage || "Unknown error: " + e.message,
|
|
132
130
|
);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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();
|
|
147
152
|
return;
|
|
148
153
|
}
|
|
149
|
-
} catch {}
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
);
|
|
155
|
+
const done = finalhandler(request, res);
|
|
154
156
|
|
|
155
|
-
|
|
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 {}
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
const watchers: Array<RollupWatcher | FSWatcher> = [];
|
|
159
|
-
if (sys?.onProcessInterrupt) {
|
|
160
|
-
sys.onProcessInterrupt(
|
|
161
|
-
async () => await onClose(server, sys, watchers, config),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
await createManifest({
|
|
166
|
-
ctx: {
|
|
167
|
-
...config,
|
|
168
|
-
client: {
|
|
169
|
-
...config.client,
|
|
170
|
-
tmpDir: buildDir,
|
|
171
|
-
},
|
|
165
|
+
serve(request, res, done);
|
|
172
166
|
},
|
|
173
|
-
|
|
174
|
-
stencilWrapperFileName: "embeddable-wrapper.js",
|
|
175
|
-
metaFileName: "embeddable-components-meta.js",
|
|
176
|
-
editorsMetaFileName: "embeddable-editors-meta.js",
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await sendDataModelsAndSecurityContextsChanges(config);
|
|
167
|
+
);
|
|
180
168
|
|
|
181
|
-
|
|
182
|
-
const plugin = getPlugin();
|
|
169
|
+
wss = new WebSocketServer({ server });
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
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
|
+
}
|
|
186
178
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|
|
190
207
|
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
const dataModelAndSecurityContextWatch =
|
|
209
|
+
dataModelAndSecurityContextWatcher(config);
|
|
193
210
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
219
|
};
|
|
199
220
|
|
|
200
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(),
|