@noego/wood 0.1.3 → 0.2.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,66 +1,86 @@
1
- import { vi } from "vitest";
1
+ function createMockFunction(implementation) {
2
+ let currentImplementation = implementation ?? (() => void 0);
3
+ const mock = ((...args) => {
4
+ mock.calls.push(args);
5
+ return currentImplementation(...args);
6
+ });
7
+ mock.calls = [];
8
+ mock.mockImplementation = (nextImplementation) => {
9
+ currentImplementation = nextImplementation;
10
+ return mock;
11
+ };
12
+ mock.mockResolvedValue = (value) => {
13
+ currentImplementation = () => Promise.resolve(value);
14
+ return mock;
15
+ };
16
+ mock.mockReturnValue = (value) => {
17
+ currentImplementation = () => value;
18
+ return mock;
19
+ };
20
+ return mock;
21
+ }
2
22
  function mockElectron() {
3
23
  const mocks = {
4
24
  ipcMain: {
5
- handle: vi.fn(),
6
- removeHandler: vi.fn(),
7
- on: vi.fn()
25
+ handle: createMockFunction(),
26
+ removeHandler: createMockFunction(),
27
+ on: createMockFunction()
8
28
  },
9
29
  ipcRenderer: {
10
- invoke: vi.fn().mockResolvedValue(void 0),
11
- send: vi.fn(),
12
- on: vi.fn(),
13
- removeListener: vi.fn()
30
+ invoke: createMockFunction().mockResolvedValue(void 0),
31
+ send: createMockFunction(),
32
+ on: createMockFunction(),
33
+ removeListener: createMockFunction()
14
34
  },
15
- BrowserWindow: vi.fn().mockImplementation(() => ({
16
- loadURL: vi.fn().mockResolvedValue(void 0),
17
- loadFile: vi.fn().mockResolvedValue(void 0),
35
+ BrowserWindow: createMockFunction().mockImplementation(() => ({
36
+ loadURL: createMockFunction().mockResolvedValue(void 0),
37
+ loadFile: createMockFunction().mockResolvedValue(void 0),
18
38
  webContents: {
19
- send: vi.fn(),
20
- reload: vi.fn(),
21
- on: vi.fn()
39
+ send: createMockFunction(),
40
+ reload: createMockFunction(),
41
+ on: createMockFunction()
22
42
  },
23
- on: vi.fn(),
24
- once: vi.fn(),
25
- close: vi.fn(),
26
- focus: vi.fn(),
27
- show: vi.fn(),
28
- destroy: vi.fn(),
29
- isDestroyed: vi.fn().mockReturnValue(false)
43
+ on: createMockFunction(),
44
+ once: createMockFunction(),
45
+ close: createMockFunction(),
46
+ focus: createMockFunction(),
47
+ show: createMockFunction(),
48
+ destroy: createMockFunction(),
49
+ isDestroyed: createMockFunction().mockReturnValue(false)
30
50
  })),
31
51
  app: {
32
- getPath: vi.fn((name) => `/mock/${name}`),
33
- on: vi.fn(),
34
- once: vi.fn(),
35
- whenReady: vi.fn().mockResolvedValue(void 0),
52
+ getPath: createMockFunction((name) => `/mock/${name}`),
53
+ on: createMockFunction(),
54
+ once: createMockFunction(),
55
+ whenReady: createMockFunction().mockResolvedValue(void 0),
36
56
  isPackaged: false,
37
- quit: vi.fn()
57
+ quit: createMockFunction()
38
58
  },
39
59
  dialog: {
40
- showOpenDialog: vi.fn().mockResolvedValue({ filePaths: [], canceled: false }),
41
- showSaveDialog: vi.fn().mockResolvedValue({ filePath: "", canceled: false }),
42
- showMessageBox: vi.fn().mockResolvedValue({ response: 0 })
60
+ showOpenDialog: createMockFunction().mockResolvedValue({ filePaths: [], canceled: false }),
61
+ showSaveDialog: createMockFunction().mockResolvedValue({ filePath: "", canceled: false }),
62
+ showMessageBox: createMockFunction().mockResolvedValue({ response: 0 })
43
63
  },
44
64
  shell: {
45
- openExternal: vi.fn().mockResolvedValue(void 0),
46
- openPath: vi.fn().mockResolvedValue("")
65
+ openExternal: createMockFunction().mockResolvedValue(void 0),
66
+ openPath: createMockFunction().mockResolvedValue("")
47
67
  },
48
68
  safeStorage: {
49
- encryptString: vi.fn().mockReturnValue(Buffer.from("")),
50
- decryptString: vi.fn().mockReturnValue(""),
51
- isEncryptionAvailable: vi.fn().mockReturnValue(true)
69
+ encryptString: createMockFunction().mockReturnValue(Buffer.from("")),
70
+ decryptString: createMockFunction().mockReturnValue(""),
71
+ isEncryptionAvailable: createMockFunction().mockReturnValue(true)
52
72
  },
53
73
  nativeTheme: {
54
74
  shouldUseDarkColors: false,
55
- on: vi.fn()
75
+ on: createMockFunction()
56
76
  },
57
77
  globalShortcut: {
58
- register: vi.fn().mockReturnValue(true),
59
- unregister: vi.fn(),
60
- unregisterAll: vi.fn()
78
+ register: createMockFunction().mockReturnValue(true),
79
+ unregister: createMockFunction(),
80
+ unregisterAll: createMockFunction()
61
81
  },
62
82
  contextBridge: {
63
- exposeInMainWorld: vi.fn()
83
+ exposeInMainWorld: createMockFunction()
64
84
  }
65
85
  };
66
86
  return mocks;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/mock_electron.ts"],"sourcesContent":["import { vi } from 'vitest';\n\n/**\n * Comprehensive Electron mock for vitest.\n * Call this in your test setup or beforeAll.\n */\nexport function mockElectron(): Record<string, any> {\n // This needs to be called with vi.mock in the test file itself\n // because vi.mock is hoisted. This function returns the mock object.\n const mocks = {\n ipcMain: {\n handle: vi.fn(),\n removeHandler: vi.fn(),\n on: vi.fn(),\n },\n ipcRenderer: {\n invoke: vi.fn().mockResolvedValue(undefined),\n send: vi.fn(),\n on: vi.fn(),\n removeListener: vi.fn(),\n },\n BrowserWindow: vi.fn().mockImplementation(() => ({\n loadURL: vi.fn().mockResolvedValue(undefined),\n loadFile: vi.fn().mockResolvedValue(undefined),\n webContents: {\n send: vi.fn(),\n reload: vi.fn(),\n on: vi.fn(),\n },\n on: vi.fn(),\n once: vi.fn(),\n close: vi.fn(),\n focus: vi.fn(),\n show: vi.fn(),\n destroy: vi.fn(),\n isDestroyed: vi.fn().mockReturnValue(false),\n })),\n app: {\n getPath: vi.fn((name: string) => `/mock/${name}`),\n on: vi.fn(),\n once: vi.fn(),\n whenReady: vi.fn().mockResolvedValue(undefined),\n isPackaged: false,\n quit: vi.fn(),\n },\n dialog: {\n showOpenDialog: vi.fn().mockResolvedValue({ filePaths: [], canceled: false }),\n showSaveDialog: vi.fn().mockResolvedValue({ filePath: '', canceled: false }),\n showMessageBox: vi.fn().mockResolvedValue({ response: 0 }),\n },\n shell: {\n openExternal: vi.fn().mockResolvedValue(undefined),\n openPath: vi.fn().mockResolvedValue(''),\n },\n safeStorage: {\n encryptString: vi.fn().mockReturnValue(Buffer.from('')),\n decryptString: vi.fn().mockReturnValue(''),\n isEncryptionAvailable: vi.fn().mockReturnValue(true),\n },\n nativeTheme: {\n shouldUseDarkColors: false,\n on: vi.fn(),\n },\n globalShortcut: {\n register: vi.fn().mockReturnValue(true),\n unregister: vi.fn(),\n unregisterAll: vi.fn(),\n },\n contextBridge: {\n exposeInMainWorld: vi.fn(),\n },\n };\n\n return mocks;\n}\n\n/**\n * Get the vi.mock factory for Electron.\n * Usage: vi.mock('electron', () => getElectronMockFactory())\n */\nexport function getElectronMockFactory(): Record<string, any> {\n return mockElectron();\n}\n"],"mappings":"AAAA,SAAS,UAAU;AAMZ,SAAS,eAAoC;AAGlD,QAAM,QAAQ;AAAA,IACZ,SAAS;AAAA,MACP,QAAQ,GAAG,GAAG;AAAA,MACd,eAAe,GAAG,GAAG;AAAA,MACrB,IAAI,GAAG,GAAG;AAAA,IACZ;AAAA,IACA,aAAa;AAAA,MACX,QAAQ,GAAG,GAAG,EAAE,kBAAkB,MAAS;AAAA,MAC3C,MAAM,GAAG,GAAG;AAAA,MACZ,IAAI,GAAG,GAAG;AAAA,MACV,gBAAgB,GAAG,GAAG;AAAA,IACxB;AAAA,IACA,eAAe,GAAG,GAAG,EAAE,mBAAmB,OAAO;AAAA,MAC/C,SAAS,GAAG,GAAG,EAAE,kBAAkB,MAAS;AAAA,MAC5C,UAAU,GAAG,GAAG,EAAE,kBAAkB,MAAS;AAAA,MAC7C,aAAa;AAAA,QACX,MAAM,GAAG,GAAG;AAAA,QACZ,QAAQ,GAAG,GAAG;AAAA,QACd,IAAI,GAAG,GAAG;AAAA,MACZ;AAAA,MACA,IAAI,GAAG,GAAG;AAAA,MACV,MAAM,GAAG,GAAG;AAAA,MACZ,OAAO,GAAG,GAAG;AAAA,MACb,OAAO,GAAG,GAAG;AAAA,MACb,MAAM,GAAG,GAAG;AAAA,MACZ,SAAS,GAAG,GAAG;AAAA,MACf,aAAa,GAAG,GAAG,EAAE,gBAAgB,KAAK;AAAA,IAC5C,EAAE;AAAA,IACF,KAAK;AAAA,MACH,SAAS,GAAG,GAAG,CAAC,SAAiB,SAAS,IAAI,EAAE;AAAA,MAChD,IAAI,GAAG,GAAG;AAAA,MACV,MAAM,GAAG,GAAG;AAAA,MACZ,WAAW,GAAG,GAAG,EAAE,kBAAkB,MAAS;AAAA,MAC9C,YAAY;AAAA,MACZ,MAAM,GAAG,GAAG;AAAA,IACd;AAAA,IACA,QAAQ;AAAA,MACN,gBAAgB,GAAG,GAAG,EAAE,kBAAkB,EAAE,WAAW,CAAC,GAAG,UAAU,MAAM,CAAC;AAAA,MAC5E,gBAAgB,GAAG,GAAG,EAAE,kBAAkB,EAAE,UAAU,IAAI,UAAU,MAAM,CAAC;AAAA,MAC3E,gBAAgB,GAAG,GAAG,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAAA,IAC3D;AAAA,IACA,OAAO;AAAA,MACL,cAAc,GAAG,GAAG,EAAE,kBAAkB,MAAS;AAAA,MACjD,UAAU,GAAG,GAAG,EAAE,kBAAkB,EAAE;AAAA,IACxC;AAAA,IACA,aAAa;AAAA,MACX,eAAe,GAAG,GAAG,EAAE,gBAAgB,OAAO,KAAK,EAAE,CAAC;AAAA,MACtD,eAAe,GAAG,GAAG,EAAE,gBAAgB,EAAE;AAAA,MACzC,uBAAuB,GAAG,GAAG,EAAE,gBAAgB,IAAI;AAAA,IACrD;AAAA,IACA,aAAa;AAAA,MACX,qBAAqB;AAAA,MACrB,IAAI,GAAG,GAAG;AAAA,IACZ;AAAA,IACA,gBAAgB;AAAA,MACd,UAAU,GAAG,GAAG,EAAE,gBAAgB,IAAI;AAAA,MACtC,YAAY,GAAG,GAAG;AAAA,MAClB,eAAe,GAAG,GAAG;AAAA,IACvB;AAAA,IACA,eAAe;AAAA,MACb,mBAAmB,GAAG,GAAG;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;AAMO,SAAS,yBAA8C;AAC5D,SAAO,aAAa;AACtB;","names":[]}
1
+ {"version":3,"sources":["../../src/testing/mock_electron.ts"],"sourcesContent":["type MockFunction = ((...args: any[]) => any) & {\n calls: any[][];\n mockImplementation: (implementation: (...args: any[]) => any) => MockFunction;\n mockResolvedValue: (value: any) => MockFunction;\n mockReturnValue: (value: any) => MockFunction;\n};\n\nfunction createMockFunction(implementation?: (...args: any[]) => any): MockFunction {\n let currentImplementation = implementation ?? (() => undefined);\n const mock = ((...args: any[]) => {\n mock.calls.push(args);\n return currentImplementation(...args);\n }) as MockFunction;\n mock.calls = [];\n mock.mockImplementation = (nextImplementation: (...args: any[]) => any) => {\n currentImplementation = nextImplementation;\n return mock;\n };\n mock.mockResolvedValue = (value: any) => {\n currentImplementation = () => Promise.resolve(value);\n return mock;\n };\n mock.mockReturnValue = (value: any) => {\n currentImplementation = () => value;\n return mock;\n };\n return mock;\n}\n\n/**\n * Comprehensive Electron mock for tests.\n * Call this in your test setup or beforeAll.\n */\nexport function mockElectron(): Record<string, any> {\n // This returns the mock object for use with a test runner's module mock factory.\n const mocks = {\n ipcMain: {\n handle: createMockFunction(),\n removeHandler: createMockFunction(),\n on: createMockFunction(),\n },\n ipcRenderer: {\n invoke: createMockFunction().mockResolvedValue(undefined),\n send: createMockFunction(),\n on: createMockFunction(),\n removeListener: createMockFunction(),\n },\n BrowserWindow: createMockFunction().mockImplementation(() => ({\n loadURL: createMockFunction().mockResolvedValue(undefined),\n loadFile: createMockFunction().mockResolvedValue(undefined),\n webContents: {\n send: createMockFunction(),\n reload: createMockFunction(),\n on: createMockFunction(),\n },\n on: createMockFunction(),\n once: createMockFunction(),\n close: createMockFunction(),\n focus: createMockFunction(),\n show: createMockFunction(),\n destroy: createMockFunction(),\n isDestroyed: createMockFunction().mockReturnValue(false),\n })),\n app: {\n getPath: createMockFunction((name: string) => `/mock/${name}`),\n on: createMockFunction(),\n once: createMockFunction(),\n whenReady: createMockFunction().mockResolvedValue(undefined),\n isPackaged: false,\n quit: createMockFunction(),\n },\n dialog: {\n showOpenDialog: createMockFunction().mockResolvedValue({ filePaths: [], canceled: false }),\n showSaveDialog: createMockFunction().mockResolvedValue({ filePath: '', canceled: false }),\n showMessageBox: createMockFunction().mockResolvedValue({ response: 0 }),\n },\n shell: {\n openExternal: createMockFunction().mockResolvedValue(undefined),\n openPath: createMockFunction().mockResolvedValue(''),\n },\n safeStorage: {\n encryptString: createMockFunction().mockReturnValue(Buffer.from('')),\n decryptString: createMockFunction().mockReturnValue(''),\n isEncryptionAvailable: createMockFunction().mockReturnValue(true),\n },\n nativeTheme: {\n shouldUseDarkColors: false,\n on: createMockFunction(),\n },\n globalShortcut: {\n register: createMockFunction().mockReturnValue(true),\n unregister: createMockFunction(),\n unregisterAll: createMockFunction(),\n },\n contextBridge: {\n exposeInMainWorld: createMockFunction(),\n },\n };\n\n return mocks;\n}\n\n/**\n * Get a module mock factory for Electron.\n * Usage: vi.mock('electron', () => getElectronMockFactory())\n */\nexport function getElectronMockFactory(): Record<string, any> {\n return mockElectron();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,212 @@
1
+ # Browser Testing
2
+
3
+ Wood includes a browser harness for tests that need real Svelte rendering,
4
+ real CSS layout, browser APIs, or animation timing without booting a full
5
+ Electron app.
6
+
7
+ Use it when the behavior is visual or renderer-bound:
8
+
9
+ - a Svelte component must mount in Chromium
10
+ - layered layouts need real positioning
11
+ - a page needs Wood route, controller, and navigation wiring
12
+ - z-index, clipping, viewport placement, or animation stability matters
13
+ - the test needs a controlled Wood bridge instead of real Electron IPC
14
+
15
+ Do not use it for pure state or service behavior. Those tests should stay in
16
+ Node and exercise the nearest owned boundary through the IoC container.
17
+
18
+ ## Import
19
+
20
+ ```ts
21
+ import { browser } from '@noego/wood/testing';
22
+ ```
23
+
24
+ The public testing entry point intentionally exports `browser` from
25
+ `@noego/wood/testing`. There is no extra `/browser` segment in normal test
26
+ imports.
27
+
28
+ ## Create a Harness
29
+
30
+ ```ts
31
+ const harness = await browser.createHarness({
32
+ rootDir: process.cwd(),
33
+ componentDir: 'ui',
34
+ css: ['ui/app.css'],
35
+ bridge: {
36
+ operations: {
37
+ 'settings.get': () => ({ readSpeed: 0 }),
38
+ },
39
+ },
40
+ });
41
+
42
+ try {
43
+ // mount components, trees, or routes here
44
+ } finally {
45
+ await harness.destroy();
46
+ }
47
+ ```
48
+
49
+ `createHarness` starts a Vite dev server with the Svelte plugin, launches a
50
+ Playwright browser, installs a Wood bridge fixture, and writes temporary mount
51
+ entries under the configured `rootDir`.
52
+
53
+ ## Mount a Component
54
+
55
+ ```ts
56
+ const mounted = await harness.mountComponent('ui/components/StatusPanel.svelte', {
57
+ props: {
58
+ title: 'Ready',
59
+ },
60
+ });
61
+
62
+ await mounted.expect.visible('[data-testid="status-panel"]');
63
+ await mounted.expect.insideViewport('[data-testid="status-panel"]');
64
+ ```
65
+
66
+ `mountComponent` takes a component file path, not an imported component object.
67
+ The browser runtime imports and compiles the Svelte module through Vite. A Node
68
+ test cannot pass an imported Svelte component class across the Playwright
69
+ process boundary.
70
+
71
+ ## Mount a Route
72
+
73
+ Use `viewsConfig` when the test needs Wood route metadata, controller creation,
74
+ layouts, route params, or query values to exist before the page controller is
75
+ constructed.
76
+
77
+ ```ts
78
+ const harness = await browser.createHarness({
79
+ rootDir: process.cwd(),
80
+ componentDir: 'ui',
81
+ viewsConfig: 'ui/openapi/views.yaml',
82
+ css: ['ui/app.css'],
83
+ bridge: {
84
+ operations: {
85
+ 'settings.get': () => ({ readSpeed: 0 }),
86
+ },
87
+ },
88
+ });
89
+
90
+ const page = await harness.mountRoute('main.settings', {
91
+ route: {
92
+ params: {},
93
+ query: {
94
+ tab: 'appearance',
95
+ },
96
+ },
97
+ });
98
+
99
+ await page.expect.visible('[data-testid="settings-page"]');
100
+ ```
101
+
102
+ `mountRoute` can also boot a generated Wood app by passing `generatedApp`, but
103
+ `viewsConfig` is preferred for route-level tests because it gives the harness
104
+ the route manifest directly. Generated app mounting is useful for smoke tests
105
+ against generated renderer output.
106
+
107
+ ## Mount a Layered Tree
108
+
109
+ Use `mountTree` when the test needs layout layering but not a full views YAML
110
+ route.
111
+
112
+ ```ts
113
+ const mounted = await harness.mountTree({
114
+ layouts: [
115
+ {
116
+ component: 'ui/layouts/AppShell.svelte',
117
+ props: {
118
+ data: { sidebarOpen: true },
119
+ },
120
+ },
121
+ ],
122
+ view: {
123
+ component: 'ui/pages/Inbox.svelte',
124
+ props: {
125
+ data: { selectedThreadId: 'thread-1' },
126
+ },
127
+ },
128
+ });
129
+
130
+ await mounted.expect.notOverlapping('[data-testid="sidebar"]', '[data-testid="composer"]');
131
+ ```
132
+
133
+ `mountTree` is a framework harness for Wood's layout composition. App-specific
134
+ fixture builders can wrap it, but Wood should not know app concepts such as
135
+ chat threads, settings themes, or domain-specific ready states.
136
+
137
+ ## Bridge Fixtures
138
+
139
+ The harness installs a Wood bridge fixture before the page loads.
140
+
141
+ ```ts
142
+ const mounted = await harness.mountComponent('ui/SaveButton.svelte', {
143
+ bridge: {
144
+ operations: {
145
+ 'document.save': async (input) => ({ ok: true, input }),
146
+ },
147
+ events: {
148
+ 'document.changed': { id: 'doc-1' },
149
+ },
150
+ },
151
+ });
152
+
153
+ await mounted.click('[data-testid="save"]');
154
+
155
+ expect(mounted.bridge.calls('document.save')).toHaveLength(1);
156
+
157
+ await mounted.bridge.emit('document.changed', { id: 'doc-2' });
158
+ ```
159
+
160
+ Operations are keyed by the same channel shape Wood exposes on the renderer
161
+ bridge, for example `settings.get` or `messages.send`. Static values are
162
+ returned directly. Functions receive the operation input and a small context
163
+ with the channel and recorded calls.
164
+
165
+ ## Assertions
166
+
167
+ Every mounted surface exposes Playwright's `page` and `locator`, plus
168
+ Wood-specific assertions for visual bugs:
169
+
170
+ ```ts
171
+ await mounted.expect.visible('.popover');
172
+ await mounted.expect.hidden('.menu');
173
+ await mounted.expect.insideViewport('.composer', { margin: 8 });
174
+ await mounted.expect.notClipped('.popover');
175
+ await mounted.expect.notOverlapping('.toast', '.composer');
176
+ await mounted.expect.zIndexAbove('.modal', '.backdrop');
177
+ await mounted.expect.stableAfterAnimation('.drawer');
178
+ await mounted.expect.screenshot('settings-drawer');
179
+ ```
180
+
181
+ Screenshots are written to `test-results/wood-browser`.
182
+
183
+ ## Lifecycle
184
+
185
+ Destroy the harness after each test:
186
+
187
+ ```ts
188
+ const harnesses: Array<{ destroy(): Promise<void> }> = [];
189
+
190
+ afterEach(async () => {
191
+ while (harnesses.length > 0) {
192
+ await harnesses.pop()?.destroy();
193
+ }
194
+ });
195
+ ```
196
+
197
+ Mounted surfaces also expose `destroy()`, but the harness owns the browser and
198
+ Vite server. In most tests, destroying the harness is the important cleanup.
199
+
200
+ ## Boundary
201
+
202
+ The browser harness is not a full Electron app:
203
+
204
+ - Electron IPC is replaced by the Wood bridge fixture.
205
+ - Main-process services do not boot unless the app explicitly reaches them
206
+ through a test-controlled operation.
207
+ - Browser-only bugs are observable because Chromium renders the page.
208
+ - Electron-only bugs still need an Electron + Playwright test.
209
+
210
+ This boundary is intentional. It gives component, layout, and page tests a real
211
+ browser while keeping setup small enough that developers can write the test
212
+ without constructing a whole application runtime.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/wood",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,7 @@
18
18
  "dist",
19
19
  "bin",
20
20
  "loaders",
21
+ "docs",
21
22
  "src/components/",
22
23
  "src/client/controller_contract.ts",
23
24
  "src/controller/controller_lifecycle.ts",
@@ -98,16 +99,21 @@
98
99
  "rxjs": "^7.x"
99
100
  },
100
101
  "peerDependencies": {
102
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
101
103
  "electron": ">=28.0.0",
102
104
  "playwright": "^1.40.0",
103
105
  "svelte": "^5.x"
104
106
  },
105
107
  "peerDependenciesMeta": {
108
+ "@sveltejs/vite-plugin-svelte": {
109
+ "optional": true
110
+ },
106
111
  "playwright": {
107
112
  "optional": true
108
113
  }
109
114
  },
110
115
  "devDependencies": {
116
+ "@sveltejs/vite-plugin-svelte": "^4.0.4",
111
117
  "@types/js-yaml": "^4.x",
112
118
  "@types/node": "^20.10.4",
113
119
  "electron-builder": "^25.x",