@embeddable.com/sdk-core 4.2.0 → 4.3.0-next.1
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 +32 -1
- package/lib/index.esm.js +311 -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 +273 -3
- package/src/generate.ts +154 -26
- 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,8 @@
|
|
|
1
|
-
import generate
|
|
1
|
+
import generate, {
|
|
2
|
+
resetForTesting, TRIGGER_BUILD_ITERATION_LIMIT,
|
|
3
|
+
triggerWebComponentRebuild, generateDTS,
|
|
4
|
+
injectBundleRender, injectCSS,
|
|
5
|
+
} from "./generate";
|
|
2
6
|
import * as fs from "node:fs/promises";
|
|
3
7
|
import * as path from "node:path";
|
|
4
8
|
import { checkNodeVersion } from "./utils";
|
|
@@ -52,6 +56,10 @@ vi.mock("node:fs/promises", () => ({
|
|
|
52
56
|
rename: vi.fn(),
|
|
53
57
|
cp: vi.fn(),
|
|
54
58
|
rm: vi.fn(),
|
|
59
|
+
copyFile: vi.fn(),
|
|
60
|
+
stat: vi.fn(),
|
|
61
|
+
truncate: vi.fn(),
|
|
62
|
+
appendFile: vi.fn(),
|
|
55
63
|
}));
|
|
56
64
|
|
|
57
65
|
vi.mock("node:path", async () => {
|
|
@@ -60,6 +68,7 @@ vi.mock("node:path", async () => {
|
|
|
60
68
|
...actual,
|
|
61
69
|
resolve: vi.fn(),
|
|
62
70
|
join: vi.fn(),
|
|
71
|
+
relative: vi.fn(),
|
|
63
72
|
};
|
|
64
73
|
});
|
|
65
74
|
|
|
@@ -69,6 +78,10 @@ vi.mock("@stencil/core/compiler", () => ({
|
|
|
69
78
|
}));
|
|
70
79
|
|
|
71
80
|
describe("generate", () => {
|
|
81
|
+
const watcherMock = vi.fn().mockResolvedValue({
|
|
82
|
+
hasError: false,
|
|
83
|
+
on: vi.fn(),
|
|
84
|
+
});
|
|
72
85
|
beforeEach(() => {
|
|
73
86
|
vi.mocked(checkNodeVersion).mockResolvedValue(true);
|
|
74
87
|
vi.mocked(fs.readdir).mockResolvedValue([
|
|
@@ -77,6 +90,7 @@ describe("generate", () => {
|
|
|
77
90
|
"styles.css",
|
|
78
91
|
] as any);
|
|
79
92
|
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
93
|
+
vi.mocked(path.relative).mockReturnValue("../buildDir/buildName");
|
|
80
94
|
vi.mocked(fs.readFile).mockResolvedValue("");
|
|
81
95
|
vi.mocked(loadConfig).mockResolvedValue({
|
|
82
96
|
config: {},
|
|
@@ -86,6 +100,7 @@ describe("generate", () => {
|
|
|
86
100
|
hasError: false,
|
|
87
101
|
}),
|
|
88
102
|
destroy: vi.fn(),
|
|
103
|
+
createWatcher: watcherMock,
|
|
89
104
|
} as any);
|
|
90
105
|
|
|
91
106
|
vi.mocked(getContentHash).mockReturnValue("hash");
|
|
@@ -124,7 +139,9 @@ describe("generate", () => {
|
|
|
124
139
|
dev: {
|
|
125
140
|
watch: true,
|
|
126
141
|
logger: vi.fn(),
|
|
127
|
-
sys: vi.
|
|
142
|
+
sys: vi.mocked({
|
|
143
|
+
onProcessInterrupt: vi.fn(),
|
|
144
|
+
}),
|
|
128
145
|
},
|
|
129
146
|
};
|
|
130
147
|
|
|
@@ -134,6 +151,7 @@ describe("generate", () => {
|
|
|
134
151
|
await generate(ctx as unknown as ResolvedEmbeddableConfig, "sdk-react");
|
|
135
152
|
|
|
136
153
|
expect(createCompiler).toHaveBeenCalled();
|
|
154
|
+
expect(watcherMock).toHaveBeenCalled();
|
|
137
155
|
|
|
138
156
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
139
157
|
"componentDir/component.tsx",
|
|
@@ -144,6 +162,7 @@ describe("generate", () => {
|
|
|
144
162
|
config: {
|
|
145
163
|
configPath: "webComponentRoot/stencil.config.ts",
|
|
146
164
|
devMode: true,
|
|
165
|
+
watchIgnoredRegex: [/\.css$/, /\.d\.ts$/, /\.js$/],
|
|
147
166
|
maxConcurrentWorkers: process.platform === "win32" ? 0 : 8,
|
|
148
167
|
minifyCss: false,
|
|
149
168
|
minifyJs: false,
|
|
@@ -161,7 +180,258 @@ describe("generate", () => {
|
|
|
161
180
|
},
|
|
162
181
|
initTsConfig: true,
|
|
163
182
|
logger: expect.any(Function),
|
|
164
|
-
sys:
|
|
183
|
+
sys: {
|
|
184
|
+
onProcessInterrupt: expect.any(Function),
|
|
185
|
+
},
|
|
165
186
|
});
|
|
166
187
|
});
|
|
167
188
|
});
|
|
189
|
+
|
|
190
|
+
describe("triggerWebComponentRebuild", () => {
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
vi.clearAllMocks();
|
|
193
|
+
resetForTesting();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should store original file stats on first call and append file", async () => {
|
|
197
|
+
const mockStats = { size: 123 };
|
|
198
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
199
|
+
|
|
200
|
+
await triggerWebComponentRebuild(
|
|
201
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const filePath = path.resolve(config.client.componentDir, "component.tsx");
|
|
205
|
+
expect(fs.stat).toHaveBeenCalledWith(filePath);
|
|
206
|
+
expect(fs.appendFile).toHaveBeenCalledWith(filePath, " ");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should append file and not call stat after first build", async () => {
|
|
210
|
+
const mockStats = { size: 123 };
|
|
211
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < 3; i++) {
|
|
214
|
+
await triggerWebComponentRebuild(
|
|
215
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
expect(fs.stat).toHaveBeenCalledTimes(1); // only once
|
|
220
|
+
expect(fs.appendFile).toHaveBeenCalledTimes(3);
|
|
221
|
+
expect(fs.truncate).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should reset file using truncate on the 6th call and reset count", async () => {
|
|
225
|
+
const mockStats = { size: 321 };
|
|
226
|
+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
|
|
227
|
+
vi.mocked(path.resolve).mockReturnValue("componentDir/component.tsx");
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < TRIGGER_BUILD_ITERATION_LIMIT; i++) {
|
|
230
|
+
await triggerWebComponentRebuild(
|
|
231
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
expect(fs.truncate).not.toHaveBeenCalled();
|
|
236
|
+
|
|
237
|
+
vi.mocked(fs.appendFile).mockClear();
|
|
238
|
+
vi.mocked(fs.truncate).mockClear();
|
|
239
|
+
|
|
240
|
+
// now truncate should be called
|
|
241
|
+
await triggerWebComponentRebuild(
|
|
242
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
243
|
+
);
|
|
244
|
+
const filePath = path.resolve(config.client.componentDir, "component.tsx");
|
|
245
|
+
|
|
246
|
+
expect(fs.truncate).toHaveBeenCalledWith(filePath, mockStats.size);
|
|
247
|
+
expect(fs.appendFile).not.toHaveBeenCalledWith(filePath, " ");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("generateDTS", () => {
|
|
252
|
+
beforeEach(() => {
|
|
253
|
+
vi.mocked(fs.readdir).mockResolvedValue([
|
|
254
|
+
"embeddable-wrapper.esm.js",
|
|
255
|
+
] as any);
|
|
256
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
257
|
+
// Template contains both tokens so we can verify replacement
|
|
258
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
259
|
+
"replace-this-with-component-name {{RENDER_IMPORT}}",
|
|
260
|
+
);
|
|
261
|
+
vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
|
|
262
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
263
|
+
build: vi.fn().mockResolvedValue({ hasError: false }),
|
|
264
|
+
destroy: vi.fn(),
|
|
265
|
+
createWatcher: vi.fn(),
|
|
266
|
+
} as any);
|
|
267
|
+
vi.mocked(findFiles).mockResolvedValue([["", ""]]);
|
|
268
|
+
Object.defineProperties(process, { chdir: { value: vi.fn() } });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should write an empty style.css", async () => {
|
|
272
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
273
|
+
|
|
274
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
275
|
+
"componentDir/style.css",
|
|
276
|
+
"",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should write component.tsx with stub render and embeddable-component tag", async () => {
|
|
281
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
282
|
+
|
|
283
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
284
|
+
"componentDir/component.tsx",
|
|
285
|
+
expect.stringContaining("embeddable-component"),
|
|
286
|
+
);
|
|
287
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
288
|
+
"componentDir/component.tsx",
|
|
289
|
+
expect.stringContaining("const render = () => {};"),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should call loadConfig with devMode=false and sourceMap=false", async () => {
|
|
294
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
295
|
+
|
|
296
|
+
expect(loadConfig).toHaveBeenCalledWith(
|
|
297
|
+
expect.objectContaining({
|
|
298
|
+
config: expect.objectContaining({
|
|
299
|
+
devMode: false,
|
|
300
|
+
sourceMap: false,
|
|
301
|
+
minifyJs: false,
|
|
302
|
+
minifyCss: false,
|
|
303
|
+
}),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should not create a watcher (not watch mode)", async () => {
|
|
309
|
+
const createWatcherMock = vi.fn();
|
|
310
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
311
|
+
build: vi.fn().mockResolvedValue({ hasError: false }),
|
|
312
|
+
destroy: vi.fn(),
|
|
313
|
+
createWatcher: createWatcherMock,
|
|
314
|
+
} as any);
|
|
315
|
+
|
|
316
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
317
|
+
|
|
318
|
+
expect(createWatcherMock).not.toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("injectBundleRender cross-platform paths", () => {
|
|
323
|
+
const ctxWithFileName = {
|
|
324
|
+
...config,
|
|
325
|
+
"sdk-react": {
|
|
326
|
+
...config["sdk-react"],
|
|
327
|
+
outputOptions: {
|
|
328
|
+
buildName: "buildName",
|
|
329
|
+
fileName: "render.js",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
336
|
+
vi.mocked(fs.readFile).mockResolvedValue("{{RENDER_IMPORT}}");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should use forward slashes in import when path.relative returns unix path", async () => {
|
|
340
|
+
vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
|
|
341
|
+
|
|
342
|
+
await injectBundleRender(
|
|
343
|
+
ctxWithFileName as unknown as ResolvedEmbeddableConfig,
|
|
344
|
+
"sdk-react",
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
348
|
+
expect.any(String),
|
|
349
|
+
expect.stringContaining("import render from '../../buildDir/buildName/render.js'"),
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should replace backslashes with forward slashes when path.relative returns windows path", async () => {
|
|
354
|
+
vi.mocked(path.relative).mockReturnValue("..\\..\\buildDir\\buildName");
|
|
355
|
+
|
|
356
|
+
await injectBundleRender(
|
|
357
|
+
ctxWithFileName as unknown as ResolvedEmbeddableConfig,
|
|
358
|
+
"sdk-react",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
362
|
+
expect.any(String),
|
|
363
|
+
expect.stringContaining("import render from '../../buildDir/buildName/render.js'"),
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("injectCSS cross-platform paths", () => {
|
|
369
|
+
beforeEach(() => {
|
|
370
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
371
|
+
vi.mocked(fs.readFile).mockResolvedValue("{{STYLES_IMPORT}}");
|
|
372
|
+
vi.mocked(fs.readdir).mockResolvedValue(["main.css"] as any);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should use forward slashes in @import when path.relative returns unix path", async () => {
|
|
376
|
+
vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
|
|
377
|
+
|
|
378
|
+
await injectCSS(
|
|
379
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
380
|
+
"sdk-react",
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
384
|
+
expect.any(String),
|
|
385
|
+
expect.stringContaining("@import '../../buildDir/buildName/main.css'"),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should replace backslashes with forward slashes when path.relative returns windows path", async () => {
|
|
390
|
+
vi.mocked(path.relative).mockReturnValue("..\\..\\buildDir\\buildName");
|
|
391
|
+
|
|
392
|
+
await injectCSS(
|
|
393
|
+
config as unknown as ResolvedEmbeddableConfig,
|
|
394
|
+
"sdk-react",
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
398
|
+
expect.any(String),
|
|
399
|
+
expect.stringContaining("@import '../../buildDir/buildName/main.css'"),
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("generate stencil build error", () => {
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
407
|
+
vi.mocked(path.relative).mockReturnValue("../buildDir/buildName");
|
|
408
|
+
vi.mocked(fs.readFile).mockResolvedValue("");
|
|
409
|
+
vi.mocked(fs.readdir).mockResolvedValue([] as any);
|
|
410
|
+
vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
|
|
411
|
+
vi.mocked(findFiles).mockResolvedValue([["", ""]]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should throw when Stencil build has errors", async () => {
|
|
415
|
+
vi.mocked(createCompiler).mockResolvedValue({
|
|
416
|
+
build: vi.fn().mockResolvedValue({
|
|
417
|
+
hasError: true,
|
|
418
|
+
diagnostics: [{ messageText: "type error" }],
|
|
419
|
+
}),
|
|
420
|
+
destroy: vi.fn(),
|
|
421
|
+
createWatcher: vi.fn(),
|
|
422
|
+
} as any);
|
|
423
|
+
|
|
424
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
425
|
+
|
|
426
|
+
await expect(
|
|
427
|
+
generate(config as unknown as ResolvedEmbeddableConfig, "sdk-react"),
|
|
428
|
+
).rejects.toThrow("Stencil build error");
|
|
429
|
+
|
|
430
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
431
|
+
"Stencil build error:",
|
|
432
|
+
expect.anything(),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
consoleSpy.mockRestore();
|
|
436
|
+
});
|
|
437
|
+
});
|
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,20 +22,93 @@ 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
|
|
|
32
|
-
|
|
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
|
+
|
|
111
|
+
export async function injectCSS(
|
|
33
112
|
ctx: ResolvedEmbeddableConfig,
|
|
34
113
|
pluginName: PluginName,
|
|
35
114
|
) {
|
|
@@ -39,12 +118,16 @@ async function injectCSS(
|
|
|
39
118
|
);
|
|
40
119
|
const allFiles = await fs.readdir(CUSTOMER_BUILD);
|
|
41
120
|
|
|
121
|
+
const importFilePath = path
|
|
122
|
+
.relative(
|
|
123
|
+
ctx.client.componentDir,
|
|
124
|
+
path.resolve(ctx.client.buildDir, ctx[pluginName].outputOptions.buildName),
|
|
125
|
+
)
|
|
126
|
+
.replaceAll("\\", "/");
|
|
127
|
+
|
|
42
128
|
const imports = allFiles
|
|
43
129
|
.filter((fileName) => fileName.endsWith(".css"))
|
|
44
|
-
.map(
|
|
45
|
-
(fileName) =>
|
|
46
|
-
`@import '../../${ctx[pluginName].outputOptions.buildName}/${fileName}';`,
|
|
47
|
-
);
|
|
130
|
+
.map((fileName) => `@import '${importFilePath}/${fileName}';`);
|
|
48
131
|
|
|
49
132
|
const componentLibraries = ctx.client.componentLibraries;
|
|
50
133
|
for (const componentLibrary of componentLibraries) {
|
|
@@ -72,11 +155,17 @@ async function injectCSS(
|
|
|
72
155
|
);
|
|
73
156
|
}
|
|
74
157
|
|
|
75
|
-
async function injectBundleRender(
|
|
158
|
+
export async function injectBundleRender(
|
|
76
159
|
ctx: ResolvedEmbeddableConfig,
|
|
77
160
|
pluginName: PluginName,
|
|
78
161
|
) {
|
|
79
|
-
const
|
|
162
|
+
const importFilePath = path
|
|
163
|
+
.relative(
|
|
164
|
+
ctx.client.componentDir,
|
|
165
|
+
path.resolve(ctx.client.buildDir, ctx[pluginName].outputOptions.buildName),
|
|
166
|
+
)
|
|
167
|
+
.replaceAll("\\", "/");
|
|
168
|
+
const importStr = `import render from '${importFilePath}/${ctx[pluginName].outputOptions.fileName}';`;
|
|
80
169
|
|
|
81
170
|
let content = await fs.readFile(
|
|
82
171
|
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
@@ -93,6 +182,27 @@ async function injectBundleRender(
|
|
|
93
182
|
);
|
|
94
183
|
}
|
|
95
184
|
|
|
185
|
+
async function injectEmptyCSS(ctx: ResolvedEmbeddableConfig) {
|
|
186
|
+
await fs.writeFile(path.resolve(ctx.client.componentDir, "style.css"), "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function injectBundleRenderStub(
|
|
190
|
+
ctx: ResolvedEmbeddableConfig,
|
|
191
|
+
) {
|
|
192
|
+
const stubStr = `const render = () => {};`;
|
|
193
|
+
|
|
194
|
+
let content = await fs.readFile(
|
|
195
|
+
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
196
|
+
"utf8",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component");
|
|
200
|
+
await fs.writeFile(
|
|
201
|
+
path.resolve(ctx.client.componentDir, "component.tsx"),
|
|
202
|
+
content.replace(RENDER_IMPORT_TOKEN, stubStr),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
96
206
|
async function addComponentTagName(filePath: string, bundleHash: string) {
|
|
97
207
|
// find entry file with a name *.entry.js
|
|
98
208
|
const entryFiles = await findFiles(path.dirname(filePath), /.*\.entry\.js/);
|
|
@@ -123,10 +233,20 @@ async function addComponentTagName(filePath: string, bundleHash: string) {
|
|
|
123
233
|
]);
|
|
124
234
|
}
|
|
125
235
|
|
|
126
|
-
async function runStencil(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
236
|
+
async function runStencil(
|
|
237
|
+
ctx: ResolvedEmbeddableConfig,
|
|
238
|
+
options?: { dtsOnly?: boolean },
|
|
239
|
+
): Promise<void | CompilerWatcher> {
|
|
240
|
+
const logger = (options?.dtsOnly ? createNodeLogger() : ctx.dev?.logger || createNodeLogger()) as Logger;
|
|
241
|
+
const sys = options?.dtsOnly ? createNodeSys({ process }) : (ctx.dev?.sys || createNodeSys({ process }));
|
|
242
|
+
const devMode = !!ctx.dev?.watch && !options?.dtsOnly;
|
|
243
|
+
if (options?.dtsOnly) {
|
|
244
|
+
logger.setLevel("error")
|
|
245
|
+
logger.createTimeSpan = () => ({
|
|
246
|
+
duration: () => 0,
|
|
247
|
+
finish: () => 0,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
130
250
|
|
|
131
251
|
const isWindows = process.platform === "win32";
|
|
132
252
|
|
|
@@ -137,6 +257,8 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
137
257
|
config: {
|
|
138
258
|
devMode,
|
|
139
259
|
maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows
|
|
260
|
+
// we will trigger a rebuild by updating the component.tsx file (see triggerBuild function)
|
|
261
|
+
watchIgnoredRegex: [/\.css$/, /\.d\.ts$/, /\.js$/],
|
|
140
262
|
rootDir: ctx.client.webComponentRoot,
|
|
141
263
|
configPath: path.resolve(
|
|
142
264
|
ctx.client.webComponentRoot,
|
|
@@ -145,9 +267,9 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
145
267
|
tsconfig: path.resolve(ctx.client.webComponentRoot, "tsconfig.json"),
|
|
146
268
|
namespace: "embeddable-wrapper",
|
|
147
269
|
srcDir: ctx.client.componentDir,
|
|
148
|
-
sourceMap:
|
|
149
|
-
minifyJs: !devMode,
|
|
150
|
-
minifyCss: !devMode,
|
|
270
|
+
sourceMap: !options?.dtsOnly, // always generate source maps in both dev and prod
|
|
271
|
+
minifyJs: !devMode && !options?.dtsOnly,
|
|
272
|
+
minifyCss: !devMode && !options?.dtsOnly,
|
|
151
273
|
outputTargets: [
|
|
152
274
|
{
|
|
153
275
|
type: "dist",
|
|
@@ -158,17 +280,23 @@ async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
|
|
|
158
280
|
});
|
|
159
281
|
|
|
160
282
|
const compiler = await createCompiler(validated.config);
|
|
283
|
+
|
|
284
|
+
if (devMode) {
|
|
285
|
+
sys.onProcessInterrupt(() => {
|
|
286
|
+
compiler.destroy();
|
|
287
|
+
});
|
|
288
|
+
return await compiler.createWatcher();
|
|
289
|
+
}
|
|
290
|
+
|
|
161
291
|
const buildResults = await compiler.build();
|
|
162
292
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await handleStencilBuildOutput(ctx);
|
|
169
|
-
}
|
|
170
|
-
await compiler.destroy();
|
|
293
|
+
if (buildResults.hasError) {
|
|
294
|
+
console.error("Stencil build error:", buildResults.diagnostics);
|
|
295
|
+
throw new Error("Stencil build error");
|
|
296
|
+
} else {
|
|
297
|
+
await handleStencilBuildOutput(ctx);
|
|
171
298
|
}
|
|
299
|
+
await compiler.destroy();
|
|
172
300
|
|
|
173
301
|
process.chdir(ctx.client.rootDir);
|
|
174
302
|
}
|