@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
package/src/generate.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import generate
|
|
1
|
+
import generate, {
|
|
2
|
+
resetForTesting, TRIGGER_BUILD_ITERATION_LIMIT,
|
|
3
|
+
triggerWebComponentRebuild, generateDTS
|
|
4
|
+
} from "./generate";
|
|
2
5
|
import * as fs from "node:fs/promises";
|
|
3
6
|
import * as path from "node:path";
|
|
4
7
|
import { checkNodeVersion } from "./utils";
|
|
@@ -52,6 +55,10 @@ vi.mock("node:fs/promises", () => ({
|
|
|
52
55
|
rename: vi.fn(),
|
|
53
56
|
cp: vi.fn(),
|
|
54
57
|
rm: vi.fn(),
|
|
58
|
+
copyFile: vi.fn(),
|
|
59
|
+
stat: vi.fn(),
|
|
60
|
+
truncate: vi.fn(),
|
|
61
|
+
appendFile: vi.fn(),
|
|
55
62
|
}));
|
|
56
63
|
|
|
57
64
|
vi.mock("node:path", async () => {
|
|
@@ -69,6 +76,10 @@ vi.mock("@stencil/core/compiler", () => ({
|
|
|
69
76
|
}));
|
|
70
77
|
|
|
71
78
|
describe("generate", () => {
|
|
79
|
+
const watcherMock = vi.fn().mockResolvedValue({
|
|
80
|
+
hasError: false,
|
|
81
|
+
on: vi.fn(),
|
|
82
|
+
});
|
|
72
83
|
beforeEach(() => {
|
|
73
84
|
vi.mocked(checkNodeVersion).mockResolvedValue(true);
|
|
74
85
|
vi.mocked(fs.readdir).mockResolvedValue([
|
|
@@ -86,6 +97,7 @@ describe("generate", () => {
|
|
|
86
97
|
hasError: false,
|
|
87
98
|
}),
|
|
88
99
|
destroy: vi.fn(),
|
|
100
|
+
createWatcher: watcherMock,
|
|
89
101
|
} as any);
|
|
90
102
|
|
|
91
103
|
vi.mocked(getContentHash).mockReturnValue("hash");
|
|
@@ -124,7 +136,9 @@ describe("generate", () => {
|
|
|
124
136
|
dev: {
|
|
125
137
|
watch: true,
|
|
126
138
|
logger: vi.fn(),
|
|
127
|
-
sys: vi.
|
|
139
|
+
sys: vi.mocked({
|
|
140
|
+
onProcessInterrupt: vi.fn(),
|
|
141
|
+
}),
|
|
128
142
|
},
|
|
129
143
|
};
|
|
130
144
|
|
|
@@ -134,6 +148,7 @@ describe("generate", () => {
|
|
|
134
148
|
await generate(ctx as unknown as ResolvedEmbeddableConfig, "sdk-react");
|
|
135
149
|
|
|
136
150
|
expect(createCompiler).toHaveBeenCalled();
|
|
151
|
+
expect(watcherMock).toHaveBeenCalled();
|
|
137
152
|
|
|
138
153
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
139
154
|
"componentDir/component.tsx",
|
|
@@ -144,6 +159,7 @@ describe("generate", () => {
|
|
|
144
159
|
config: {
|
|
145
160
|
configPath: "webComponentRoot/stencil.config.ts",
|
|
146
161
|
devMode: true,
|
|
162
|
+
watchIgnoredRegex: [/\.css$/, /\.d\.ts$/, /\.js$/],
|
|
147
163
|
maxConcurrentWorkers: process.platform === "win32" ? 0 : 8,
|
|
148
164
|
minifyCss: false,
|
|
149
165
|
minifyJs: false,
|
|
@@ -161,7 +177,175 @@ describe("generate", () => {
|
|
|
161
177
|
},
|
|
162
178
|
initTsConfig: true,
|
|
163
179
|
logger: expect.any(Function),
|
|
164
|
-
sys:
|
|
180
|
+
sys: {
|
|
181
|
+
onProcessInterrupt: expect.any(Function),
|
|
182
|
+
},
|
|
165
183
|
});
|
|
166
184
|
});
|
|
167
185
|
});
|
|
186
|
+
|
|
187
|
+
describe("triggerWebComponentRebuild", () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
vi.clearAllMocks();
|
|
190
|
+
resetForTesting();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should store original file stats on first call and append file", async () => {
|
|
194
|
+
const mockStats = { size: 123 };
|
|
195
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
196
|
+
|
|
197
|
+
await triggerWebComponentRebuild(
|
|
198
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const filePath = path.resolve(config.client.componentDir, "component.tsx");
|
|
202
|
+
expect(fs.stat).toHaveBeenCalledWith(filePath);
|
|
203
|
+
expect(fs.appendFile).toHaveBeenCalledWith(filePath, " ");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should append file and not call stat after first build", async () => {
|
|
207
|
+
const mockStats = { size: 123 };
|
|
208
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < 3; i++) {
|
|
211
|
+
await triggerWebComponentRebuild(
|
|
212
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
expect(fs.stat).toHaveBeenCalledTimes(1); // only once
|
|
217
|
+
expect(fs.appendFile).toHaveBeenCalledTimes(3);
|
|
218
|
+
expect(fs.truncate).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should reset file using truncate on the 6th call and reset count", async () => {
|
|
222
|
+
const mockStats = { size: 321 };
|
|
223
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
224
|
+
vi.mocked(path.resolve).mockReturnValue("componentDir/component.tsx");
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < TRIGGER_BUILD_ITERATION_LIMIT; i++) {
|
|
227
|
+
await triggerWebComponentRebuild(
|
|
228
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(fs.truncate).not.toHaveBeenCalled();
|
|
233
|
+
|
|
234
|
+
vi.mocked(fs.appendFile).mockClear();
|
|
235
|
+
vi.mocked(fs.truncate).mockClear();
|
|
236
|
+
|
|
237
|
+
// now truncate should be called
|
|
238
|
+
await triggerWebComponentRebuild(
|
|
239
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
240
|
+
);
|
|
241
|
+
const filePath = path.resolve(config.client.componentDir, "component.tsx");
|
|
242
|
+
|
|
243
|
+
expect(fs.truncate).toHaveBeenCalledWith(filePath, mockStats.size);
|
|
244
|
+
expect(fs.appendFile).not.toHaveBeenCalledWith(filePath, " ");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("generateDTS", () => {
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
vi.mocked(fs.readdir).mockResolvedValue([
|
|
251
|
+
"embeddable-wrapper.esm.js",
|
|
252
|
+
] as any);
|
|
253
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
254
|
+
// Template contains both tokens so we can verify replacement
|
|
255
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
256
|
+
"replace-this-with-component-name {{RENDER_IMPORT}}",
|
|
257
|
+
);
|
|
258
|
+
vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
|
|
259
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
260
|
+
build: vi.fn().mockResolvedValue({ hasError: false }),
|
|
261
|
+
destroy: vi.fn(),
|
|
262
|
+
createWatcher: vi.fn(),
|
|
263
|
+
} as any);
|
|
264
|
+
vi.mocked(findFiles).mockResolvedValue([["", ""]]);
|
|
265
|
+
Object.defineProperties(process, { chdir: { value: vi.fn() } });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should write an empty style.css", async () => {
|
|
269
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
270
|
+
|
|
271
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
272
|
+
"componentDir/style.css",
|
|
273
|
+
"",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should write component.tsx with stub render and embeddable-component tag", async () => {
|
|
278
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
279
|
+
|
|
280
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
281
|
+
"componentDir/component.tsx",
|
|
282
|
+
expect.stringContaining("embeddable-component"),
|
|
283
|
+
);
|
|
284
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
285
|
+
"componentDir/component.tsx",
|
|
286
|
+
expect.stringContaining("const render = () => {};"),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should call loadConfig with devMode=false and sourceMap=false", async () => {
|
|
291
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
292
|
+
|
|
293
|
+
expect(loadConfig).toHaveBeenCalledWith(
|
|
294
|
+
expect.objectContaining({
|
|
295
|
+
config: expect.objectContaining({
|
|
296
|
+
devMode: false,
|
|
297
|
+
sourceMap: false,
|
|
298
|
+
minifyJs: false,
|
|
299
|
+
minifyCss: false,
|
|
300
|
+
}),
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should not create a watcher (not watch mode)", async () => {
|
|
306
|
+
const createWatcherMock = vi.fn();
|
|
307
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
308
|
+
build: vi.fn().mockResolvedValue({ hasError: false }),
|
|
309
|
+
destroy: vi.fn(),
|
|
310
|
+
createWatcher: createWatcherMock,
|
|
311
|
+
} as any);
|
|
312
|
+
|
|
313
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
314
|
+
|
|
315
|
+
expect(createWatcherMock).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("generate stencil build error", () => {
|
|
320
|
+
beforeEach(() => {
|
|
321
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
322
|
+
vi.mocked(fs.readFile).mockResolvedValue("");
|
|
323
|
+
vi.mocked(fs.readdir).mockResolvedValue([] as any);
|
|
324
|
+
vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
|
|
325
|
+
vi.mocked(findFiles).mockResolvedValue([["", ""]]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should throw when Stencil build has errors", async () => {
|
|
329
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
330
|
+
build: vi.fn().mockResolvedValue({
|
|
331
|
+
hasError: true,
|
|
332
|
+
diagnostics: [{ messageText: "type error" }],
|
|
333
|
+
}),
|
|
334
|
+
destroy: vi.fn(),
|
|
335
|
+
createWatcher: vi.fn(),
|
|
336
|
+
} as any);
|
|
337
|
+
|
|
338
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
339
|
+
|
|
340
|
+
await expect(
|
|
341
|
+
generate(config as unknown as ResolvedEmbeddableConfig, "sdk-react"),
|
|
342
|
+
).rejects.toThrow("Stencil build error");
|
|
343
|
+
|
|
344
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
345
|
+
"Stencil build error:",
|
|
346
|
+
expect.anything(),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
consoleSpy.mockRestore();
|
|
350
|
+
});
|
|
351
|
+
});
|
package/src/generate.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { createNodeLogger, createNodeSys } from "@stencil/core/sys/node";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CompilerWatcher,
|
|
6
|
+
createCompiler,
|
|
7
|
+
loadConfig,
|
|
8
|
+
} from "@stencil/core/compiler";
|
|
5
9
|
import { PluginName, ResolvedEmbeddableConfig } from "./defineConfig";
|
|
6
10
|
import {
|
|
7
11
|
findFiles,
|
|
@@ -9,6 +13,8 @@ import {
|
|
|
9
13
|
} from "@embeddable.com/sdk-utils";
|
|
10
14
|
|
|
11
15
|
import * as sorcery from "sorcery";
|
|
16
|
+
import { Stats } from "node:fs";
|
|
17
|
+
import type { Logger } from "@stencil/core/internal";
|
|
12
18
|
|
|
13
19
|
const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}";
|
|
14
20
|
const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";
|
|
@@ -16,19 +22,92 @@ const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";
|
|
|
16
22
|
// stencil doesn't support dynamic component tag name, so we need to replace it manually
|
|
17
23
|
const COMPONENT_TAG_TOKEN = "replace-this-with-component-name";
|
|
18
24
|
|
|
25
|
+
let triggeredBuildCount = 0;
|
|
26
|
+
/**
|
|
27
|
+
* Stencil watcher doesnt react on file metadata changes,
|
|
28
|
+
* so we have to change the file content to trigger a rebuild by appending a space character.
|
|
29
|
+
* This constant defines how many times the space character can be appended before the file is truncated back to its original size.
|
|
30
|
+
*/
|
|
31
|
+
export const TRIGGER_BUILD_ITERATION_LIMIT = 5;
|
|
32
|
+
let originalFileStats: Stats | null = null;
|
|
33
|
+
|
|
34
|
+
export function resetForTesting() {
|
|
35
|
+
triggeredBuildCount = 0;
|
|
36
|
+
originalFileStats = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Triggers a rebuild of a Stencil web component by modifying the `component.tsx` file.
|
|
41
|
+
*
|
|
42
|
+
* This function works by appending a space character to the file, which causes Stencil's watcher
|
|
43
|
+
* to detect a change and rebuild the component. After every TRIGGER_BUILD_ITERATION_LIMIT rebuilds, the file is truncated back
|
|
44
|
+
* to its original size to prevent indefinite growth and reset the internal rebuild counter.
|
|
45
|
+
*
|
|
46
|
+
* Append and truncate are used instead of rewriting the file to ensure minimal I/O overhead and preserve file metadata.
|
|
47
|
+
*/
|
|
48
|
+
export async function triggerWebComponentRebuild(
|
|
49
|
+
ctx: ResolvedEmbeddableConfig,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const filePath = path.resolve(ctx.client.componentDir, "component.tsx");
|
|
52
|
+
|
|
53
|
+
if (triggeredBuildCount === 0) {
|
|
54
|
+
// store original file stats on the first build
|
|
55
|
+
originalFileStats = await fs.stat(filePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (triggeredBuildCount === TRIGGER_BUILD_ITERATION_LIMIT && originalFileStats) {
|
|
59
|
+
await fs.truncate(filePath, originalFileStats.size);
|
|
60
|
+
triggeredBuildCount = 0; // reset the counter after resetting the file
|
|
61
|
+
} else {
|
|
62
|
+
await fs.appendFile(filePath, " ");
|
|
63
|
+
triggeredBuildCount++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
19
67
|
export default async (
|
|
20
68
|
ctx: ResolvedEmbeddableConfig,
|
|
21
69
|
pluginName: PluginName,
|
|
22
|
-
) => {
|
|
70
|
+
): Promise<void | CompilerWatcher> => {
|
|
23
71
|
await injectCSS(ctx, pluginName);
|
|
24
72
|
|
|
25
73
|
await injectBundleRender(ctx, pluginName);
|
|
26
74
|
|
|
27
|
-
await runStencil(ctx);
|
|
75
|
+
const watcher = await runStencil(ctx);
|
|
76
|
+
|
|
77
|
+
if (watcher) {
|
|
78
|
+
watcher.on("buildFinish", () => {
|
|
79
|
+
// stencil always changes the working directory to the root of the web component.
|
|
80
|
+
// We need to change it back to the client root directory
|
|
81
|
+
process.chdir(ctx.client.rootDir);
|
|
82
|
+
generateSourceMap(ctx, pluginName);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
await generateSourceMap(ctx, pluginName);
|
|
86
|
+
}
|
|
28
87
|
|
|
29
|
-
|
|
88
|
+
return watcher;
|
|
30
89
|
};
|
|
31
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Generates only the d.ts type declaration files using Stencil, without performing a full build.
|
|
93
|
+
* Used in dev mode to pre-generate types before the watcher starts, avoiding a double-build
|
|
94
|
+
* triggered by the watcher reacting to freshly generated d.ts files.
|
|
95
|
+
*
|
|
96
|
+
* Key differences from the default generate function:
|
|
97
|
+
* - Writes an empty style.css stub (no real CSS injection needed for type generation)
|
|
98
|
+
* - Injects a no-op render stub instead of the real render import
|
|
99
|
+
* - Always creates a fresh sys (never reuses ctx.dev?.sys) to avoid watcher interference
|
|
100
|
+
*/
|
|
101
|
+
export async function generateDTS(
|
|
102
|
+
ctx: ResolvedEmbeddableConfig,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
await injectEmptyCSS(ctx);
|
|
105
|
+
|
|
106
|
+
await injectBundleRenderStub(ctx);
|
|
107
|
+
|
|
108
|
+
await runStencil(ctx, { dtsOnly: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
32
111
|
async function injectCSS(
|
|
33
112
|
ctx: ResolvedEmbeddableConfig,
|
|
34
113
|
pluginName: PluginName,
|
|
@@ -39,12 +118,14 @@ async function injectCSS(
|
|
|
39
118
|
);
|
|
40
119
|
const allFiles = await fs.readdir(CUSTOMER_BUILD);
|
|
41
120
|
|
|
121
|
+
const importFilePath = path.relative(
|
|
122
|
+
ctx.client.componentDir,
|
|
123
|
+
path.resolve(ctx.client.buildDir, ctx[pluginName].outputOptions.buildName),
|
|
124
|
+
);
|
|
125
|
+
|
|
42
126
|
const imports = allFiles
|
|
43
127
|
.filter((fileName) => fileName.endsWith(".css"))
|
|
44
|
-
.map(
|
|
45
|
-
(fileName) =>
|
|
46
|
-
`@import '../../${ctx[pluginName].outputOptions.buildName}/${fileName}';`,
|
|
47
|
-
);
|
|
128
|
+
.map((fileName) => `@import '${importFilePath}/${fileName}';`);
|
|
48
129
|
|
|
49
130
|
const componentLibraries = ctx.client.componentLibraries;
|
|
50
131
|
for (const componentLibrary of componentLibraries) {
|
|
@@ -76,7 +157,11 @@ async function injectBundleRender(
|
|
|
76
157
|
ctx: ResolvedEmbeddableConfig,
|
|
77
158
|
pluginName: PluginName,
|
|
78
159
|
) {
|
|
79
|
-
const
|
|
160
|
+
const importFilePath = path.relative(
|
|
161
|
+
ctx.client.componentDir,
|
|
162
|
+
path.resolve(ctx.client.buildDir, ctx[pluginName].outputOptions.buildName),
|
|
163
|
+
);
|
|
164
|
+
const importStr = `import render from '${importFilePath}/${ctx[pluginName].outputOptions.fileName}';`;
|
|
80
165
|
|
|
81
166
|
let content = await fs.readFile(
|
|
82
167
|
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
@@ -93,6 +178,27 @@ async function injectBundleRender(
|
|
|
93
178
|
);
|
|
94
179
|
}
|
|
95
180
|
|
|
181
|
+
async function injectEmptyCSS(ctx: ResolvedEmbeddableConfig) {
|
|
182
|
+
await fs.writeFile(path.resolve(ctx.client.componentDir, "style.css"), "");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function injectBundleRenderStub(
|
|
186
|
+
ctx: ResolvedEmbeddableConfig,
|
|
187
|
+
) {
|
|
188
|
+
const stubStr = `const render = () => {};`;
|
|
189
|
+
|
|
190
|
+
let content = await fs.readFile(
|
|
191
|
+
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
192
|
+
"utf8",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component");
|
|
196
|
+
await fs.writeFile(
|
|
197
|
+
path.resolve(ctx.client.componentDir, "component.tsx"),
|
|
198
|
+
content.replace(RENDER_IMPORT_TOKEN, stubStr),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
96
202
|
async function addComponentTagName(filePath: string, bundleHash: string) {
|
|
97
203
|
// find entry file with a name *.entry.js
|
|
98
204
|
const entryFiles = await findFiles(path.dirname(filePath), /.*\.entry\.js/);
|
|
@@ -123,10 +229,20 @@ async function addComponentTagName(filePath: string, bundleHash: string) {
|
|
|
123
229
|
]);
|
|
124
230
|
}
|
|
125
231
|
|
|
126
|
-
async function runStencil(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
232
|
+
async function runStencil(
|
|
233
|
+
ctx: ResolvedEmbeddableConfig,
|
|
234
|
+
options?: { dtsOnly?: boolean },
|
|
235
|
+
): Promise<void | CompilerWatcher> {
|
|
236
|
+
const logger = (options?.dtsOnly ? createNodeLogger() : ctx.dev?.logger || createNodeLogger()) as Logger;
|
|
237
|
+
const sys = options?.dtsOnly ? createNodeSys({ process }) : (ctx.dev?.sys || createNodeSys({ process }));
|
|
238
|
+
const devMode = !!ctx.dev?.watch && !options?.dtsOnly;
|
|
239
|
+
if (options?.dtsOnly) {
|
|
240
|
+
logger.setLevel("error")
|
|
241
|
+
logger.createTimeSpan = () => ({
|
|
242
|
+
duration: () => 0,
|
|
243
|
+
finish: () => 0,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
130
246
|
|
|
131
247
|
const isWindows = process.platform === "win32";
|
|
132
248
|
|
|
@@ -137,6 +253,8 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
137
253
|
config: {
|
|
138
254
|
devMode,
|
|
139
255
|
maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows
|
|
256
|
+
// we will trigger a rebuild by updating the component.tsx file (see triggerBuild function)
|
|
257
|
+
watchIgnoredRegex: [/\.css$/, /\.d\.ts$/, /\.js$/],
|
|
140
258
|
rootDir: ctx.client.webComponentRoot,
|
|
141
259
|
configPath: path.resolve(
|
|
142
260
|
ctx.client.webComponentRoot,
|
|
@@ -145,9 +263,9 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
145
263
|
tsconfig: path.resolve(ctx.client.webComponentRoot, "tsconfig.json"),
|
|
146
264
|
namespace: "embeddable-wrapper",
|
|
147
265
|
srcDir: ctx.client.componentDir,
|
|
148
|
-
sourceMap:
|
|
149
|
-
minifyJs: !devMode,
|
|
150
|
-
minifyCss: !devMode,
|
|
266
|
+
sourceMap: !options?.dtsOnly, // always generate source maps in both dev and prod
|
|
267
|
+
minifyJs: !devMode && !options?.dtsOnly,
|
|
268
|
+
minifyCss: !devMode && !options?.dtsOnly,
|
|
151
269
|
outputTargets: [
|
|
152
270
|
{
|
|
153
271
|
type: "dist",
|
|
@@ -158,17 +276,23 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
158
276
|
});
|
|
159
277
|
|
|
160
278
|
const compiler = await createCompiler(validated.config);
|
|
279
|
+
|
|
280
|
+
if (devMode) {
|
|
281
|
+
sys.onProcessInterrupt(() => {
|
|
282
|
+
compiler.destroy();
|
|
283
|
+
});
|
|
284
|
+
return await compiler.createWatcher();
|
|
285
|
+
}
|
|
286
|
+
|
|
161
287
|
const buildResults = await compiler.build();
|
|
162
288
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await handleStencilBuildOutput(ctx);
|
|
169
|
-
}
|
|
170
|
-
await compiler.destroy();
|
|
289
|
+
if (buildResults.hasError) {
|
|
290
|
+
console.error("Stencil build error:", buildResults.diagnostics);
|
|
291
|
+
throw new Error("Stencil build error");
|
|
292
|
+
} else {
|
|
293
|
+
await handleStencilBuildOutput(ctx);
|
|
171
294
|
}
|
|
295
|
+
await compiler.destroy();
|
|
172
296
|
|
|
173
297
|
process.chdir(ctx.client.rootDir);
|
|
174
298
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWatcherLock,
|
|
3
|
+
preventContentLength,
|
|
4
|
+
waitUntilFileStable,
|
|
5
|
+
} from "./dev.utils";
|
|
6
|
+
import { ServerResponse } from "http";
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import {createReadStream} from "node:fs";
|
|
9
|
+
import { Readable } from "node:stream";
|
|
10
|
+
|
|
11
|
+
vi.mock("node:fs/promises", async () => {
|
|
12
|
+
let size = 100;
|
|
13
|
+
return {
|
|
14
|
+
stat: vi.fn().mockImplementation(() => ({ size })),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("node:fs", async () => ({
|
|
19
|
+
createReadStream: vi.fn()
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("preventContentLength", () => {
|
|
23
|
+
it("should not allow setting 'content-length' header", () => {
|
|
24
|
+
const headers: Record<string, string> = {};
|
|
25
|
+
const res = {
|
|
26
|
+
setHeader: (key: string, value: any) => {
|
|
27
|
+
headers[key.toLowerCase()] = value;
|
|
28
|
+
},
|
|
29
|
+
} as unknown as ServerResponse;
|
|
30
|
+
|
|
31
|
+
preventContentLength(res);
|
|
32
|
+
res.setHeader("content-length", "1234");
|
|
33
|
+
res.setHeader("x-custom-header", "abc");
|
|
34
|
+
|
|
35
|
+
expect(headers["content-length"]).toBeUndefined();
|
|
36
|
+
expect(headers["x-custom-header"]).toBe("abc");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("createWatcherLock", () => {
|
|
41
|
+
it("should block and unblock correctly", async () => {
|
|
42
|
+
const lock = createWatcherLock();
|
|
43
|
+
|
|
44
|
+
lock.lock();
|
|
45
|
+
let unlocked = false;
|
|
46
|
+
|
|
47
|
+
const waiter = lock.waitUntilFree().then(() => {
|
|
48
|
+
unlocked = true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Still locked
|
|
52
|
+
expect(unlocked).toBe(false);
|
|
53
|
+
|
|
54
|
+
lock.unlock();
|
|
55
|
+
|
|
56
|
+
// Wait for Promise resolution
|
|
57
|
+
await waiter;
|
|
58
|
+
expect(unlocked).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should resolve immediately if not locked", async () => {
|
|
62
|
+
const lock = createWatcherLock();
|
|
63
|
+
await expect(lock.waitUntilFree()).resolves.toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should not resolve until unlock is called", async () => {
|
|
67
|
+
const lock = createWatcherLock();
|
|
68
|
+
lock.lock();
|
|
69
|
+
|
|
70
|
+
let resolved = false;
|
|
71
|
+
lock.waitUntilFree().then(() => {
|
|
72
|
+
resolved = true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
76
|
+
expect(resolved).toBe(false);
|
|
77
|
+
|
|
78
|
+
lock.unlock();
|
|
79
|
+
await new Promise((r) => setTimeout(r, 0)); // allow promise to resolve
|
|
80
|
+
expect(resolved).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("waitUntilFileStable", () => {
|
|
85
|
+
it("should resolve when file becomes stable and has the expected tail", async () => {
|
|
86
|
+
const filePath = "mock/path.js";
|
|
87
|
+
const expectedTail = "sourceMappingURL";
|
|
88
|
+
let size = 10;
|
|
89
|
+
|
|
90
|
+
const mockStat = vi.fn().mockImplementation(() => {
|
|
91
|
+
return Promise.resolve({ size });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
vi.mocked(fs.stat).mockImplementation(mockStat);
|
|
95
|
+
|
|
96
|
+
const streamData = "some data\n// sourceMappingURL=something.js";
|
|
97
|
+
|
|
98
|
+
vi.mocked(createReadStream).mockImplementation(() => {
|
|
99
|
+
return Readable.from([streamData]) as any;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await expect(waitUntilFileStable(filePath, expectedTail, {
|
|
103
|
+
maxAttempts: 5,
|
|
104
|
+
})).resolves.toBeUndefined();
|
|
105
|
+
|
|
106
|
+
expect(fs.stat).toHaveBeenCalled();
|
|
107
|
+
expect(createReadStream).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should throw if file never stabilizes", async () => {
|
|
111
|
+
const filePath = "mock/path.js";
|
|
112
|
+
const expectedTail = "sourceMappingURL";
|
|
113
|
+
|
|
114
|
+
vi.mocked(fs.stat).mockResolvedValue({ size: 0 } as any);
|
|
115
|
+
|
|
116
|
+
vi.mocked(createReadStream).mockImplementation(() => {
|
|
117
|
+
return Readable.from([""]) as any;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(waitUntilFileStable(filePath, expectedTail, {
|
|
121
|
+
maxAttempts: 3,
|
|
122
|
+
})).rejects.toThrow("File did not stabilize");
|
|
123
|
+
|
|
124
|
+
expect(fs.stat).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
});
|