@multiplatform.one/vite-plugin-webext 6.2.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/src/index.ts ADDED
@@ -0,0 +1,412 @@
1
+ // Vite config factories for the web-extension target of a One platform app.
2
+ //
3
+ // The target lives INSIDE the product app (apps/<name>/webext/) — the same
4
+ // pattern as the GNOME target's vite.config.gnome*.ts — and produces a
5
+ // loadable MV3 extension staging dir:
6
+ //
7
+ // <stagingDir>/manifest.json
8
+ // <stagingDir>/dist/views/<view>/index.html (popup/options/sidepanel/…)
9
+ // <stagingDir>/dist/background/index.mjs (service worker / bg script)
10
+ // <stagingDir>/dist/contentScripts/index.global.js
11
+ //
12
+ // Three vite builds cover the three JS worlds (DOM views, worker, injected
13
+ // content script); `runWebextPrepare` writes the manifest and the dev-mode
14
+ // HMR scaffolding. Hard-won workarounds (workspace source aliases, the
15
+ // one-server-only browser stub, expo type-declaration stubs) come from
16
+ // @multiplatform.one/utils/dev so every product app shares one copy.
17
+
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import {
21
+ lookupTamaguiModules,
22
+ oneServerOnlyBrowserStub,
23
+ stubExpoTypeDeclarations,
24
+ workspaceSourceAliases,
25
+ lookupProjectRoot,
26
+ } from "@multiplatform.one/utils/dev";
27
+ import react from "@vitejs/plugin-react";
28
+ import type { Alias, Plugin, UserConfig } from "vite";
29
+
30
+ export interface WebextPackageInfo {
31
+ name: string;
32
+ displayName?: string;
33
+ version: string;
34
+ description?: string;
35
+ }
36
+
37
+ export interface WebextTargetOptions {
38
+ /** Absolute path of the webext target source dir (contains views/,
39
+ * background/, content_scripts/). */
40
+ targetDir: string;
41
+ /** Absolute path of the loadable-extension staging dir (manifest.json +
42
+ * dist/). Keep it under the app's dist/ so tree-clean stays green. */
43
+ stagingDir: string;
44
+ /** Absolute path to the Tamagui config compiled into the views. */
45
+ tamaguiConfig: string;
46
+ /** Package identity for defines and bundle names. */
47
+ packageInfo: WebextPackageInfo;
48
+ /** Workspace root (defaults to the git toplevel). */
49
+ workspaceRoot?: string;
50
+ /** Views dev-server port. */
51
+ port?: number;
52
+ /** Development mode (watch + HMR scaffolding). Defaults to
53
+ * NODE_ENV !== "production". */
54
+ isDev?: boolean;
55
+ /** Extra defines merged over the factory defaults. */
56
+ define?: Record<string, unknown>;
57
+ /** Extra resolve aliases appended after the factory's. */
58
+ aliases?: Alias[];
59
+ }
60
+
61
+ const WEBEXT_EXTENSIONS = [
62
+ ".webext.ts",
63
+ ".webext.tsx",
64
+ ".webext.js",
65
+ ".webext.jsx",
66
+ ".web.ts",
67
+ ".web.tsx",
68
+ ".web.js",
69
+ ".web.jsx",
70
+ ".ts",
71
+ ".tsx",
72
+ ".js",
73
+ ".jsx",
74
+ ];
75
+
76
+ const OPTIMIZE_INCLUDE = [
77
+ "react",
78
+ "react-dom",
79
+ "webextension-polyfill",
80
+ "@tamagui/core",
81
+ "@tamagui/web",
82
+ "tamagui",
83
+ "react-native-web",
84
+ "@multiplatform.one/components",
85
+ ];
86
+
87
+ /** shiki's chunk graph (wasm/TLA) can't fold into the single-file iife a
88
+ * content script requires. Stub it with an empty module —
89
+ * @multiplatform.one/components' highlightCode() catches the missing
90
+ * codeToTokens and falls back to plain text. */
91
+ export function shikiStubPlugin(): Plugin {
92
+ const VIRTUAL_ID = "\0webext-shiki-stub";
93
+ return {
94
+ name: "webext-shiki-stub",
95
+ enforce: "pre",
96
+ resolveId(id: string) {
97
+ if (/^shiki(\/.*)?$/.test(id)) return VIRTUAL_ID;
98
+ },
99
+ load(id: string) {
100
+ if (id === VIRTUAL_ID) return "export {}";
101
+ },
102
+ };
103
+ }
104
+
105
+ function resolveDefaults(options: WebextTargetOptions) {
106
+ const workspaceRoot = options.workspaceRoot ?? lookupProjectRoot();
107
+ const isDev = options.isDev ?? process.env.NODE_ENV !== "production";
108
+ const port = options.port ?? (Number(process.env.PORT) || 3303);
109
+ return { workspaceRoot, isDev, port };
110
+ }
111
+
112
+ function baseDefine(options: WebextTargetOptions, isDev: boolean): Record<string, unknown> {
113
+ return {
114
+ __DEV__: isDev,
115
+ __NAME__: JSON.stringify(options.packageInfo.name),
116
+ "process.env.NODE_ENV": JSON.stringify(isDev ? "development" : "production"),
117
+ global: "globalThis",
118
+ ...options.define,
119
+ };
120
+ }
121
+
122
+ function baseResolve(options: WebextTargetOptions, workspaceRoot: string) {
123
+ return {
124
+ alias: [
125
+ ...Object.entries(workspaceSourceAliases(workspaceRoot)).map(([find, replacement]) => ({
126
+ find,
127
+ replacement,
128
+ })),
129
+ { find: "react-native", replacement: "react-native-web" },
130
+ { find: "@", replacement: options.targetDir },
131
+ ...(options.aliases ?? []),
132
+ ],
133
+ extensions: WEBEXT_EXTENSIONS,
134
+ mainFields: ["browser", "module", "main"],
135
+ } satisfies UserConfig["resolve"];
136
+ }
137
+
138
+ function baseOptimizeDeps(): UserConfig["optimizeDeps"] {
139
+ return {
140
+ include: OPTIMIZE_INCLUDE,
141
+ esbuildOptions: {
142
+ resolveExtensions: WEBEXT_EXTENSIONS,
143
+ mainFields: ["module", "main"],
144
+ },
145
+ };
146
+ }
147
+
148
+ /** Discover view names — every directory under <targetDir>/views with an
149
+ * index.html becomes an extension page (popup, options, sidepanel, …). */
150
+ export function listWebextViews(targetDir: string): string[] {
151
+ const viewsDir = path.join(targetDir, "views");
152
+ if (!fs.existsSync(viewsDir)) return [];
153
+ return fs
154
+ .readdirSync(viewsDir)
155
+ .filter((file) => fs.statSync(path.join(viewsDir, file)).isDirectory())
156
+ .filter((view) => fs.existsSync(path.join(viewsDir, view, "index.html")));
157
+ }
158
+
159
+ /** The extension pages build (popup/options/sidepanel/devtools…). */
160
+ export async function createWebextViewsConfig(options: WebextTargetOptions): Promise<UserConfig> {
161
+ const { workspaceRoot, isDev, port } = resolveDefaults(options);
162
+ const { tamaguiPlugin } = await import("@tamagui/vite-plugin");
163
+ const views = listWebextViews(options.targetDir);
164
+ return {
165
+ root: options.targetDir,
166
+ base: isDev ? `http://localhost:${port}/` : "/dist/",
167
+ plugins: [
168
+ stubExpoTypeDeclarations(),
169
+ oneServerOnlyBrowserStub(workspaceRoot),
170
+ react({ jsxRuntime: "automatic" }),
171
+ tamaguiPlugin({
172
+ components: lookupTamaguiModules([options.targetDir]),
173
+ config: options.tamaguiConfig,
174
+ outputCSS: path.join(options.targetDir, "tamagui.css"),
175
+ }) as Plugin,
176
+ ],
177
+ define: baseDefine(options, isDev),
178
+ optimizeDeps: baseOptimizeDeps(),
179
+ esbuild: {
180
+ jsx: "automatic",
181
+ target: "esnext",
182
+ },
183
+ resolve: baseResolve(options, workspaceRoot),
184
+ server: {
185
+ port,
186
+ hmr: {
187
+ host: "localhost",
188
+ protocol: "ws",
189
+ clientPort: port,
190
+ },
191
+ origin: `http://localhost:${port}`,
192
+ cors: true,
193
+ headers: {
194
+ "Access-Control-Allow-Origin": "*",
195
+ },
196
+ },
197
+ build: {
198
+ watch: isDev ? {} : undefined,
199
+ outDir: path.join(options.stagingDir, "dist"),
200
+ emptyOutDir: false,
201
+ sourcemap: isDev ? "inline" : false,
202
+ terserOptions: {
203
+ mangle: false,
204
+ },
205
+ rollupOptions: {
206
+ input: Object.fromEntries(
207
+ views.map((view) => [view, path.join(options.targetDir, "views", view, "index.html")]),
208
+ ),
209
+ },
210
+ },
211
+ };
212
+ }
213
+
214
+ /** The background service-worker (chromium) / background-script (firefox)
215
+ * build — a single-file iife, no DOM. */
216
+ export function createWebextBackgroundConfig(options: WebextTargetOptions): UserConfig {
217
+ const { workspaceRoot, isDev } = resolveDefaults(options);
218
+ return {
219
+ root: options.targetDir,
220
+ plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
221
+ define: baseDefine(options, isDev),
222
+ optimizeDeps: baseOptimizeDeps(),
223
+ esbuild: {
224
+ jsx: "automatic",
225
+ target: "esnext",
226
+ },
227
+ resolve: baseResolve(options, workspaceRoot),
228
+ build: {
229
+ watch: isDev ? {} : undefined,
230
+ outDir: path.join(options.stagingDir, "dist/background"),
231
+ cssCodeSplit: false,
232
+ emptyOutDir: false,
233
+ sourcemap: isDev ? "inline" : false,
234
+ lib: {
235
+ entry: path.join(options.targetDir, "background/main.ts"),
236
+ name: options.packageInfo.name,
237
+ formats: ["iife"],
238
+ },
239
+ rollupOptions: {
240
+ output: {
241
+ entryFileNames: "index.mjs",
242
+ extend: true,
243
+ },
244
+ },
245
+ },
246
+ };
247
+ }
248
+
249
+ /** The content-script build — a single-file iife injected into pages, with
250
+ * Tamagui CSS extracted separately (webext.css web-accessible resource). */
251
+ export async function createWebextContentConfig(options: WebextTargetOptions): Promise<UserConfig> {
252
+ const { workspaceRoot, isDev } = resolveDefaults(options);
253
+ const { tamaguiPlugin } = await import("@tamagui/vite-plugin");
254
+ return {
255
+ root: options.targetDir,
256
+ plugins: [
257
+ stubExpoTypeDeclarations(),
258
+ oneServerOnlyBrowserStub(workspaceRoot),
259
+ shikiStubPlugin(),
260
+ tamaguiPlugin({
261
+ components: lookupTamaguiModules([options.targetDir]),
262
+ config: options.tamaguiConfig,
263
+ outputCSS: path.join(options.targetDir, "tamagui.content.css"),
264
+ }) as Plugin,
265
+ ],
266
+ define: {
267
+ ...baseDefine(options, isDev),
268
+ process: {
269
+ env: {
270
+ NODE_ENV: JSON.stringify(isDev ? "development" : "production"),
271
+ },
272
+ },
273
+ },
274
+ optimizeDeps: baseOptimizeDeps(),
275
+ esbuild: {
276
+ jsx: "automatic",
277
+ target: "esnext",
278
+ },
279
+ resolve: baseResolve(options, workspaceRoot),
280
+ build: {
281
+ watch: isDev ? {} : undefined,
282
+ outDir: path.join(options.stagingDir, "dist/contentScripts"),
283
+ cssCodeSplit: false,
284
+ emptyOutDir: false,
285
+ sourcemap: isDev ? "inline" : false,
286
+ lib: {
287
+ entry: path.join(options.targetDir, "content_scripts/index.tsx"),
288
+ name: options.packageInfo.name,
289
+ formats: ["iife"],
290
+ },
291
+ rollupOptions: {
292
+ output: {
293
+ entryFileNames: "index.global.js",
294
+ extend: true,
295
+ },
296
+ },
297
+ },
298
+ };
299
+ }
300
+
301
+ // ── prepare pipeline (manifest + dev HMR scaffolding) ─────────────
302
+
303
+ export interface WebextPrepareOptions {
304
+ targetDir: string;
305
+ stagingDir: string;
306
+ /** Produces the manifest object (app-owned — permissions, views, icons).
307
+ * Typed `object` so interface-typed manifests (webextension-polyfill's
308
+ * WebExtensionManifest has no index signature) assign cleanly. */
309
+ getManifest: () => Promise<object> | object;
310
+ /** Static files (icons, …) copied verbatim into the staging root.
311
+ * Defaults to `<targetDir>/static` when it exists. */
312
+ staticDir?: string;
313
+ isDev?: boolean;
314
+ port?: number;
315
+ /** Watch manifest/html sources and rewrite on change (dev loop). */
316
+ watch?: boolean;
317
+ }
318
+
319
+ /** Copy static extension files (icons, …) into the staging root — the
320
+ * staging dir is build output (gitignored), so committed assets live in
321
+ * the target source tree instead. */
322
+ export async function copyWebextStatic(options: WebextPrepareOptions): Promise<void> {
323
+ const staticDir = options.staticDir ?? path.join(options.targetDir, "static");
324
+ if (!fs.existsSync(staticDir)) return;
325
+ await fs.promises.mkdir(options.stagingDir, { recursive: true });
326
+ await fs.promises.cp(staticDir, options.stagingDir, { recursive: true });
327
+ }
328
+
329
+ export async function writeWebextManifest(options: WebextPrepareOptions): Promise<void> {
330
+ const manifestPath = path.join(options.stagingDir, "manifest.json");
331
+ await fs.promises.mkdir(path.dirname(manifestPath), { recursive: true });
332
+ await fs.promises.writeFile(
333
+ manifestPath,
334
+ JSON.stringify(await options.getManifest(), null, 2),
335
+ "utf-8",
336
+ );
337
+ }
338
+
339
+ /** Dev mode: emit view HTML that loads main.tsx from the vite dev server
340
+ * (extension pages can't be served BY vite — the html on disk must point
341
+ * at it) plus the react-refresh preamble shim. */
342
+ export async function stubWebextIndexHtml(options: WebextPrepareOptions): Promise<void> {
343
+ const port = options.port ?? 3303;
344
+ for (const view of listWebextViews(options.targetDir)) {
345
+ let data = await fs.promises.readFile(
346
+ path.join(options.targetDir, "views", view, "index.html"),
347
+ "utf-8",
348
+ );
349
+ data = data.replace(
350
+ "</head>",
351
+ ` <script type="module" src="http://localhost:${port}/@vite/client"></script>
352
+ <script type="module" src="/dist/refresh.js"></script>
353
+ </head>`,
354
+ );
355
+ data = data
356
+ .replace(
357
+ '<script type="module" src="./main.tsx"></script>',
358
+ `<script type="module" src="http://localhost:${port}/views/${view}/main.tsx"></script>`,
359
+ )
360
+ .replace('<div id="app"></div>', '<div id="app">vite server did not start</div>');
361
+ const outPath = path.join(options.stagingDir, "dist/views", view, "index.html");
362
+ await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
363
+ await fs.promises.writeFile(outPath, data, "utf-8");
364
+ }
365
+ }
366
+
367
+ export async function writeWebextRefreshScript(options: WebextPrepareOptions): Promise<void> {
368
+ const port = options.port ?? 3303;
369
+ const refreshPath = path.join(options.stagingDir, "dist/refresh.js");
370
+ await fs.promises.mkdir(path.dirname(refreshPath), { recursive: true });
371
+ await fs.promises.writeFile(
372
+ refreshPath,
373
+ `window.__vite_plugin_react_preamble_installed__ = true;
374
+ window.$RefreshReg$ = () => {};
375
+ window.$RefreshSig$ = () => (type) => type;
376
+ window.addEventListener('load', () => {
377
+ import("http://localhost:${port}/@react-refresh")
378
+ .then((RefreshRuntime) => {
379
+ if (RefreshRuntime && typeof RefreshRuntime.injectIntoGlobalHook === 'function') {
380
+ RefreshRuntime.injectIntoGlobalHook(window);
381
+ }
382
+ })
383
+ .catch(console.error);
384
+ });
385
+ `,
386
+ "utf-8",
387
+ );
388
+ }
389
+
390
+ /** One-shot (prod) or watching (dev) prepare: manifest + static assets +
391
+ * dev scaffolding. */
392
+ export async function runWebextPrepare(options: WebextPrepareOptions): Promise<void> {
393
+ const isDev = options.isDev ?? process.env.NODE_ENV !== "production";
394
+ if (!isDev) {
395
+ await copyWebextStatic(options);
396
+ await writeWebextManifest(options);
397
+ return;
398
+ }
399
+ await copyWebextStatic(options);
400
+ await stubWebextIndexHtml(options);
401
+ await writeWebextRefreshScript(options);
402
+ await writeWebextManifest(options);
403
+ if (options.watch) {
404
+ const { watch } = await import("chokidar");
405
+ watch(path.join(options.targetDir, "**/*.html")).on("change", () =>
406
+ stubWebextIndexHtml(options).catch(console.error),
407
+ );
408
+ watch(path.join(options.targetDir, "manifest.ts")).on("change", () =>
409
+ writeWebextManifest(options).catch(console.error),
410
+ );
411
+ }
412
+ }
@@ -0,0 +1,75 @@
1
+ import type { Alias, Plugin, UserConfig } from "vite";
2
+ export interface WebextPackageInfo {
3
+ name: string;
4
+ displayName?: string;
5
+ version: string;
6
+ description?: string;
7
+ }
8
+ export interface WebextTargetOptions {
9
+ /** Absolute path of the webext target source dir (contains views/,
10
+ * background/, content_scripts/). */
11
+ targetDir: string;
12
+ /** Absolute path of the loadable-extension staging dir (manifest.json +
13
+ * dist/). Keep it under the app's dist/ so tree-clean stays green. */
14
+ stagingDir: string;
15
+ /** Absolute path to the Tamagui config compiled into the views. */
16
+ tamaguiConfig: string;
17
+ /** Package identity for defines and bundle names. */
18
+ packageInfo: WebextPackageInfo;
19
+ /** Workspace root (defaults to the git toplevel). */
20
+ workspaceRoot?: string;
21
+ /** Views dev-server port. */
22
+ port?: number;
23
+ /** Development mode (watch + HMR scaffolding). Defaults to
24
+ * NODE_ENV !== "production". */
25
+ isDev?: boolean;
26
+ /** Extra defines merged over the factory defaults. */
27
+ define?: Record<string, unknown>;
28
+ /** Extra resolve aliases appended after the factory's. */
29
+ aliases?: Alias[];
30
+ }
31
+ /** shiki's chunk graph (wasm/TLA) can't fold into the single-file iife a
32
+ * content script requires. Stub it with an empty module —
33
+ * @multiplatform.one/components' highlightCode() catches the missing
34
+ * codeToTokens and falls back to plain text. */
35
+ export declare function shikiStubPlugin(): Plugin;
36
+ /** Discover view names — every directory under <targetDir>/views with an
37
+ * index.html becomes an extension page (popup, options, sidepanel, …). */
38
+ export declare function listWebextViews(targetDir: string): string[];
39
+ /** The extension pages build (popup/options/sidepanel/devtools…). */
40
+ export declare function createWebextViewsConfig(options: WebextTargetOptions): Promise<UserConfig>;
41
+ /** The background service-worker (chromium) / background-script (firefox)
42
+ * build — a single-file iife, no DOM. */
43
+ export declare function createWebextBackgroundConfig(options: WebextTargetOptions): UserConfig;
44
+ /** The content-script build — a single-file iife injected into pages, with
45
+ * Tamagui CSS extracted separately (webext.css web-accessible resource). */
46
+ export declare function createWebextContentConfig(options: WebextTargetOptions): Promise<UserConfig>;
47
+ export interface WebextPrepareOptions {
48
+ targetDir: string;
49
+ stagingDir: string;
50
+ /** Produces the manifest object (app-owned — permissions, views, icons).
51
+ * Typed `object` so interface-typed manifests (webextension-polyfill's
52
+ * WebExtensionManifest has no index signature) assign cleanly. */
53
+ getManifest: () => Promise<object> | object;
54
+ /** Static files (icons, …) copied verbatim into the staging root.
55
+ * Defaults to `<targetDir>/static` when it exists. */
56
+ staticDir?: string;
57
+ isDev?: boolean;
58
+ port?: number;
59
+ /** Watch manifest/html sources and rewrite on change (dev loop). */
60
+ watch?: boolean;
61
+ }
62
+ /** Copy static extension files (icons, …) into the staging root — the
63
+ * staging dir is build output (gitignored), so committed assets live in
64
+ * the target source tree instead. */
65
+ export declare function copyWebextStatic(options: WebextPrepareOptions): Promise<void>;
66
+ export declare function writeWebextManifest(options: WebextPrepareOptions): Promise<void>;
67
+ /** Dev mode: emit view HTML that loads main.tsx from the vite dev server
68
+ * (extension pages can't be served BY vite — the html on disk must point
69
+ * at it) plus the react-refresh preamble shim. */
70
+ export declare function stubWebextIndexHtml(options: WebextPrepareOptions): Promise<void>;
71
+ export declare function writeWebextRefreshScript(options: WebextPrepareOptions): Promise<void>;
72
+ /** One-shot (prod) or watching (dev) prepare: manifest + static assets +
73
+ * dev scaffolding. */
74
+ export declare function runWebextPrepare(options: WebextPrepareOptions): Promise<void>;
75
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC;0CACsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB;2EACuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,WAAW,EAAE,iBAAiB,CAAC;IAC/B,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;qCACiC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;CACnB;AA4BD;;;iDAGiD;AACjD,wBAAgB,eAAe,IAAI,MAAM,CAYxC;AA6CD;2EAC2E;AAC3E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAO3D;AAED,qEAAqE;AACrE,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoD/F;AAED;0CAC0C;AAC1C,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CA+BrF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgDjG;AAID,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;uEAEmE;IACnE,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC5C;2DACuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;sCAEsC;AACtC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF;AAED;;mDAEmD;AACnD,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBtF;AAED,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED;uBACuB;AACvB,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBnF"}