@noego/wood 0.4.0 → 0.4.2

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.
Files changed (85) hide show
  1. package/README.md +100 -2
  2. package/dist/client/global_polyfill.d.cts +1 -2
  3. package/dist/client/global_polyfill.d.ts +1 -2
  4. package/dist/client/index.cjs.map +1 -1
  5. package/dist/client/index.d.cts +1 -1
  6. package/dist/client/index.d.ts +1 -1
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/index.cjs +0 -4
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +0 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/main/index.cjs +16 -2
  15. package/dist/main/index.cjs.map +1 -1
  16. package/dist/main/index.js +16 -2
  17. package/dist/main/index.js.map +1 -1
  18. package/dist/runtime/runtime.cjs +4 -4
  19. package/dist/runtime/runtime.cjs.map +1 -1
  20. package/dist/runtime/runtime.d.cts +2 -1
  21. package/dist/runtime/runtime.d.ts +2 -1
  22. package/dist/runtime/runtime.js +8 -5
  23. package/dist/runtime/runtime.js.map +1 -1
  24. package/dist/testing/browser.cjs +224 -32
  25. package/dist/testing/browser.cjs.map +1 -1
  26. package/dist/testing/browser.d.cts +18 -1
  27. package/dist/testing/browser.d.ts +18 -1
  28. package/dist/testing/browser.js +226 -32
  29. package/dist/testing/browser.js.map +1 -1
  30. package/dist/testing/create_controller_harness.cjs +3 -1
  31. package/dist/testing/create_controller_harness.cjs.map +1 -1
  32. package/dist/testing/create_controller_harness.d.cts +4 -2
  33. package/dist/testing/create_controller_harness.d.ts +4 -2
  34. package/dist/testing/create_controller_harness.js +3 -1
  35. package/dist/testing/create_controller_harness.js.map +1 -1
  36. package/dist/testing/index.cjs +3 -3
  37. package/dist/testing/index.cjs.map +1 -1
  38. package/dist/testing/index.d.cts +6 -4
  39. package/dist/testing/index.d.ts +6 -4
  40. package/dist/testing/index.js +2 -2
  41. package/dist/testing/index.js.map +1 -1
  42. package/dist/trace-testing/index.cjs +39 -0
  43. package/dist/trace-testing/index.cjs.map +1 -0
  44. package/dist/trace-testing/index.d.cts +6 -0
  45. package/dist/trace-testing/index.d.ts +6 -0
  46. package/dist/trace-testing/index.js +19 -0
  47. package/dist/trace-testing/index.js.map +1 -0
  48. package/dist/trace-testing/trace_assertions.cjs +29 -0
  49. package/dist/trace-testing/trace_assertions.cjs.map +1 -0
  50. package/dist/trace-testing/trace_assertions.d.cts +1 -0
  51. package/dist/trace-testing/trace_assertions.d.ts +1 -0
  52. package/dist/trace-testing/trace_assertions.js +7 -0
  53. package/dist/trace-testing/trace_assertions.js.map +1 -0
  54. package/dist/trace-testing/trace_query.cjs +33 -0
  55. package/dist/trace-testing/trace_query.cjs.map +1 -0
  56. package/dist/trace-testing/trace_query.d.cts +1 -0
  57. package/dist/trace-testing/trace_query.d.ts +1 -0
  58. package/dist/trace-testing/trace_query.js +11 -0
  59. package/dist/trace-testing/trace_query.js.map +1 -0
  60. package/dist/trace-testing/trace_recorder.cjs +50 -0
  61. package/dist/trace-testing/trace_recorder.cjs.map +1 -0
  62. package/dist/trace-testing/trace_recorder.d.cts +12 -0
  63. package/dist/trace-testing/trace_recorder.d.ts +12 -0
  64. package/dist/trace-testing/trace_recorder.js +32 -0
  65. package/dist/trace-testing/trace_recorder.js.map +1 -0
  66. package/dist/tracing/index.cjs +0 -4
  67. package/dist/tracing/index.cjs.map +1 -1
  68. package/dist/tracing/index.d.cts +1 -1
  69. package/dist/tracing/index.d.ts +1 -1
  70. package/dist/tracing/index.js +1 -5
  71. package/dist/tracing/index.js.map +1 -1
  72. package/dist/tracing/trace_hub.cjs +19 -86
  73. package/dist/tracing/trace_hub.cjs.map +1 -1
  74. package/dist/tracing/trace_hub.d.cts +11 -16
  75. package/dist/tracing/trace_hub.d.ts +11 -16
  76. package/dist/tracing/trace_hub.js +19 -74
  77. package/dist/tracing/trace_hub.js.map +1 -1
  78. package/dist/tracing/trace_provider.cjs +0 -6
  79. package/dist/tracing/trace_provider.cjs.map +1 -1
  80. package/dist/tracing/trace_provider.d.cts +1 -3
  81. package/dist/tracing/trace_provider.d.ts +1 -3
  82. package/dist/tracing/trace_provider.js +0 -7
  83. package/dist/tracing/trace_provider.js.map +1 -1
  84. package/docs/browser-testing.md +84 -0
  85. package/package.json +8 -1
@@ -2,10 +2,18 @@ import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import {
6
+ dispatchRendererTracePayload
7
+ } from "../tracing/index.js";
8
+ import { TraceRecorder } from "../trace-testing/index.js";
5
9
  const DEFAULT_VIEWPORT = {
6
10
  width: 1280,
7
11
  height: 800
8
12
  };
13
+ const DEFAULT_EXPECT_TIMEOUT_MS = 1e4;
14
+ const DEFAULT_RESOURCE_CLOSE_TIMEOUT_MS = 5e3;
15
+ const WORKER_RUNTIME_IDLE_CLOSE_MS = 5e3;
16
+ const browserWorkerRuntimes = /* @__PURE__ */ new Map();
9
17
  function normalizeRoot(rootDir) {
10
18
  return path.resolve(rootDir ?? process.cwd());
11
19
  }
@@ -28,6 +36,18 @@ function mergeBridge(base, override) {
28
36
  record: override?.record ?? base?.record
29
37
  };
30
38
  }
39
+ async function waitForBoundedClose(promise, timeoutMs = DEFAULT_RESOURCE_CLOSE_TIMEOUT_MS) {
40
+ let timeout;
41
+ await Promise.race([
42
+ promise.catch(() => void 0),
43
+ new Promise((resolve) => {
44
+ timeout = setTimeout(resolve, timeoutMs);
45
+ })
46
+ ]);
47
+ if (timeout) {
48
+ clearTimeout(timeout);
49
+ }
50
+ }
31
51
  function channelToManifestEntry(channel, handler) {
32
52
  const pathSegments = channel.split(".").filter(Boolean);
33
53
  const action = pathSegments.length > 0 ? pathSegments[pathSegments.length - 1] : channel;
@@ -58,24 +78,32 @@ function createBridgeState(input) {
58
78
  };
59
79
  }
60
80
  function createBrowserBridgeFixture(input) {
81
+ return createBrowserBridgeFixtureWithTrace(input, void 0);
82
+ }
83
+ function createBrowserBridgeFixtureWithTrace(input, traceConfig) {
61
84
  const state = createBridgeState(input);
62
85
  const manifest = Array.from(state.operations.entries()).map(([channel, handler]) => channelToManifestEntry(channel, handler));
63
86
  return {
64
87
  state,
65
88
  manifest,
66
- bridgeInit: buildBridgeInitScript(manifest)
89
+ bridgeInit: buildBridgeInitScript(manifest, traceConfig)
67
90
  };
68
91
  }
69
- function buildBridgeInitScript(manifest) {
92
+ function buildBridgeInitScript(manifest, traceConfig) {
93
+ const traceEnabled = traceConfig?.enabled === true;
94
+ const traceMinLevel = traceConfig?.minLevel ?? "debug";
70
95
  return `
71
96
  (() => {
72
97
  const manifest = ${JSON.stringify(manifest)};
73
98
  const listeners = new Map();
74
99
  const bridge = {
75
100
  __rpcManifest: manifest,
76
- __woodConfig: async () => ({ rendererTraceEnabled: false }),
101
+ __woodConfig: async () => ({
102
+ rendererTraceEnabled: ${JSON.stringify(traceEnabled)},
103
+ rendererTraceMinLevel: ${JSON.stringify(traceMinLevel)}
104
+ }),
77
105
  __log: () => {},
78
- __trace: () => {},
106
+ __trace: (payload) => window.__woodBrowserTrace?.(payload),
79
107
  __window: {
80
108
  current: async () => ({ ok: true, windowId: 'wood-browser-harness', defaultRoute: window.__WOOD_BROWSER_DEFAULT_ROUTE__ }),
81
109
  open: async () => ({}),
@@ -151,12 +179,13 @@ async function loadViews(viewsPath) {
151
179
  const parser = await import("../parser/views_parser.js");
152
180
  return parser.parseViewsFile(viewsPath);
153
181
  }
154
- async function createViteServer(rootDir) {
182
+ async function createViteServer(rootDir, cacheDir) {
155
183
  const vite = await loadVite();
156
184
  const svelte = await loadSveltePlugin(rootDir);
157
185
  const plugins = svelte ? [svelte()] : [];
158
186
  const server = await vite.createServer({
159
187
  root: rootDir,
188
+ cacheDir,
160
189
  logLevel: process.env.WOOD_BROWSER_TEST_DEBUG ? "info" : "silent",
161
190
  plugins,
162
191
  resolve: {
@@ -167,6 +196,92 @@ async function createViteServer(rootDir) {
167
196
  await server.listen();
168
197
  return server;
169
198
  }
199
+ function workerRuntimeKey(input) {
200
+ return [
201
+ input.rootDir,
202
+ input.browserName,
203
+ input.headless ? "headless" : "headed"
204
+ ].join("::");
205
+ }
206
+ async function createBrowserWorkerRuntime(input) {
207
+ const cacheRoot = fs.mkdtempSync(path.join(input.rootDir, ".wood-browser-vite-"));
208
+ let browser2 = null;
209
+ try {
210
+ const playwrightBrowser = await loadPlaywright(input.browserName);
211
+ browser2 = await playwrightBrowser.launch({ headless: input.headless });
212
+ const server = await createViteServer(input.rootDir, path.join(cacheRoot, ".vite"));
213
+ return {
214
+ browser: browser2,
215
+ server,
216
+ cacheDir: cacheRoot,
217
+ rootDir: input.rootDir,
218
+ key: input.key,
219
+ activeHarnesses: 0,
220
+ closing: false,
221
+ idleCloseTimer: null
222
+ };
223
+ } catch (error) {
224
+ await browser2?.close().catch(() => void 0);
225
+ fs.rmSync(cacheRoot, { recursive: true, force: true });
226
+ throw error;
227
+ }
228
+ }
229
+ async function acquireBrowserWorkerRuntime(input) {
230
+ const key = workerRuntimeKey(input);
231
+ let entry = browserWorkerRuntimes.get(key);
232
+ if (!entry) {
233
+ entry = {
234
+ promise: createBrowserWorkerRuntime({ ...input, key })
235
+ };
236
+ browserWorkerRuntimes.set(key, entry);
237
+ }
238
+ const runtime = await entry.promise;
239
+ if (runtime.closing) {
240
+ browserWorkerRuntimes.delete(key);
241
+ return await acquireBrowserWorkerRuntime(input);
242
+ }
243
+ if (runtime.idleCloseTimer) {
244
+ clearTimeout(runtime.idleCloseTimer);
245
+ runtime.idleCloseTimer = null;
246
+ }
247
+ runtime.activeHarnesses += 1;
248
+ return runtime;
249
+ }
250
+ async function closeBrowserWorkerRuntime(runtime) {
251
+ if (runtime.closing) {
252
+ return;
253
+ }
254
+ runtime.closing = true;
255
+ try {
256
+ await Promise.all([
257
+ waitForBoundedClose(runtime.server.close()),
258
+ waitForBoundedClose(runtime.browser.close())
259
+ ]);
260
+ } finally {
261
+ fs.rmSync(runtime.cacheDir, { recursive: true, force: true });
262
+ const entry = browserWorkerRuntimes.get(runtime.key);
263
+ const current = entry ? await entry.promise.catch(() => null) : null;
264
+ if (current === runtime) {
265
+ browserWorkerRuntimes.delete(runtime.key);
266
+ }
267
+ }
268
+ }
269
+ function scheduleBrowserWorkerRuntimeClose(runtime) {
270
+ if (runtime.closing || runtime.activeHarnesses > 0 || runtime.idleCloseTimer) {
271
+ return;
272
+ }
273
+ runtime.idleCloseTimer = setTimeout(() => {
274
+ runtime.idleCloseTimer = null;
275
+ if (runtime.activeHarnesses === 0) {
276
+ void closeBrowserWorkerRuntime(runtime);
277
+ }
278
+ }, WORKER_RUNTIME_IDLE_CLOSE_MS);
279
+ runtime.idleCloseTimer.unref?.();
280
+ }
281
+ function releaseBrowserWorkerRuntime(runtime) {
282
+ runtime.activeHarnesses = Math.max(0, runtime.activeHarnesses - 1);
283
+ scheduleBrowserWorkerRuntimeClose(runtime);
284
+ }
170
285
  function serverBaseUrl(server) {
171
286
  return server.resolvedUrls?.local?.[0] ?? "http://127.0.0.1:5173";
172
287
  }
@@ -373,6 +488,14 @@ async function installBridge(page, bridgeState, manifest, bridgeInit) {
373
488
  window.__WOOD_BROWSER_BRIDGE_MANIFEST__ = entries;
374
489
  }, manifest);
375
490
  }
491
+ async function installTraceBinding(page, traceRecorder, context) {
492
+ if (!traceRecorder) {
493
+ return;
494
+ }
495
+ await page.exposeBinding("__woodBrowserTrace", async (_source, payload) => {
496
+ dispatchRendererTracePayload(payload, context);
497
+ });
498
+ }
376
499
  function createMountedBridge(page, state) {
377
500
  return {
378
501
  async emit(channel, payload) {
@@ -449,20 +572,35 @@ async function elementRect(page, selector) {
449
572
  }).catch(() => null);
450
573
  }
451
574
  function createExpect(page, outputDir) {
575
+ async function waitForVisible(selector) {
576
+ const locator = page.locator(selector).first();
577
+ try {
578
+ await locator.waitFor({ state: "attached", timeout: DEFAULT_EXPECT_TIMEOUT_MS });
579
+ } catch {
580
+ throw new Error(`Expected "${selector}" to exist.`);
581
+ }
582
+ try {
583
+ await locator.waitFor({ state: "visible", timeout: DEFAULT_EXPECT_TIMEOUT_MS });
584
+ } catch {
585
+ throw new Error(`Expected "${selector}" to be visible.`);
586
+ }
587
+ }
452
588
  return {
453
589
  async visible(selector) {
454
- const count = await page.locator(selector).count();
455
- if (count < 1) throw new Error(`Expected "${selector}" to exist.`);
456
- const visible = await page.locator(selector).first().isVisible();
457
- if (!visible) throw new Error(`Expected "${selector}" to be visible.`);
590
+ await waitForVisible(selector);
458
591
  },
459
592
  async hidden(selector) {
460
- const count = await page.locator(selector).count();
461
- if (count < 1) return;
462
- const visible = await page.locator(selector).first().isVisible();
463
- if (visible) throw new Error(`Expected "${selector}" to be hidden.`);
593
+ try {
594
+ await page.locator(selector).first().waitFor({
595
+ state: "hidden",
596
+ timeout: DEFAULT_EXPECT_TIMEOUT_MS
597
+ });
598
+ } catch {
599
+ throw new Error(`Expected "${selector}" to be hidden.`);
600
+ }
464
601
  },
465
602
  async insideViewport(selector, options = {}) {
603
+ await waitForVisible(selector);
466
604
  const margin = options.margin ?? 0;
467
605
  const rect = await elementRect(page, selector);
468
606
  if (!rect) throw new Error(`Expected "${selector}" to exist.`);
@@ -473,6 +611,7 @@ function createExpect(page, outputDir) {
473
611
  }
474
612
  },
475
613
  async notClipped(selector) {
614
+ await waitForVisible(selector);
476
615
  const result = await page.locator(selector).first().evaluate((element) => {
477
616
  const rect = element.getBoundingClientRect();
478
617
  let parent = element.parentElement;
@@ -494,6 +633,8 @@ function createExpect(page, outputDir) {
494
633
  }
495
634
  },
496
635
  async notOverlapping(firstSelector, secondSelector) {
636
+ await waitForVisible(firstSelector);
637
+ await waitForVisible(secondSelector);
497
638
  const first = await elementRect(page, firstSelector);
498
639
  const second = await elementRect(page, secondSelector);
499
640
  if (!first || !second) throw new Error(`Expected both "${firstSelector}" and "${secondSelector}" to exist.`);
@@ -503,6 +644,8 @@ function createExpect(page, outputDir) {
503
644
  }
504
645
  },
505
646
  async zIndexAbove(topSelector, lowerSelector) {
647
+ await waitForVisible(topSelector);
648
+ await waitForVisible(lowerSelector);
506
649
  const values = await page.evaluate((payload) => {
507
650
  const { topSelector: top, lowerSelector: lower } = payload;
508
651
  const topElement = document.querySelector(top);
@@ -576,6 +719,7 @@ async function openMountPage(input) {
576
719
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
577
720
  const entryPath = writeEntry(input.runtime.tempDir, input.name, input.entrySource);
578
721
  const htmlPath = writeHtml(input.runtime.tempDir, input.name, entryPath, input.css);
722
+ await installTraceBinding(input.runtime.page, input.traceRecorder, input.traceContext ?? {});
579
723
  await installBridge(page, input.bridge.state, input.bridge.manifest, input.bridge.bridgeInit);
580
724
  const mountedBridge = createMountedBridge(page, input.bridge.state);
581
725
  const context = {
@@ -608,22 +752,47 @@ async function openMountPage(input) {
608
752
  async function createHarness(config = {}) {
609
753
  const rootDir = normalizeRoot(config.rootDir);
610
754
  const browserName = config.browser?.browserName ?? "chromium";
611
- const playwrightBrowser = await loadPlaywright(browserName);
612
- const launchedBrowser = await playwrightBrowser.launch({
613
- headless: config.browser?.headless ?? true
755
+ const headless = config.browser?.headless ?? true;
756
+ const workerRuntime = await acquireBrowserWorkerRuntime({
757
+ rootDir,
758
+ browserName,
759
+ headless
614
760
  });
615
- const server = await createViteServer(rootDir);
616
- const page = await launchedBrowser.newPage({
617
- viewport: {
618
- width: config.viewport?.width ?? DEFAULT_VIEWPORT.width,
619
- height: config.viewport?.height ?? DEFAULT_VIEWPORT.height,
620
- deviceScaleFactor: config.viewport?.deviceScaleFactor
761
+ let released = false;
762
+ let destroyed = false;
763
+ let tempDir = null;
764
+ let context = null;
765
+ let page = null;
766
+ let traceRecorder;
767
+ function releaseRuntimeOnce() {
768
+ if (released) {
769
+ return;
621
770
  }
622
- });
623
- const tempDir = fs.mkdtempSync(path.join(rootDir, ".wood-browser-harness-"));
771
+ released = true;
772
+ releaseBrowserWorkerRuntime(workerRuntime);
773
+ }
774
+ try {
775
+ tempDir = fs.mkdtempSync(path.join(rootDir, ".wood-browser-harness-"));
776
+ context = await workerRuntime.browser.newContext({
777
+ viewport: {
778
+ width: config.viewport?.width ?? DEFAULT_VIEWPORT.width,
779
+ height: config.viewport?.height ?? DEFAULT_VIEWPORT.height,
780
+ deviceScaleFactor: config.viewport?.deviceScaleFactor
781
+ }
782
+ });
783
+ page = await context.newPage();
784
+ traceRecorder = config.trace?.enabled === true ? new TraceRecorder() : void 0;
785
+ traceRecorder?.enable({ maxEvents: config.trace?.maxEvents });
786
+ } catch (error) {
787
+ if (page) await waitForBoundedClose(page.close());
788
+ if (context) await waitForBoundedClose(context.close());
789
+ if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
790
+ releaseRuntimeOnce();
791
+ throw error;
792
+ }
624
793
  const runtime = {
625
- browser: launchedBrowser,
626
- server,
794
+ context,
795
+ server: workerRuntime.server,
627
796
  page,
628
797
  tempDir,
629
798
  rootDir
@@ -635,8 +804,9 @@ async function createHarness(config = {}) {
635
804
  });
636
805
  return {
637
806
  page,
807
+ ...traceRecorder ? { traceRecorder } : {},
638
808
  async mountRoute(routeKey, options = {}) {
639
- const bridge = createBrowserBridgeFixture(mergeBridge(config.bridge, options.bridge));
809
+ const bridge = createBrowserBridgeFixtureWithTrace(mergeBridge(config.bridge, options.bridge), config.trace);
640
810
  const route = {
641
811
  params: options.route?.params ?? {},
642
812
  query: options.route?.query ?? {}
@@ -667,6 +837,12 @@ async function createHarness(config = {}) {
667
837
  css,
668
838
  viewport: options.viewport ?? config.viewport,
669
839
  bridge,
840
+ traceRecorder,
841
+ traceContext: {
842
+ windowId: config.trace?.windowId ?? routeKey.split(".")[0],
843
+ routeKey,
844
+ harnessId: config.trace?.harnessId
845
+ },
670
846
  context: {
671
847
  route: {
672
848
  key: routeKey,
@@ -687,7 +863,7 @@ async function createHarness(config = {}) {
687
863
  async mountComponent(component, options = {}) {
688
864
  const componentPath = resolveFromRoot(rootDir, component);
689
865
  assertFileExists(componentPath, "Component");
690
- const bridge = createBrowserBridgeFixture(mergeBridge(config.bridge, options.bridge));
866
+ const bridge = createBrowserBridgeFixtureWithTrace(mergeBridge(config.bridge, options.bridge), config.trace);
691
867
  return await openMountPage({
692
868
  runtime,
693
869
  entrySource: buildComponentEntrySource({
@@ -698,6 +874,11 @@ async function createHarness(config = {}) {
698
874
  css,
699
875
  viewport: options.viewport ?? config.viewport,
700
876
  bridge,
877
+ traceRecorder,
878
+ traceContext: {
879
+ windowId: config.trace?.windowId ?? "wood-browser-harness",
880
+ harnessId: config.trace?.harnessId
881
+ },
701
882
  context: {},
702
883
  hooks: {
703
884
  beforeMount: options.page?.beforeMount ?? config.page?.beforeMount,
@@ -706,7 +887,7 @@ async function createHarness(config = {}) {
706
887
  });
707
888
  },
708
889
  async mountTree(tree) {
709
- const bridge = createBrowserBridgeFixture(mergeBridge(config.bridge, tree.bridge));
890
+ const bridge = createBrowserBridgeFixtureWithTrace(mergeBridge(config.bridge, tree.bridge), config.trace);
710
891
  const layouts = (tree.layouts ?? []).map((layout) => ({
711
892
  component: resolveFromRoot(rootDir, layout.component),
712
893
  props: layout.props
@@ -726,6 +907,12 @@ async function createHarness(config = {}) {
726
907
  css,
727
908
  viewport: tree.viewport ?? config.viewport,
728
909
  bridge,
910
+ traceRecorder,
911
+ traceContext: {
912
+ windowId: config.trace?.windowId ?? tree.page?.key?.split(".")[0] ?? "wood-browser-harness",
913
+ routeKey: tree.page?.key,
914
+ harnessId: config.trace?.harnessId
915
+ },
729
916
  context: tree.page?.key ? {
730
917
  route: {
731
918
  key: tree.page.key,
@@ -737,10 +924,17 @@ async function createHarness(config = {}) {
737
924
  });
738
925
  },
739
926
  async destroy() {
740
- await page.close().catch(() => void 0);
741
- await server.close().catch(() => void 0);
742
- await launchedBrowser.close().catch(() => void 0);
927
+ if (destroyed) {
928
+ return;
929
+ }
930
+ destroyed = true;
931
+ await Promise.all([
932
+ waitForBoundedClose(page.close()),
933
+ waitForBoundedClose(context.close())
934
+ ]);
935
+ traceRecorder?.disable();
743
936
  fs.rmSync(tempDir, { recursive: true, force: true });
937
+ releaseRuntimeOnce();
744
938
  }
745
939
  };
746
940
  }