@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.
@@ -1,4 +1,8 @@
1
- import generate from "./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.fn(),
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: expect.any(Function),
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 { createCompiler, loadConfig } from "@stencil/core/compiler";
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
- await generateSourceMap(ctx, pluginName);
88
+ return watcher;
30
89
  };
31
90
 
32
- async function injectCSS(
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 importStr = `import render from '../../${ctx[pluginName].outputOptions.buildName}/${ctx[pluginName].outputOptions.fileName}';`;
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(ctx: ResolvedEmbeddableConfig): Promise<void> {
127
- const logger = ctx.dev?.logger || createNodeLogger();
128
- const sys = ctx.dev?.sys || createNodeSys({ process });
129
- const devMode = !!ctx.dev?.watch;
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: true, // always generate source maps in both dev and prod
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 (!devMode) {
164
- if (buildResults.hasError) {
165
- console.error("Stencil build error:", buildResults.diagnostics);
166
- throw new Error("Stencil build error");
167
- } else {
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
  }