@embeddable.com/sdk-core 4.2.0 → 4.3.0-next.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.
- package/lib/dev.d.ts +6 -1
- package/lib/generate.d.ts +30 -1
- package/lib/index.esm.js +307 -36
- package/lib/index.esm.js.map +1 -1
- package/lib/utils/dev.utils.d.ts +26 -0
- package/package.json +1 -1
- package/src/dev.test.ts +184 -14
- package/src/dev.ts +163 -15
- package/src/generate.test.ts +187 -3
- package/src/generate.ts +148 -24
- package/src/utils/dev.utils.test.ts +126 -0
- package/src/utils/dev.utils.ts +117 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ServerResponse } from "http";
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a ServerResponse object to prevent setting the Content-Length header.
|
|
4
|
+
*/
|
|
5
|
+
export declare function preventContentLength(res: ServerResponse): void;
|
|
6
|
+
export declare function createWatcherLock(): {
|
|
7
|
+
lock(): void;
|
|
8
|
+
unlock(): void;
|
|
9
|
+
waitUntilFree(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare const delay: (ms: number) => Promise<unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* This function waits until a file stabilizes, meaning its size does not change for a certain number of attempts
|
|
14
|
+
* and the content ends with a specific expected tail.
|
|
15
|
+
* It uses stream reading to check the file's content, the same wait serve-static uses.
|
|
16
|
+
* This will help to prevent when serve-static serves a file that is still being written to.
|
|
17
|
+
* One of the issues, related to this, is "Constructor for "embeddable-component#undefined" was not found", that we saw quite often in the past.
|
|
18
|
+
* @param filePath
|
|
19
|
+
* @param expectedTail
|
|
20
|
+
* @param maxAttempts
|
|
21
|
+
* @param stableCount
|
|
22
|
+
*/
|
|
23
|
+
export declare function waitUntilFileStable(filePath: string, expectedTail: string, { maxAttempts, requiredStableCount, }?: {
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
requiredStableCount?: number;
|
|
26
|
+
}): Promise<void>;
|
package/package.json
CHANGED
package/src/dev.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import provideConfig from "./provideConfig";
|
|
|
5
5
|
import buildGlobalHooks from "./buildGlobalHooks";
|
|
6
6
|
import { getToken } from "./login";
|
|
7
7
|
import * as chokidar from "chokidar";
|
|
8
|
-
import generate from "./generate";
|
|
8
|
+
import generate, { generateDTS } from "./generate";
|
|
9
9
|
import buildTypes from "./buildTypes";
|
|
10
10
|
import validate from "./validate";
|
|
11
11
|
import { findFiles } from "@embeddable.com/sdk-utils";
|
|
@@ -15,6 +15,9 @@ import dev, {
|
|
|
15
15
|
globalHookWatcher,
|
|
16
16
|
openDevWorkspacePage,
|
|
17
17
|
sendBuildChanges,
|
|
18
|
+
onWebComponentBuildFinish,
|
|
19
|
+
waitForStableHmrFiles,
|
|
20
|
+
resetStateForTesting,
|
|
18
21
|
} from "./dev";
|
|
19
22
|
import login from "./login";
|
|
20
23
|
import { checkNodeVersion } from "./utils";
|
|
@@ -41,14 +44,18 @@ vi.mock("./buildTypes", () => ({
|
|
|
41
44
|
}));
|
|
42
45
|
vi.mock("./buildGlobalHooks", () => ({ default: vi.fn() }));
|
|
43
46
|
vi.mock("./prepare", () => ({ default: vi.fn(), removeIfExists: vi.fn() }));
|
|
44
|
-
vi.mock("./generate", () => ({
|
|
47
|
+
vi.mock("./generate", () => ({
|
|
48
|
+
default: vi.fn(),
|
|
49
|
+
generateDTS: vi.fn(),
|
|
50
|
+
triggerWebComponentRebuild: vi.fn(),
|
|
51
|
+
}));
|
|
45
52
|
vi.mock("./provideConfig", () => ({ default: vi.fn() }));
|
|
46
53
|
vi.mock("@stencil/core/sys/node", () => ({
|
|
47
54
|
createNodeLogger: vi.fn(),
|
|
48
55
|
createNodeSys: vi.fn(),
|
|
49
56
|
}));
|
|
50
57
|
vi.mock("open", () => ({ default: vi.fn() }));
|
|
51
|
-
vi.mock("ws", () => ({
|
|
58
|
+
vi.mock("ws", () => ({
|
|
52
59
|
WebSocketServer: class WebSocketServer {
|
|
53
60
|
constructor() {}
|
|
54
61
|
}
|
|
@@ -108,10 +115,15 @@ vi.mock("./dev", async (importOriginal) => {
|
|
|
108
115
|
};
|
|
109
116
|
});
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
vi.
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
vi.mock("./utils/dev.utils", () => ({
|
|
119
|
+
createWatcherLock: vi.fn(() => ({
|
|
120
|
+
lock: vi.fn(),
|
|
121
|
+
unlock: vi.fn(),
|
|
122
|
+
waitUntilFree: vi.fn().mockResolvedValue(undefined),
|
|
123
|
+
})),
|
|
124
|
+
delay: vi.fn().mockResolvedValue(undefined),
|
|
125
|
+
preventContentLength: vi.fn(),
|
|
126
|
+
waitUntilFileStable: vi.fn().mockResolvedValue(undefined),
|
|
115
127
|
}));
|
|
116
128
|
|
|
117
129
|
const mockConfig = {
|
|
@@ -140,6 +152,9 @@ describe("dev command", () => {
|
|
|
140
152
|
let mockServer: any;
|
|
141
153
|
|
|
142
154
|
beforeEach(async () => {
|
|
155
|
+
// Reset module-level state between tests
|
|
156
|
+
resetStateForTesting();
|
|
157
|
+
|
|
143
158
|
listenMock = vi.fn();
|
|
144
159
|
wsMock = {
|
|
145
160
|
send: vi.fn(),
|
|
@@ -192,9 +207,16 @@ describe("dev command", () => {
|
|
|
192
207
|
lifecycleWatcher: watcherMock,
|
|
193
208
|
});
|
|
194
209
|
|
|
210
|
+
// Return a proper stencil watcher mock so buildWebComponent does not crash
|
|
211
|
+
// when it tries to call .on() and .start() on the result of generate()
|
|
212
|
+
const stencilWatcherMock = { on: vi.fn(), start: vi.fn(), close: vi.fn() };
|
|
213
|
+
vi.mocked(generate).mockResolvedValue(stencilWatcherMock as any);
|
|
214
|
+
|
|
195
215
|
vi.mocked(buildWebComponent).mockImplementation(() => Promise.resolve());
|
|
196
216
|
vi.mocked(validate).mockImplementation(() => Promise.resolve(true));
|
|
197
217
|
|
|
218
|
+
vi.mocked(selectWorkspace).mockResolvedValue({ workspaceId: "mock-workspace" });
|
|
219
|
+
|
|
198
220
|
vi.mocked(findFiles).mockResolvedValue([
|
|
199
221
|
["mock-model.json", "/mock/root/models/mock-model.json"],
|
|
200
222
|
]);
|
|
@@ -340,7 +362,7 @@ describe("dev command", () => {
|
|
|
340
362
|
describe("globalHookWatcher", () => {
|
|
341
363
|
it("should call watcher.on", async () => {
|
|
342
364
|
const watcher = { on: vi.fn() } as unknown as RollupWatcher;
|
|
343
|
-
globalHookWatcher(watcher);
|
|
365
|
+
globalHookWatcher(watcher, "theme");
|
|
344
366
|
|
|
345
367
|
await expect
|
|
346
368
|
.poll(() => watcher.on)
|
|
@@ -1072,25 +1094,30 @@ describe("dev command", () => {
|
|
|
1072
1094
|
}),
|
|
1073
1095
|
} as unknown as RollupWatcher;
|
|
1074
1096
|
|
|
1075
|
-
await globalHookWatcher(watcher);
|
|
1097
|
+
await globalHookWatcher(watcher, "theme");
|
|
1076
1098
|
|
|
1077
1099
|
// Test change event
|
|
1078
1100
|
changeHandler!("/path/to/file.ts");
|
|
1079
1101
|
|
|
1080
|
-
// Test BUNDLE_START event
|
|
1102
|
+
// Test BUNDLE_START event — key="theme" → type "themeBuildStart"
|
|
1081
1103
|
await eventHandler!({ code: "BUNDLE_START" });
|
|
1082
1104
|
expect(mockWss.clients[0].send).toHaveBeenCalledWith(
|
|
1083
1105
|
JSON.stringify({
|
|
1084
|
-
type: "
|
|
1106
|
+
type: "themeBuildStart",
|
|
1085
1107
|
changedFiles: ["/path/to/file.ts"],
|
|
1086
1108
|
}),
|
|
1087
1109
|
);
|
|
1088
1110
|
|
|
1089
|
-
// Test BUNDLE_END event
|
|
1111
|
+
// Test BUNDLE_END event — key="theme" → type "themeBuildSuccess" with a numeric version
|
|
1090
1112
|
await eventHandler!({ code: "BUNDLE_END" });
|
|
1091
|
-
|
|
1092
|
-
JSON.
|
|
1113
|
+
const sentMessages = mockWss.clients[0].send.mock.calls.map((call: any) =>
|
|
1114
|
+
JSON.parse(call[0]),
|
|
1093
1115
|
);
|
|
1116
|
+
const bundleEndMsg = sentMessages.find(
|
|
1117
|
+
(msg: any) => msg.type === "themeBuildSuccess",
|
|
1118
|
+
);
|
|
1119
|
+
expect(bundleEndMsg).toBeDefined();
|
|
1120
|
+
expect(typeof bundleEndMsg.version).toBe("number");
|
|
1094
1121
|
|
|
1095
1122
|
// Test ERROR event
|
|
1096
1123
|
await eventHandler!({
|
|
@@ -1927,4 +1954,147 @@ describe("dev command", () => {
|
|
|
1927
1954
|
expect(mockPlugin.build).toHaveBeenCalled();
|
|
1928
1955
|
});
|
|
1929
1956
|
});
|
|
1957
|
+
|
|
1958
|
+
describe("onWebComponentBuildFinish", () => {
|
|
1959
|
+
it("should open the workspace page on first call (browserWindow is null)", async () => {
|
|
1960
|
+
const mockOpen = await import("open");
|
|
1961
|
+
|
|
1962
|
+
// Initialize wss by running dev() first
|
|
1963
|
+
await dev();
|
|
1964
|
+
|
|
1965
|
+
const buildResult = {
|
|
1966
|
+
hasSuccessfulBuild: true,
|
|
1967
|
+
hmr: undefined,
|
|
1968
|
+
} as any;
|
|
1969
|
+
|
|
1970
|
+
await onWebComponentBuildFinish(buildResult, mockConfig as any);
|
|
1971
|
+
|
|
1972
|
+
expect(mockOpen.default).toHaveBeenCalledWith(
|
|
1973
|
+
`${mockConfig.previewBaseUrl}/workspace/mock-workspace`,
|
|
1974
|
+
);
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
it("should send HMR message when build has HMR updates", async () => {
|
|
1978
|
+
const mockWss = {
|
|
1979
|
+
clients: [{ send: vi.fn() }],
|
|
1980
|
+
on: vi.fn(),
|
|
1981
|
+
close: vi.fn(),
|
|
1982
|
+
};
|
|
1983
|
+
const wsModule = await import("ws");
|
|
1984
|
+
vi.spyOn(wsModule, "WebSocketServer").mockImplementation(function() {
|
|
1985
|
+
return mockWss as any;
|
|
1986
|
+
} as any);
|
|
1987
|
+
|
|
1988
|
+
// open() must return a truthy ChildProcess so browserWindow is set on the first call
|
|
1989
|
+
vi.mocked(open.default).mockResolvedValue({ unref: vi.fn() } as any);
|
|
1990
|
+
|
|
1991
|
+
await dev();
|
|
1992
|
+
|
|
1993
|
+
// First call: browserWindow is null → opens workspace and returns early
|
|
1994
|
+
const firstBuild = { hasSuccessfulBuild: true, hmr: undefined } as any;
|
|
1995
|
+
await onWebComponentBuildFinish(firstBuild, mockConfig as any);
|
|
1996
|
+
|
|
1997
|
+
// Second call with HMR updates — browserWindow is now truthy, proceeds to send
|
|
1998
|
+
const hmrData = {
|
|
1999
|
+
componentsUpdated: ["my-component"],
|
|
2000
|
+
reloadStrategy: "hmr",
|
|
2001
|
+
};
|
|
2002
|
+
const buildResult = {
|
|
2003
|
+
hasSuccessfulBuild: true,
|
|
2004
|
+
hmr: hmrData,
|
|
2005
|
+
} as any;
|
|
2006
|
+
|
|
2007
|
+
await onWebComponentBuildFinish(buildResult, mockConfig as any);
|
|
2008
|
+
|
|
2009
|
+
const sentMessages = mockWss.clients[0].send.mock.calls.map((call: any) =>
|
|
2010
|
+
JSON.parse(call[0]),
|
|
2011
|
+
);
|
|
2012
|
+
expect(sentMessages).toContainEqual(
|
|
2013
|
+
expect.objectContaining({ type: "componentsBuildSuccessHmr" }),
|
|
2014
|
+
);
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
it("should send componentsBuildSuccess when no HMR updates", async () => {
|
|
2018
|
+
const mockWss = {
|
|
2019
|
+
clients: [{ send: vi.fn() }],
|
|
2020
|
+
on: vi.fn(),
|
|
2021
|
+
close: vi.fn(),
|
|
2022
|
+
};
|
|
2023
|
+
const wsModule = await import("ws");
|
|
2024
|
+
vi.spyOn(wsModule, "WebSocketServer").mockImplementation(function() {
|
|
2025
|
+
return mockWss as any;
|
|
2026
|
+
} as any);
|
|
2027
|
+
|
|
2028
|
+
// open() must return a truthy ChildProcess so browserWindow is set on the first call
|
|
2029
|
+
vi.mocked(open.default).mockResolvedValue({ unref: vi.fn() } as any);
|
|
2030
|
+
|
|
2031
|
+
await dev();
|
|
2032
|
+
|
|
2033
|
+
// First call: browserWindow is null → opens workspace and returns early
|
|
2034
|
+
const firstBuild = { hasSuccessfulBuild: true, hmr: undefined } as any;
|
|
2035
|
+
await onWebComponentBuildFinish(firstBuild, mockConfig as any);
|
|
2036
|
+
|
|
2037
|
+
// Second call without HMR — should send componentsBuildSuccess
|
|
2038
|
+
const buildResult = {
|
|
2039
|
+
hasSuccessfulBuild: true,
|
|
2040
|
+
hmr: { componentsUpdated: undefined, reloadStrategy: "page" },
|
|
2041
|
+
} as any;
|
|
2042
|
+
|
|
2043
|
+
await onWebComponentBuildFinish(buildResult, mockConfig as any);
|
|
2044
|
+
|
|
2045
|
+
const sentMessages = mockWss.clients[0].send.mock.calls.map((call: any) =>
|
|
2046
|
+
JSON.parse(call[0]),
|
|
2047
|
+
);
|
|
2048
|
+
expect(sentMessages).toContainEqual({ type: "componentsBuildSuccess" });
|
|
2049
|
+
});
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
describe("waitForStableHmrFiles", () => {
|
|
2053
|
+
it("should resolve immediately when componentGraph is undefined", async () => {
|
|
2054
|
+
await expect(
|
|
2055
|
+
waitForStableHmrFiles(undefined, mockConfig as any),
|
|
2056
|
+
).resolves.toBeUndefined();
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
it("should resolve immediately when componentGraph is empty", async () => {
|
|
2060
|
+
await expect(
|
|
2061
|
+
waitForStableHmrFiles({}, mockConfig as any),
|
|
2062
|
+
).resolves.toBeUndefined();
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
it("should wait for embeddable-component files to stabilize", async () => {
|
|
2066
|
+
const { waitUntilFileStable } = await import("./utils/dev.utils");
|
|
2067
|
+
|
|
2068
|
+
const componentGraph = {
|
|
2069
|
+
"my-component": [
|
|
2070
|
+
"embeddable-component-my.js",
|
|
2071
|
+
"other-file.js",
|
|
2072
|
+
],
|
|
2073
|
+
} as any;
|
|
2074
|
+
|
|
2075
|
+
await waitForStableHmrFiles(componentGraph, mockConfig as any);
|
|
2076
|
+
|
|
2077
|
+
// Only files starting with "embeddable-component" are waited on
|
|
2078
|
+
expect(waitUntilFileStable).toHaveBeenCalledTimes(1);
|
|
2079
|
+
expect(waitUntilFileStable).toHaveBeenCalledWith(
|
|
2080
|
+
expect.stringContaining("embeddable-component-my.js"),
|
|
2081
|
+
"sourceMappingURL",
|
|
2082
|
+
);
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it("should wait for all embeddable-component files across multiple components", async () => {
|
|
2086
|
+
const { waitUntilFileStable } = await import("./utils/dev.utils");
|
|
2087
|
+
vi.mocked(waitUntilFileStable).mockClear();
|
|
2088
|
+
|
|
2089
|
+
const componentGraph = {
|
|
2090
|
+
"comp-a": ["embeddable-component-a.js"],
|
|
2091
|
+
"comp-b": ["embeddable-component-b.js", "embeddable-component-b-chunk.js"],
|
|
2092
|
+
} as any;
|
|
2093
|
+
|
|
2094
|
+
await waitForStableHmrFiles(componentGraph, mockConfig as any);
|
|
2095
|
+
|
|
2096
|
+
expect(waitUntilFileStable).toHaveBeenCalledTimes(3);
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
1930
2099
|
});
|
|
2100
|
+
|
package/src/dev.ts
CHANGED
|
@@ -3,9 +3,10 @@ import buildTypes, {
|
|
|
3
3
|
EMB_TYPE_FILE_REGEX,
|
|
4
4
|
} from "./buildTypes";
|
|
5
5
|
import prepare, { removeIfExists } from "./prepare";
|
|
6
|
-
import generate from "./generate";
|
|
6
|
+
import generate, {generateDTS, triggerWebComponentRebuild} from "./generate";
|
|
7
7
|
import open from "open";
|
|
8
8
|
import provideConfig from "./provideConfig";
|
|
9
|
+
import { CompilerBuildResults, CompilerWatcher } from "@stencil/core/compiler";
|
|
9
10
|
import {
|
|
10
11
|
CompilerSystem,
|
|
11
12
|
createNodeLogger,
|
|
@@ -42,6 +43,13 @@ import finalhandler from "finalhandler";
|
|
|
42
43
|
import serveStatic from "serve-static";
|
|
43
44
|
import { ResolvedEmbeddableConfig } from "./defineConfig";
|
|
44
45
|
import buildGlobalHooks from "./buildGlobalHooks";
|
|
46
|
+
import {
|
|
47
|
+
createWatcherLock,
|
|
48
|
+
delay,
|
|
49
|
+
preventContentLength,
|
|
50
|
+
waitUntilFileStable
|
|
51
|
+
} from "./utils/dev.utils";
|
|
52
|
+
import { BuildResultsComponentGraph } from "@stencil/core/internal";
|
|
45
53
|
|
|
46
54
|
type FSWatcher = chokidar.FSWatcher;
|
|
47
55
|
|
|
@@ -62,8 +70,30 @@ const BUILD_DEV_DIR = ".embeddable-dev-build";
|
|
|
62
70
|
// NOTE: for backward compatibility, keep the file name as global.css
|
|
63
71
|
const CUSTOM_CANVAS_CSS = "/global.css";
|
|
64
72
|
|
|
73
|
+
let stencilWatcher: CompilerWatcher | undefined;
|
|
74
|
+
let isActiveBundleBuild = false;
|
|
75
|
+
|
|
76
|
+
/** We use two steps compilation for embeddable components.
|
|
77
|
+
* 1. Compile *emb.ts files using plugin complier (sdk-react)
|
|
78
|
+
* 2. Compile the web component using Stencil compiler.
|
|
79
|
+
* These compilations can happen in parallel, but we need to ensure that
|
|
80
|
+
* the first step is not started until the second step is finished (if recompilation is needed).
|
|
81
|
+
* We use this lock to lock it before the second step starts and unlock it after the second step is finished.
|
|
82
|
+
* */
|
|
83
|
+
const lock = createWatcherLock();
|
|
84
|
+
|
|
65
85
|
export const buildWebComponent = async (config: any) => {
|
|
66
|
-
|
|
86
|
+
// if there is no watcher, then this is the first build. We need to create a watcher
|
|
87
|
+
// otherwise we can just trigger a rebuild
|
|
88
|
+
if (!stencilWatcher) {
|
|
89
|
+
stencilWatcher = (await generate(config, "sdk-react")) as CompilerWatcher;
|
|
90
|
+
stencilWatcher.on("buildFinish", (e) =>
|
|
91
|
+
onWebComponentBuildFinish(e, config),
|
|
92
|
+
);
|
|
93
|
+
stencilWatcher.start();
|
|
94
|
+
} else {
|
|
95
|
+
await triggerWebComponentRebuild(config);
|
|
96
|
+
}
|
|
67
97
|
};
|
|
68
98
|
|
|
69
99
|
const executePluginBuilds = async (
|
|
@@ -175,7 +205,14 @@ export default async () => {
|
|
|
175
205
|
breadcrumbs.push("prepare config");
|
|
176
206
|
await prepare(config);
|
|
177
207
|
|
|
178
|
-
const serve = serveStatic(config.client.buildDir
|
|
208
|
+
const serve = serveStatic(config.client.buildDir, {
|
|
209
|
+
setHeaders: (res, path) => {
|
|
210
|
+
if (path.includes("/dist/embeddable-wrapper/")) {
|
|
211
|
+
// Prevent content length for HMR files
|
|
212
|
+
preventContentLength(res);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
179
216
|
|
|
180
217
|
let workspacePreparation = ora("Preparing workspace...").start();
|
|
181
218
|
|
|
@@ -233,11 +270,36 @@ export default async () => {
|
|
|
233
270
|
}
|
|
234
271
|
} catch {}
|
|
235
272
|
|
|
273
|
+
// Last line of defence: wait for the file to be fully written before
|
|
274
|
+
// handing it to serve-static. This catches any race condition between
|
|
275
|
+
// the WS "build success" notification and the actual HTTP request —
|
|
276
|
+
// e.g. when buildFinish fires slightly before Stencil flushes files.
|
|
277
|
+
const urlPath = (request.url ?? "").split("?")[0];
|
|
278
|
+
if (
|
|
279
|
+
urlPath.includes("/dist/embeddable-wrapper/") &&
|
|
280
|
+
urlPath.endsWith(".js")
|
|
281
|
+
) {
|
|
282
|
+
const filePath = path.resolve(
|
|
283
|
+
config.client.buildDir,
|
|
284
|
+
urlPath.slice(1),
|
|
285
|
+
);
|
|
286
|
+
await waitUntilFileStable(filePath, "sourceMappingURL", {
|
|
287
|
+
maxAttempts: 40, // up to ~2 s; fast in the happy path
|
|
288
|
+
requiredStableCount: 2,
|
|
289
|
+
}).catch(() => {
|
|
290
|
+
// If the check times out we still serve — better a partial file
|
|
291
|
+
// warning in the console than a hung request.
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
236
295
|
serve(request, res, done);
|
|
237
296
|
},
|
|
238
297
|
);
|
|
239
298
|
|
|
240
299
|
const { themeWatcher, lifecycleWatcher } = await buildGlobalHooks(config);
|
|
300
|
+
const dtsOra = ora("Generating component type files...").start();
|
|
301
|
+
await generateDTS(config)
|
|
302
|
+
dtsOra.succeed("Component type files generated");
|
|
241
303
|
|
|
242
304
|
wss = new WebSocketServer({ server });
|
|
243
305
|
server.listen(SERVER_PORT, async () => {
|
|
@@ -276,11 +338,11 @@ export default async () => {
|
|
|
276
338
|
watchers.push(customCanvasCssWatch);
|
|
277
339
|
|
|
278
340
|
if (themeWatcher) {
|
|
279
|
-
await globalHookWatcher(themeWatcher);
|
|
341
|
+
await globalHookWatcher(themeWatcher, "themeProvider");
|
|
280
342
|
watchers.push(themeWatcher);
|
|
281
343
|
}
|
|
282
344
|
if (lifecycleWatcher) {
|
|
283
|
-
await globalHookWatcher(lifecycleWatcher);
|
|
345
|
+
await globalHookWatcher(lifecycleWatcher, "lifecycleHook");
|
|
284
346
|
watchers.push(lifecycleWatcher);
|
|
285
347
|
}
|
|
286
348
|
} else {
|
|
@@ -307,31 +369,52 @@ export const configureWatcher = async (
|
|
|
307
369
|
});
|
|
308
370
|
|
|
309
371
|
watcher.on("event", async (e) => {
|
|
372
|
+
if (e.code === "START") {
|
|
373
|
+
await lock.waitUntilFree();
|
|
374
|
+
}
|
|
310
375
|
if (e.code === "BUNDLE_START") {
|
|
376
|
+
isActiveBundleBuild = true;
|
|
311
377
|
await onBuildStart(ctx);
|
|
312
378
|
}
|
|
313
379
|
if (e.code === "BUNDLE_END") {
|
|
380
|
+
lock.lock();
|
|
381
|
+
isActiveBundleBuild = false;
|
|
382
|
+
if (stencilWatcher && shouldRebuildWebComponent()) {
|
|
383
|
+
try {
|
|
384
|
+
await fs.rm(
|
|
385
|
+
path.resolve(ctx.client.buildDir, "dist", "embeddable-wrapper"),
|
|
386
|
+
{ recursive: true },
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error("Error cleaning up build directory:", error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
314
392
|
await onBundleBuildEnd(ctx);
|
|
315
393
|
changedFiles = [];
|
|
316
394
|
}
|
|
317
395
|
if (e.code === "ERROR") {
|
|
396
|
+
lock.unlock();
|
|
397
|
+
isActiveBundleBuild = false;
|
|
318
398
|
sendMessage("componentsBuildError", { error: e.error?.message });
|
|
319
399
|
changedFiles = [];
|
|
320
400
|
}
|
|
321
401
|
});
|
|
322
402
|
};
|
|
323
403
|
|
|
324
|
-
export const globalHookWatcher = async (
|
|
404
|
+
export const globalHookWatcher = async (
|
|
405
|
+
watcher: RollupWatcher,
|
|
406
|
+
key: string,
|
|
407
|
+
) => {
|
|
325
408
|
watcher.on("change", (path) => {
|
|
326
409
|
changedFiles.push(path);
|
|
327
410
|
});
|
|
328
411
|
|
|
329
412
|
watcher.on("event", async (e) => {
|
|
330
413
|
if (e.code === "BUNDLE_START") {
|
|
331
|
-
sendMessage(
|
|
414
|
+
sendMessage(`${key}BuildStart`, { changedFiles });
|
|
332
415
|
}
|
|
333
416
|
if (e.code === "BUNDLE_END") {
|
|
334
|
-
sendMessage(
|
|
417
|
+
sendMessage(`${key}BuildSuccess`, { version: new Date().getTime() });
|
|
335
418
|
changedFiles = [];
|
|
336
419
|
}
|
|
337
420
|
if (e.code === "ERROR") {
|
|
@@ -371,16 +454,15 @@ export const openDevWorkspacePage = async (
|
|
|
371
454
|
return await open(`${previewBaseUrl}/workspace/${workspaceId}`);
|
|
372
455
|
};
|
|
373
456
|
|
|
457
|
+
function shouldRebuildWebComponent() {
|
|
458
|
+
return !onlyTypesChanged() || changedFiles.length === 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
374
461
|
const onBundleBuildEnd = async (ctx: ResolvedEmbeddableConfig) => {
|
|
375
|
-
if (
|
|
462
|
+
if (shouldRebuildWebComponent()) {
|
|
376
463
|
await buildWebComponent(ctx);
|
|
377
|
-
}
|
|
378
|
-
if (browserWindow == null) {
|
|
379
|
-
browserWindow = await openDevWorkspacePage(
|
|
380
|
-
ctx.previewBaseUrl,
|
|
381
|
-
previewWorkspace,
|
|
382
|
-
);
|
|
383
464
|
} else {
|
|
465
|
+
lock.unlock();
|
|
384
466
|
sendMessage("componentsBuildSuccess");
|
|
385
467
|
}
|
|
386
468
|
};
|
|
@@ -514,6 +596,7 @@ const onClose = async (
|
|
|
514
596
|
server.close();
|
|
515
597
|
wss.close();
|
|
516
598
|
browserWindow?.unref();
|
|
599
|
+
await stencilWatcher?.close();
|
|
517
600
|
for (const watcher of watchers) {
|
|
518
601
|
if (watcher.close) {
|
|
519
602
|
await watcher.close();
|
|
@@ -572,3 +655,68 @@ const getPreviewWorkspace = async (
|
|
|
572
655
|
}
|
|
573
656
|
}
|
|
574
657
|
};
|
|
658
|
+
|
|
659
|
+
export async function onWebComponentBuildFinish(
|
|
660
|
+
e: CompilerBuildResults,
|
|
661
|
+
config: ResolvedEmbeddableConfig,
|
|
662
|
+
) {
|
|
663
|
+
lock.unlock();
|
|
664
|
+
|
|
665
|
+
if (!browserWindow) {
|
|
666
|
+
browserWindow = await openDevWorkspacePage(
|
|
667
|
+
config.previewBaseUrl,
|
|
668
|
+
previewWorkspace,
|
|
669
|
+
);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
await delay(50);
|
|
674
|
+
if (isActiveBundleBuild) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (
|
|
679
|
+
e.hasSuccessfulBuild &&
|
|
680
|
+
e.hmr?.componentsUpdated &&
|
|
681
|
+
e.hmr.reloadStrategy === "hmr"
|
|
682
|
+
) {
|
|
683
|
+
try {
|
|
684
|
+
await waitForStableHmrFiles(e.componentGraph, config);
|
|
685
|
+
} finally {
|
|
686
|
+
sendMessage("componentsBuildSuccessHmr", e.hmr);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
sendMessage("componentsBuildSuccess");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export async function waitForStableHmrFiles(
|
|
694
|
+
componentGraph: BuildResultsComponentGraph | undefined,
|
|
695
|
+
config: ResolvedEmbeddableConfig,
|
|
696
|
+
) {
|
|
697
|
+
const promises = [];
|
|
698
|
+
|
|
699
|
+
for (const files of Object.values(componentGraph ?? {})) {
|
|
700
|
+
for (const file of files) {
|
|
701
|
+
if (file.startsWith("embeddable-component")) {
|
|
702
|
+
const fullPath = path.resolve(
|
|
703
|
+
config.client.buildDir,
|
|
704
|
+
"dist",
|
|
705
|
+
"embeddable-wrapper",
|
|
706
|
+
file,
|
|
707
|
+
);
|
|
708
|
+
promises.push(waitUntilFileStable(fullPath, "sourceMappingURL"));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
await Promise.all(promises);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function resetStateForTesting() {
|
|
717
|
+
stencilWatcher = undefined;
|
|
718
|
+
isActiveBundleBuild = false;
|
|
719
|
+
pluginBuildInProgress = false;
|
|
720
|
+
pendingPluginBuilds = [];
|
|
721
|
+
browserWindow = null;
|
|
722
|
+
}
|