From b3c0f7a340e7343738d727ce72afd86010229a6b Mon Sep 17 00:00:00 2001 From: gronxb Date: Mon, 20 Jan 2025 23:20:29 +0900 Subject: [PATCH 01/61] feat: command --- packages/hot-updater/src/commands/init.ts | 13 ++++++++++++- .../hot-updater/src/commands/init/cloudflareD1R2.ts | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2.ts diff --git a/packages/hot-updater/src/commands/init.ts b/packages/hot-updater/src/commands/init.ts index 0baf899e..a1f74f59 100644 --- a/packages/hot-updater/src/commands/init.ts +++ b/packages/hot-updater/src/commands/init.ts @@ -1,6 +1,7 @@ import { printBanner } from "@/components/banner"; import { ensureInstallPackages } from "@/utils/ensureInstallPackages"; import { isCancel, select } from "@clack/prompts"; +import { initCloudflareD1R2 } from "./init/cloudflareD1R2"; import { initSupabase } from "./init/supabase"; const REQUIRED_PACKAGES = { @@ -17,6 +18,10 @@ const PACKAGE_MAP = { dependencies: [], devDependencies: ["@hot-updater/aws"], }, + "cloudflare-d1-r2": { + dependencies: [], + devDependencies: ["@hot-updater/cloudflare"], + }, } as const; export const init = async () => { @@ -41,7 +46,10 @@ export const init = async () => { const provider = await select({ message: "Select a provider", - options: [{ value: "supabase", label: "Supabase" }], + options: [ + { value: "supabase", label: "Supabase" }, + { value: "cloudflare-d1-r2", label: "Cloudflare D1 + R2" }, + ], }); if (isCancel(provider)) { @@ -65,6 +73,9 @@ export const init = async () => { case "supabase": await initSupabase(); break; + case "cloudflare-d1-r2": + await initCloudflareD1R2(); + break; default: throw new Error("Invalid provider"); } diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2.ts new file mode 100644 index 00000000..ce9a184b --- /dev/null +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2.ts @@ -0,0 +1,3 @@ +export const initCloudflareD1R2 = async () => { + console.log("initCloudflareD1R2"); +}; From 2081721a94a42d01d793d06673f8b8daba476723 Mon Sep 17 00:00:00 2001 From: gronxb Date: Wed, 22 Jan 2025 01:07:24 +0900 Subject: [PATCH 02/61] feat: d2 + r2 list --- packages/hot-updater/src/commands/init.ts | 3 +- .../src/commands/init/cloudflareD1R2.ts | 3 - .../src/commands/init/cloudflareD1R2/index.ts | 59 +++++++++++++++++++ .../init/cloudflareD1R2/parseR2Output.spec.ts | 52 ++++++++++++++++ .../init/cloudflareD1R2/parseR2Output.ts | 20 +++++++ 5 files changed, 133 insertions(+), 4 deletions(-) delete mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2.ts create mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts create mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts create mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts diff --git a/packages/hot-updater/src/commands/init.ts b/packages/hot-updater/src/commands/init.ts index 0a03ba8f..c6152a4b 100644 --- a/packages/hot-updater/src/commands/init.ts +++ b/packages/hot-updater/src/commands/init.ts @@ -20,7 +20,8 @@ const PACKAGE_MAP = { }, "cloudflare-d1-r2": { dependencies: [], - devDependencies: ["@hot-updater/cloudflare"], + devDependencies: [], + // devDependencies: ["@hot-updater/cloudflare"], }, } as const; diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2.ts deleted file mode 100644 index ce9a184b..00000000 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const initCloudflareD1R2 = async () => { - console.log("initCloudflareD1R2"); -}; diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts new file mode 100644 index 00000000..6ca1e230 --- /dev/null +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts @@ -0,0 +1,59 @@ +import * as p from "@clack/prompts"; +import { execa } from "execa"; +import { parseR2Output } from "./parseR2Output"; + +const d1 = async (command: "list" | "create", ...args: string[]) => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "d1", command, "--json"], + {}, + ); + if (!stdout) { + throw new Error(`Failed to run 'wrangler ${args}'`); + } + return JSON.parse(stdout); +}; + +/** + * + * + ❯ npx wrangler r2 bucket list + + ⛅️ wrangler 3.103.2 +-------------------- + +Listing buckets... +name: bundle +creation_date: 2025-01-21T15:55:24.480Z + */ +const r2Bucket = async (command: "list" | "create", ...args: string[]) => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "r2", "bucket", command], + {}, + ); + if (!stdout) { + throw new Error(`Failed to run 'wrangler ${args}'`); + } + + return parseR2Output(stdout); +}; + +export const initCloudflareD1R2 = async () => { + p.tasks([ + { + title: "Checking D1 List...", + task: async () => { + const d1List = await d1("list"); + p.log.info(`D1 List: ${JSON.stringify(d1List, null, 2)}`); + }, + }, + { + title: "Checking R2 List...", + task: async () => { + const r2List = await r2Bucket("list"); + p.log.info(`R2 List: ${JSON.stringify(r2List, null, 2)}`); + }, + }, + ]); +}; diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts new file mode 100644 index 00000000..7cfa30fa --- /dev/null +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { parseR2Output } from "./parseR2Output"; + +describe("cloudflareD1R2", () => { + it("should return empty array when no buckets exist", async () => { + const result = await parseR2Output(` +⛅️ wrangler 3.103.2 +-------------------- + +Listing buckets...`); + expect(result).toEqual([]); + }); + + it("should correctly parse when one bucket exists", async () => { + const result = await parseR2Output(` + ⛅️ wrangler 3.103.2 +-------------------- + +Listing buckets... +name: bundles +creation_date: 2025-01-21T15:55:24.480Z`); + expect(result).toEqual([ + { + name: "bundles", + creation_date: "2025-01-21T15:55:24.480Z", + }, + ]); + }); + + it("should correctly parse all buckets when multiple exist", async () => { + const result = await parseR2Output(` + ⛅️ wrangler 3.103.2 +-------------------- + +Listing buckets... +name: bundles2 +creation_date: 2025-01-21T15:59:57.183Z + +name: bundles +creation_date: 2025-01-21T15:55:24.480Z`); + expect(result).toEqual([ + { + name: "bundles2", + creation_date: "2025-01-21T15:59:57.183Z", + }, + { + name: "bundles", + creation_date: "2025-01-21T15:55:24.480Z", + }, + ]); + }); +}); diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts new file mode 100644 index 00000000..c5042e86 --- /dev/null +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts @@ -0,0 +1,20 @@ +export const parseR2Output = ( + str: string, +): { name: string; creation_date: string }[] => { + // Use regex to match bucket information at once + const bucketRegex = /name:\s+(.+)\s*\ncreation_date:\s+(.+)/g; + const buckets: { name: string; creation_date: string }[] = []; + + // Process all matches + const matches = str.matchAll(bucketRegex); + for (const match of matches) { + if (match[1] && match[2]) { + buckets.push({ + name: match[1].trim(), + creation_date: match[2].trim(), + }); + } + } + + return buckets; +}; From e5a6954ac1080ba43c289672a21bade93ec451a6 Mon Sep 17 00:00:00 2001 From: gronxb Date: Wed, 22 Jan 2025 22:58:01 +0900 Subject: [PATCH 03/61] fix: folder --- packages/hot-updater/src/commands/init.ts | 15 +++++++++------ .../index.ts | 2 +- .../parseR2Output.spec.ts | 0 .../parseR2Output.ts | 0 packages/hot-updater/src/commands/init/index.ts | 0 .../init/{initSupabase.ts => supabase/index.ts} | 0 6 files changed, 10 insertions(+), 7 deletions(-) rename packages/hot-updater/src/commands/init/{cloudflareD1R2 => cloudflareD1R2Worker}/index.ts (95%) rename packages/hot-updater/src/commands/init/{cloudflareD1R2 => cloudflareD1R2Worker}/parseR2Output.spec.ts (100%) rename packages/hot-updater/src/commands/init/{cloudflareD1R2 => cloudflareD1R2Worker}/parseR2Output.ts (100%) delete mode 100644 packages/hot-updater/src/commands/init/index.ts rename packages/hot-updater/src/commands/init/{initSupabase.ts => supabase/index.ts} (100%) diff --git a/packages/hot-updater/src/commands/init.ts b/packages/hot-updater/src/commands/init.ts index 2882115d..7b0e7055 100644 --- a/packages/hot-updater/src/commands/init.ts +++ b/packages/hot-updater/src/commands/init.ts @@ -1,8 +1,8 @@ import { printBanner } from "@/components/banner"; import { ensureInstallPackages } from "@/utils/ensureInstallPackages"; import { isCancel, select } from "@clack/prompts"; -import { initCloudflareD1R2 } from "./init/cloudflareD1R2"; -import { initSupabase } from "./init/initSupabase"; +import { initCloudflareD1R2Worker } from "./init/cloudflareD1R2Worker"; +import { initSupabase } from "./init/supabase"; const REQUIRED_PACKAGES = { dependencies: ["@hot-updater/react-native"], @@ -18,7 +18,7 @@ const PACKAGE_MAP = { dependencies: [], devDependencies: ["@hot-updater/aws"], }, - "cloudflare-d1-r2": { + "cloudflare-d1-r2-worker": { dependencies: [], devDependencies: [], // devDependencies: ["@hot-updater/cloudflare"], @@ -49,7 +49,10 @@ export const init = async () => { message: "Select a provider", options: [ { value: "supabase", label: "Supabase" }, - { value: "cloudflare-d1-r2", label: "Cloudflare D1 + R2" }, + { + value: "cloudflare-d1-r2-worker", + label: "Cloudflare D1 + R2 + Worker", + }, ], }); @@ -74,8 +77,8 @@ export const init = async () => { case "supabase": await initSupabase(); break; - case "cloudflare-d1-r2": - await initCloudflareD1R2(); + case "cloudflare-d1-r2-worker": + await initCloudflareD1R2Worker(); break; default: throw new Error("Invalid provider"); diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts similarity index 95% rename from packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts rename to packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 6ca1e230..96c1bcfb 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -39,7 +39,7 @@ const r2Bucket = async (command: "list" | "create", ...args: string[]) => { return parseR2Output(stdout); }; -export const initCloudflareD1R2 = async () => { +export const initCloudflareD1R2Worker = async () => { p.tasks([ { title: "Checking D1 List...", diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/parseR2Output.spec.ts similarity index 100% rename from packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.spec.ts rename to packages/hot-updater/src/commands/init/cloudflareD1R2Worker/parseR2Output.spec.ts diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/parseR2Output.ts similarity index 100% rename from packages/hot-updater/src/commands/init/cloudflareD1R2/parseR2Output.ts rename to packages/hot-updater/src/commands/init/cloudflareD1R2Worker/parseR2Output.ts diff --git a/packages/hot-updater/src/commands/init/index.ts b/packages/hot-updater/src/commands/init/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/hot-updater/src/commands/init/initSupabase.ts b/packages/hot-updater/src/commands/init/supabase/index.ts similarity index 100% rename from packages/hot-updater/src/commands/init/initSupabase.ts rename to packages/hot-updater/src/commands/init/supabase/index.ts From 60fe8f1cf1dfe4c10d2714f9ec1fab3195d851ec Mon Sep 17 00:00:00 2001 From: gronxb Date: Wed, 22 Jan 2025 23:14:03 +0900 Subject: [PATCH 04/61] feat: make r2 + d1 --- .../init/cloudflareD1R2Worker/index.ts | 165 +++++++++++++----- 1 file changed, 118 insertions(+), 47 deletions(-) diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 96c1bcfb..a9ede7cb 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -2,58 +2,129 @@ import * as p from "@clack/prompts"; import { execa } from "execa"; import { parseR2Output } from "./parseR2Output"; -const d1 = async (command: "list" | "create", ...args: string[]) => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "d1", command, "--json"], - {}, - ); - if (!stdout) { - throw new Error(`Failed to run 'wrangler ${args}'`); - } - return JSON.parse(stdout); -}; +const cloudflareApi = { + getR2List: async () => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "r2", "bucket", "list"], + {}, + ); + if (!stdout) { + throw new Error(`Failed to run 'wrangler r2 bucket list'`); + } -/** - * - * - ❯ npx wrangler r2 bucket list - - ⛅️ wrangler 3.103.2 --------------------- - -Listing buckets... -name: bundle -creation_date: 2025-01-21T15:55:24.480Z - */ -const r2Bucket = async (command: "list" | "create", ...args: string[]) => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "r2", "bucket", command], - {}, - ); - if (!stdout) { - throw new Error(`Failed to run 'wrangler ${args}'`); - } + return parseR2Output(stdout); + }, + createR2Bucket: async (bucketName: string) => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "r2", "bucket", "create", bucketName], + {}, + ); + if (!stdout) { + throw new Error( + `Failed to run 'wrangler r2 bucket create ${bucketName}'`, + ); + } - return parseR2Output(stdout); + return true; + }, + getD1List: async () => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "d1", "list", "--json"], + {}, + ); + return JSON.parse(stdout) as { + uuid: string; + name: string; + created_at: string; + version: string; + num_tables: number | null; + file_size: number | null; + }[]; + }, + createD1Database: async (databaseName: string) => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "d1", "create", databaseName], + {}, + ); + return stdout; + }, }; export const initCloudflareD1R2Worker = async () => { - p.tasks([ - { - title: "Checking D1 List...", - task: async () => { - const d1List = await d1("list"); - p.log.info(`D1 List: ${JSON.stringify(d1List, null, 2)}`); + const s = p.spinner(); + const createKey = `create/${Math.random().toString(36).substring(2, 15)}`; + + s.start("Checking R2 List..."); + const r2List = await cloudflareApi.getR2List(); + s.stop(); + + const selectedR2 = await p.select({ + message: "R2 List", + options: [ + ...r2List.map((r2) => ({ + value: r2.name, + label: r2.name, + })), + { + value: createKey, + label: "Create New R2 Bucket", }, - }, - { - title: "Checking R2 List...", - task: async () => { - const r2List = await r2Bucket("list"); - p.log.info(`R2 List: ${JSON.stringify(r2List, null, 2)}`); + ], + }); + + if (p.isCancel(selectedR2)) { + process.exit(1); + } + + if (selectedR2 === createKey) { + const name = await p.text({ + message: "Enter the name of the new R2 Bucket", + }); + if (p.isCancel(name)) { + process.exit(1); + } + const newR2 = await cloudflareApi.createR2Bucket(name); + p.log.info(`Created new R2 Bucket: ${newR2}`); + } else { + p.log.info(`Selected R2: ${selectedR2}`); + } + + s.start("Checking D1 List..."); + const d1List = await cloudflareApi.getD1List(); + s.stop(); + + const selectedD1 = await p.select({ + message: "D1 List", + options: [ + ...d1List.map((d1) => ({ + value: d1.name, + label: d1.name, + })), + { + value: createKey, + label: "Create New D1 Database", }, - }, - ]); + ], + }); + + if (p.isCancel(selectedD1)) { + process.exit(1); + } + + if (selectedD1 === createKey) { + const name = await p.text({ + message: "Enter the name of the new D1 Database", + }); + if (p.isCancel(name)) { + process.exit(1); + } + const newD1 = await cloudflareApi.createD1Database(name); + p.log.info(`Created new D1 Database: ${newD1}`); + } else { + p.log.info(`Selected D1: ${selectedD1}`); + } }; From e0c2ca8fa9e96862c271747d0d69fdf314b67655 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 23 Jan 2025 09:15:45 +0900 Subject: [PATCH 05/61] fix: move deps --- plugins/supabase/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/supabase/package.json b/plugins/supabase/package.json index f9f15a1c..ffc24e0e 100644 --- a/plugins/supabase/package.json +++ b/plugins/supabase/package.json @@ -36,10 +36,10 @@ "dependencies": { "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10", - "@supabase/supabase-js": "^2.47.10", - "picocolors": "^1.0.0" + "@supabase/supabase-js": "^2.47.10" }, "devDependencies": { + "picocolors": "^1.0.0", "@hot-updater/postgres": "0.5.10", "dayjs": "^1.11.13", "mime": "^4.0.4" From 68e76459ca97b6d83485c620c8d54dd744ea2b3f Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 23 Jan 2025 09:16:00 +0900 Subject: [PATCH 06/61] chore: init proj --- .../init/cloudflareD1R2Worker/index.ts | 8 + plugins/cloudflare/.gitignore | 6 + plugins/cloudflare/package.json | 44 ++++ plugins/cloudflare/rslib.config.ts | 14 ++ plugins/cloudflare/scripts/make-migrations.ts | 140 +++++++++++ plugins/cloudflare/src/d1Database.ts | 112 +++++++++ plugins/cloudflare/src/index.ts | 3 + plugins/cloudflare/src/r2Storage.ts | 65 +++++ plugins/cloudflare/src/types.ts | 33 +++ plugins/cloudflare/src/utils/index.ts | 2 + plugins/cloudflare/src/utils/supabaseApi.ts | 50 ++++ plugins/cloudflare/src/utils/templates.ts | 6 + .../supabase/functions/update-server/index.ts | 57 +++++ .../migrations/20250103114225_init.sql | 228 ++++++++++++++++++ plugins/cloudflare/tsconfig.json | 5 + pnpm-lock.yaml | 25 +- 16 files changed, 795 insertions(+), 3 deletions(-) create mode 100644 plugins/cloudflare/.gitignore create mode 100644 plugins/cloudflare/package.json create mode 100644 plugins/cloudflare/rslib.config.ts create mode 100644 plugins/cloudflare/scripts/make-migrations.ts create mode 100644 plugins/cloudflare/src/d1Database.ts create mode 100644 plugins/cloudflare/src/index.ts create mode 100644 plugins/cloudflare/src/r2Storage.ts create mode 100644 plugins/cloudflare/src/types.ts create mode 100644 plugins/cloudflare/src/utils/index.ts create mode 100644 plugins/cloudflare/src/utils/supabaseApi.ts create mode 100644 plugins/cloudflare/src/utils/templates.ts create mode 100644 plugins/cloudflare/supabase/functions/update-server/index.ts create mode 100644 plugins/cloudflare/supabase/migrations/20250103114225_init.sql create mode 100644 plugins/cloudflare/tsconfig.json diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index a9ede7cb..365251b2 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -52,6 +52,14 @@ const cloudflareApi = { ); return stdout; }, + getR2AccessTokens: async () => { + const { stdout } = await execa( + "npx", + ["-y", "wrangler", "r2", "bucket", "access-tokens", "list"], + {}, + ); + return stdout; + }, }; export const initCloudflareD1R2Worker = async () => { diff --git a/plugins/cloudflare/.gitignore b/plugins/cloudflare/.gitignore new file mode 100644 index 00000000..c006352d --- /dev/null +++ b/plugins/cloudflare/.gitignore @@ -0,0 +1,6 @@ +# Supabase +.branches +.temp +.env + +supabase/config.toml \ No newline at end of file diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json new file mode 100644 index 00000000..87ac7483 --- /dev/null +++ b/plugins/cloudflare/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hot-updater/cloudflare", + "type": "module", + "version": "0.5.10", + "description": "React Native OTA solution for self-hosted", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "license": "MIT", + "repository": "https://github.com/gronxb/hot-updater", + "author": "gronxb (https://github.com/gronxb)", + "bugs": { + "url": "https://github.com/gronxb/hot-updater/issues" + }, + "homepage": "https://github.com/gronxb/hot-updater#readme", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "sql", + "package.json" + ], + "scripts": { + "build": "rslib build", + "test:type": "tsc --noEmit", + "make-migrations": "node --experimental-strip-types ./scripts/make-migrations.ts" + }, + "dependencies": { + "@hot-updater/core": "0.5.10", + "@hot-updater/plugin-core": "0.5.10", + "picocolors": "^1.0.0" + }, + "devDependencies": { + "dayjs": "^1.11.13", + "mime": "^4.0.4" + } +} diff --git a/plugins/cloudflare/rslib.config.ts b/plugins/cloudflare/rslib.config.ts new file mode 100644 index 00000000..cd505720 --- /dev/null +++ b/plugins/cloudflare/rslib.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "@rslib/core"; + +export default defineConfig({ + lib: [ + { + format: "esm", + dts: true, + }, + { + format: "cjs", + dts: true, + }, + ], +}); diff --git a/plugins/cloudflare/scripts/make-migrations.ts b/plugins/cloudflare/scripts/make-migrations.ts new file mode 100644 index 00000000..f74a31a8 --- /dev/null +++ b/plugins/cloudflare/scripts/make-migrations.ts @@ -0,0 +1,140 @@ +import path from "path"; +import dayjs from "dayjs"; +import fs from "fs/promises"; +import pc from "picocolors"; + +/** + * Find markers in the format '-- HotUpdater.xxxx' from content and + * extract SQL statements from each marker to the next marker into a Map + * @param {string} content - SQL file content + * @returns {Map} - key is HotUpdater marker, value is the block + */ +function parseHotUpdaterContents(content: string) { + const markerRegex = /--\s*HotUpdater\.[^\n]*/g; + const resultMap = new Map(); + + // Extract HotUpdater markers + const markers = content.match(markerRegex); + if (!markers) return resultMap; + + // Find blocks for each marker and store in Map + for (const marker of markers) { + const key = marker.trim(); + // Regex: Extract statement from marker(key) to next HotUpdater marker + const blockRegex = new RegExp(`${key}[\\s\\S]*?(?=--\\s*HotUpdater\\.|$)`); + const matchBlock = content.match(blockRegex); + if (matchBlock) { + resultMap.set(key, matchBlock[0].trim()); + } + } + + return resultMap; +} + +/** + * Read all .sql files from a directory, extract HotUpdater blocks + * and combine them into a single Map + * @param {string} dirPath - Migration directory path + * @returns {Promise>} - key: HotUpdater marker, value: block + */ +async function readHotUpdaterBlocksFromDir(dirPath: string) { + const files = (await fs.readdir(dirPath)).sort((a, b) => a.localeCompare(b)); + const migrationMap = new Map(); + + for (const file of files) { + if (!file.endsWith(".sql")) continue; + + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, "utf-8"); + // Extract HotUpdater blocks from file content + const parsedBlocks = parseHotUpdaterContents(content); + + // Store all extracted blocks in Map + for (const [key, block] of parsedBlocks.entries()) { + migrationMap.set(key, block); + } + } + + return migrationMap; +} + +/** + * Extract HotUpdater blocks from new SQL files (@hot-updater/postgres/sql) into a Map + * @param {string} dirPath - @hot-updater/postgres/sql directory path + * @returns {Promise>} + */ +async function readNewMigrations(dirPath: string) { + const files = await fs.readdir(dirPath); + const newMigrationMap = new Map(); + + // Select .sql files and extract HotUpdater blocks + for (const file of files) { + if (!file.endsWith(".sql")) continue; + + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, "utf-8"); + const parsedBlocks = parseHotUpdaterContents(content); + + for (const [key, block] of parsedBlocks.entries()) { + newMigrationMap.set(key, block); + } + } + + return newMigrationMap; +} + +/** + * Main function responsible for actual migration file creation + */ +async function main() { + // @hot-updater/postgres/sql path + const postgresPath = import.meta + .resolve("@hot-updater/postgres/sql") + .replace("file://", ""); + + // Create migrations directory (skip if exists) + const migrationsDir = path.join(process.cwd(), "supabase/migrations"); + await fs.mkdir(migrationsDir, { recursive: true }); + + // Extract all HotUpdater blocks from existing migration .sql files + const existingMigrations = await readHotUpdaterBlocksFromDir(migrationsDir); + console.log( + pc.blue("Existing migration contents:"), + Array.from(existingMigrations.keys()), + ); + + // Extract all HotUpdater blocks from new SQL files + const newMigrations = await readNewMigrations(postgresPath); + console.log( + pc.blue("New migration contents:"), + Array.from(newMigrations.keys()), + ); + + // Collect blocks that differ from existing ones + const changedBlocks: string[] = []; + for (const [key, block] of newMigrations.entries()) { + const existingBlock = existingMigrations.get(key); + if (existingBlock !== block) { + changedBlocks.push(block); + } + } + + // Create new migration file if there are changed blocks + if (changedBlocks.length > 0) { + const combinedSql = changedBlocks.join("\n\n"); + const newFileName = `${dayjs().format("YYYYMMDDHHmmss")}.sql`; + + await fs.writeFile(path.join(migrationsDir, newFileName), combinedSql); + console.log(pc.green("New migration file created:"), pc.bold(newFileName)); + } else { + console.log( + pc.yellow("No changes detected. No new migration file created."), + ); + } +} + +// Execute main function +main().catch((err) => { + console.error(pc.red("Error during migration creation:"), err); + process.exit(1); +}); diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts new file mode 100644 index 00000000..d181d8ce --- /dev/null +++ b/plugins/cloudflare/src/d1Database.ts @@ -0,0 +1,112 @@ +import type { + BasePluginArgs, + Bundle, + DatabasePlugin, + DatabasePluginHooks, +} from "@hot-updater/plugin-core"; + +import { createClient } from "@supabase/supabase-js"; +import type { Database } from "./types"; + +export interface SupabaseDatabaseConfig { + supabaseUrl: string; + supabaseAnonKey: string; +} + +export const d1Database = + (config: SupabaseDatabaseConfig, hooks?: DatabasePluginHooks) => + (_: BasePluginArgs): DatabasePlugin => { + const supabase = createClient( + config.supabaseUrl, + config.supabaseAnonKey, + ); + + let bundles: Bundle[] = []; + + return { + name: "supabaseDatabase", + async commitBundle() { + await supabase.from("bundles").upsert( + bundles.map((bundle) => ({ + id: bundle.id, + enabled: bundle.enabled, + file_url: bundle.fileUrl, + should_force_update: bundle.shouldForceUpdate, + file_hash: bundle.fileHash, + git_commit_hash: bundle.gitCommitHash, + message: bundle.message, + platform: bundle.platform, + target_app_version: bundle.targetAppVersion, + })), + { onConflict: "id" }, + ); + + hooks?.onDatabaseUpdated?.(); + }, + async updateBundle(targetBundleId: string, newBundle: Partial) { + bundles = await this.getBundles(); + + const targetIndex = bundles.findIndex((u) => u.id === targetBundleId); + if (targetIndex === -1) { + throw new Error("target bundle version not found"); + } + + Object.assign(bundles[targetIndex], newBundle); + }, + async appendBundle(inputBundle) { + bundles = await this.getBundles(); + bundles.unshift(inputBundle); + }, + async setBundles(inputBundles) { + bundles = inputBundles; + }, + async getBundleById(bundleId) { + const { data } = await supabase + .from("bundles") + .select("*") + .eq("id", bundleId) + .single(); + + if (!data) { + return null; + } + return { + enabled: data.enabled, + fileUrl: data.file_url, + shouldForceUpdate: data.should_force_update, + fileHash: data.file_hash, + gitCommitHash: data.git_commit_hash, + id: data.id, + message: data.message, + platform: data.platform, + targetAppVersion: data.target_app_version, + } as Bundle; + }, + async getBundles(refresh = false) { + if (bundles.length > 0 && !refresh) { + return bundles; + } + + const { data } = await supabase + .from("bundles") + .select("*") + .order("id", { ascending: false }); + + if (!data) { + return []; + } + + return data.map((bundle) => ({ + enabled: bundle.enabled, + fileUrl: bundle.file_url, + shouldForceUpdate: bundle.should_force_update, + fileHash: bundle.file_hash, + gitCommitHash: bundle.git_commit_hash, + id: bundle.id, + message: bundle.message, + platform: bundle.platform, + targetAppVersion: bundle.target_app_version, + })) as Bundle[]; + }, + }; + }; diff --git a/plugins/cloudflare/src/index.ts b/plugins/cloudflare/src/index.ts new file mode 100644 index 00000000..8ff3c66b --- /dev/null +++ b/plugins/cloudflare/src/index.ts @@ -0,0 +1,3 @@ +export * from "./d1Database"; +export * from "./r2Storage"; +export * from "./utils"; diff --git a/plugins/cloudflare/src/r2Storage.ts b/plugins/cloudflare/src/r2Storage.ts new file mode 100644 index 00000000..41dbd97e --- /dev/null +++ b/plugins/cloudflare/src/r2Storage.ts @@ -0,0 +1,65 @@ +import path from "path"; +import type { + BasePluginArgs, + StoragePlugin, + StoragePluginHooks, +} from "@hot-updater/plugin-core"; +import { createClient } from "@supabase/supabase-js"; +import fs from "fs/promises"; +import mime from "mime"; +import type { Database } from "./types"; + +export interface SupabaseStorageConfig { + supabaseUrl: string; + supabaseAnonKey: string; + bucketName: string; +} + +export const r2Storage = + (config: SupabaseStorageConfig, hooks?: StoragePluginHooks) => + (_: BasePluginArgs): StoragePlugin => { + const supabase = createClient( + config.supabaseUrl, + config.supabaseAnonKey, + ); + + const bucket = supabase.storage.from(config.bucketName); + return { + name: "supabaseStorage", + async deleteBundle(bundleId) { + const Key = [bundleId].join("/"); + + await bucket.remove([Key]); + return Key; + }, + async uploadBundle(bundleId, bundlePath) { + const Body = await fs.readFile(bundlePath); + const ContentType = mime.getType(bundlePath) ?? void 0; + + const filename = path.basename(bundlePath); + + const Key = [bundleId, filename].join("/"); + + const upload = await bucket.upload(Key, Body, { + contentType: ContentType, + }); + + const fullPath = upload.data?.fullPath; + if (!fullPath) { + throw new Error( + "Upload failed. The Supabase key might be incorrect. Please verify the key using the `hot-updater get-plugin-env` command.", + ); + } + + hooks?.onStorageUploaded?.(); + + const fileUrl = new URL( + `storage/v1/object/public/${fullPath}`, + config.supabaseUrl, + ).toString(); + return { + fileUrl: hooks?.transformFileUrl?.(fullPath) ?? fileUrl, + }; + }, + }; + }; diff --git a/plugins/cloudflare/src/types.ts b/plugins/cloudflare/src/types.ts new file mode 100644 index 00000000..dd0c80db --- /dev/null +++ b/plugins/cloudflare/src/types.ts @@ -0,0 +1,33 @@ +import type { Bundle } from "@hot-updater/core"; + +type SnakeCase = S extends `${infer T}${infer U}` + ? `${T extends Capitalize ? "_" : ""}${Lowercase}${SnakeCase}` + : S; + +// Utility type to recursively map object keys to snake_case +type SnakeKeyObject = T extends Record + ? { + [K in keyof T as SnakeCase>]: T[K] extends object + ? SnakeKeyObject + : T[K]; + } + : T; + +export type BundlesTable = SnakeKeyObject; + +export type Database = { + public: { + Tables: { + bundles: { + Row: BundlesTable; + Insert: BundlesTable; + Update: BundlesTable; + Relationships: []; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: any; + }; +}; diff --git a/plugins/cloudflare/src/utils/index.ts b/plugins/cloudflare/src/utils/index.ts new file mode 100644 index 00000000..b8c1bf35 --- /dev/null +++ b/plugins/cloudflare/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./supabaseApi"; +export * from "./templates"; diff --git a/plugins/cloudflare/src/utils/supabaseApi.ts b/plugins/cloudflare/src/utils/supabaseApi.ts new file mode 100644 index 00000000..1fc0ad93 --- /dev/null +++ b/plugins/cloudflare/src/utils/supabaseApi.ts @@ -0,0 +1,50 @@ +import { createClient } from "@supabase/supabase-js"; + +export interface SupabaseApi { + listBuckets: () => Promise< + { + id: string; + name: string; + isPublic: boolean; + createdAt: string; + }[] + >; + createBucket: ( + bucketName: string, + options: { public: boolean }, + ) => Promise<{ + name: string; + }>; +} + +export const supabaseApi = ( + supabaseUrl: string, + supabaseAnonKey: string, +): SupabaseApi => { + const supabase = createClient(supabaseUrl, supabaseAnonKey); + + return { + listBuckets: async () => { + const { data, error } = await supabase.storage.listBuckets(); + if (error) { + throw error; + } + return data.map((file) => ({ + id: file.id, + name: file.name, + isPublic: file.public, + createdAt: file.created_at, + })); + }, + createBucket: async (bucketName, options) => { + const { data, error } = await supabase.storage.createBucket( + bucketName, + options, + ); + if (error) { + throw error; + } + return data; + }, + }; +}; diff --git a/plugins/cloudflare/src/utils/templates.ts b/plugins/cloudflare/src/utils/templates.ts new file mode 100644 index 00000000..576fff24 --- /dev/null +++ b/plugins/cloudflare/src/utils/templates.ts @@ -0,0 +1,6 @@ +export const supabaseConfigTomlTemplate = ` +project_id = "%%projectId%%" + +[db.seed] +enabled = false +`; diff --git a/plugins/cloudflare/supabase/functions/update-server/index.ts b/plugins/cloudflare/supabase/functions/update-server/index.ts new file mode 100644 index 00000000..cd38c240 --- /dev/null +++ b/plugins/cloudflare/supabase/functions/update-server/index.ts @@ -0,0 +1,57 @@ +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import camelcaseKeys from "npm:camelcase-keys@9.1.3"; +import { createClient } from "jsr:@supabase/supabase-js@2.47.10"; + +// 에러 응답 생성 함수 +const createErrorResponse = (message: string, statusCode: number) => { + return new Response(JSON.stringify({ code: statusCode, message }), { + headers: { "Content-Type": "application/json" }, + status: statusCode, + }); +}; + +Deno.serve(async (req) => { + try { + // Supabase 클라이언트 초기화 + const supabase = createClient( + Deno.env.get("SUPABASE_URL") ?? "", + Deno.env.get("SUPABASE_ANON_KEY") ?? "", + { + global: { + headers: { Authorization: req.headers.get("Authorization")! }, + }, + }, + ); + + // 요청 헤더에서 필요한 정보 추출 + const bundleId = req.headers.get("x-bundle-id") as string; + const appPlatform = req.headers.get("x-app-platform") as "ios" | "android"; + const appVersion = req.headers.get("x-app-version") as string; + + // 필수 헤더 검증 + if (!bundleId || !appPlatform || !appVersion) { + return createErrorResponse( + "Missing bundleId, appPlatform, or appVersion", + 400, + ); + } + + const { data, error } = await supabase.rpc("get_update_info", { + app_platform: appPlatform, + app_version: appVersion, + bundle_id: bundleId, + }); + + if (error) { + throw error; + } + + const response = data[0] ? camelcaseKeys(data[0]) : null; + return new Response(JSON.stringify(response), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + } catch (err: unknown) { + return createErrorResponse(JSON.stringify(err), 500); + } +}); diff --git a/plugins/cloudflare/supabase/migrations/20250103114225_init.sql b/plugins/cloudflare/supabase/migrations/20250103114225_init.sql new file mode 100644 index 00000000..4419a030 --- /dev/null +++ b/plugins/cloudflare/supabase/migrations/20250103114225_init.sql @@ -0,0 +1,228 @@ +-- HotUpdater.bundles + +CREATE TYPE platforms AS ENUM ('ios', 'android'); + +CREATE TABLE bundles ( + id uuid PRIMARY KEY, + platform platforms NOT NULL, + target_app_version text NOT NULL, + should_force_update boolean NOT NULL, + enabled boolean NOT NULL, + file_url text NOT NULL, + file_hash text NOT NULL, + git_commit_hash text, + message text +); + +CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version); + + +-- HotUpdater.get_update_info + +CREATE OR REPLACE FUNCTION get_update_info ( + app_platform platforms, + app_version text, + bundle_id uuid +) +RETURNS TABLE ( + id uuid, + should_force_update boolean, + file_url text, + file_hash text, + status text +) +LANGUAGE plpgsql +AS +$$ +DECLARE + NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000'; +BEGIN + RETURN QUERY + WITH rollback_candidate AS ( + SELECT + b.id, + -- If status is 'ROLLBACK', should_force_update is always TRUE + TRUE AS should_force_update, + b.file_url, + b.file_hash, + 'ROLLBACK' AS status + FROM bundles b + WHERE b.enabled = TRUE + AND b.platform = app_platform + AND b.id < bundle_id + ORDER BY b.id DESC + LIMIT 1 + ), + update_candidate AS ( + SELECT + b.id, + b.should_force_update, + b.file_url, + b.file_hash, + 'UPDATE' AS status + FROM bundles b + WHERE b.enabled = TRUE + AND b.platform = app_platform + AND b.id >= bundle_id + AND semver_satisfies(b.target_app_version, app_version) + ORDER BY b.id DESC + LIMIT 1 + ), + final_result AS ( + SELECT * + FROM update_candidate + + UNION ALL + + SELECT * + FROM rollback_candidate + WHERE NOT EXISTS (SELECT 1 FROM update_candidate) + ) + SELECT * + FROM final_result WHERE final_result.id != bundle_id + + UNION ALL + /* + When there are no final results and bundle_id != NIL_UUID, + add one fallback row. + This fallback row is also ROLLBACK so shouldForceUpdate = TRUE. + */ + SELECT + NIL_UUID AS id, + TRUE AS should_force_update, -- Always TRUE + NULL AS file_url, + NULL AS file_hash, + 'ROLLBACK' AS status + WHERE (SELECT COUNT(*) FROM final_result) = 0 + AND bundle_id != NIL_UUID; + +END; +$$; + +-- HotUpdater.semver_satisfies + +CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT) +RETURNS BOOLEAN AS $$ +DECLARE + version_parts TEXT[]; + version_major INT; + version_minor INT; + version_patch INT; + satisfies BOOLEAN := FALSE; +BEGIN + -- Split the version into major, minor, and patch + version_parts := string_to_array(version, '.'); + version_major := version_parts[1]::INT; + version_minor := version_parts[2]::INT; + version_patch := version_parts[3]::INT; + + -- Parse range expression and evaluate + IF range_expression ~ '^\d+\.\d+\.\d+$' THEN + -- Exact match + satisfies := (range_expression = version); + + ELSIF range_expression = '*' THEN + -- Matches any version + satisfies := TRUE; + + ELSIF range_expression ~ '^\d+\.x\.x$' THEN + -- Matches major.x.x + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + BEGIN + satisfies := (version_major = major_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN + -- Matches major.minor.x + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + minor_range INT := split_part(range_expression, '.', 2)::INT; + BEGIN + satisfies := (version_major = major_range AND version_minor = minor_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+$' THEN + -- Matches major.minor + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + minor_range INT := split_part(range_expression, '.', 2)::INT; + BEGIN + satisfies := (version_major = major_range AND version_minor = minor_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN + -- Matches range e.g., 1.2.3 - 1.2.7 + DECLARE + lower_bound TEXT := split_part(range_expression, ' - ', 1); + upper_bound TEXT := split_part(range_expression, ' - ', 2); + BEGIN + satisfies := (version >= lower_bound AND version <= upper_bound); + END; + + ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN + -- Matches range with inequalities + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1'); + upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1'); + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN + -- Matches ~1.2.3 (>=1.2.3 <1.3.0) + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '~', ''); + upper_bound_major INT := split_part(lower_bound, '.', 1)::INT; + upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1; + upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN + -- Matches ^1.2.3 (>=1.2.3 <2.0.0) + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '\^', ''); + upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1; + upper_bound TEXT := upper_bound_major || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + -- [Added] 1) Single major version pattern '^(\d+)$' + ELSIF range_expression ~ '^\d+$' THEN + /* + e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range + "2" would be interpreted as (>=2.0.0 <3.0.0) + */ + DECLARE + major_range INT := range_expression::INT; + lower_bound TEXT := major_range || '.0.0'; + upper_bound TEXT := (major_range + 1) || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + -- [Added] 2) major.x pattern '^(\d+)\.x$' + ELSIF range_expression ~ '^\d+\.x$' THEN + /* + e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK + effectively works like (>=2.0.0 <3.0.0) + */ + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + lower_bound TEXT := major_range || '.0.0'; + upper_bound TEXT := (major_range + 1) || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSE + RAISE EXCEPTION 'Unsupported range expression: %', range_expression; + END IF; + + RETURN satisfies; +END; +$$ LANGUAGE plpgsql; + diff --git a/plugins/cloudflare/tsconfig.json b/plugins/cloudflare/tsconfig.json new file mode 100644 index 00000000..7ccc5491 --- /dev/null +++ b/plugins/cloudflare/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "exclude": ["lib", "supabase"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ffad857..dc1c7fa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,6 +574,25 @@ importers: specifier: ^4.0.4 version: 4.0.4 + plugins/cloudflare: + dependencies: + '@hot-updater/core': + specifier: workspace:* + version: link:../../packages/core + '@hot-updater/plugin-core': + specifier: workspace:* + version: link:../plugin-core + picocolors: + specifier: ^1.0.0 + version: 1.1.1 + devDependencies: + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + mime: + specifier: ^4.0.4 + version: 4.0.4 + plugins/js: dependencies: '@hot-updater/core': @@ -664,9 +683,6 @@ importers: '@supabase/supabase-js': specifier: ^2.47.10 version: 2.47.10 - picocolors: - specifier: ^1.0.0 - version: 1.1.1 devDependencies: '@hot-updater/postgres': specifier: workspace:* @@ -677,6 +693,9 @@ importers: mime: specifier: ^4.0.4 version: 4.0.4 + picocolors: + specifier: ^1.0.0 + version: 1.1.1 packages: From effa7b34d54993b982b03276972214a61e632c69 Mon Sep 17 00:00:00 2001 From: gronxb Date: Fri, 24 Jan 2025 01:36:31 +0900 Subject: [PATCH 07/61] feat: unused script --- plugins/cloudflare/package.json | 3 +- plugins/cloudflare/scripts/make-migrations.ts | 140 ----------- .../supabase/functions/update-server/index.ts | 57 ----- .../migrations/20250103114225_init.sql | 228 ------------------ 4 files changed, 1 insertion(+), 427 deletions(-) delete mode 100644 plugins/cloudflare/scripts/make-migrations.ts delete mode 100644 plugins/cloudflare/supabase/functions/update-server/index.ts delete mode 100644 plugins/cloudflare/supabase/migrations/20250103114225_init.sql diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 87ac7483..a5af19af 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -29,8 +29,7 @@ ], "scripts": { "build": "rslib build", - "test:type": "tsc --noEmit", - "make-migrations": "node --experimental-strip-types ./scripts/make-migrations.ts" + "test:type": "tsc --noEmit" }, "dependencies": { "@hot-updater/core": "0.5.10", diff --git a/plugins/cloudflare/scripts/make-migrations.ts b/plugins/cloudflare/scripts/make-migrations.ts deleted file mode 100644 index f74a31a8..00000000 --- a/plugins/cloudflare/scripts/make-migrations.ts +++ /dev/null @@ -1,140 +0,0 @@ -import path from "path"; -import dayjs from "dayjs"; -import fs from "fs/promises"; -import pc from "picocolors"; - -/** - * Find markers in the format '-- HotUpdater.xxxx' from content and - * extract SQL statements from each marker to the next marker into a Map - * @param {string} content - SQL file content - * @returns {Map} - key is HotUpdater marker, value is the block - */ -function parseHotUpdaterContents(content: string) { - const markerRegex = /--\s*HotUpdater\.[^\n]*/g; - const resultMap = new Map(); - - // Extract HotUpdater markers - const markers = content.match(markerRegex); - if (!markers) return resultMap; - - // Find blocks for each marker and store in Map - for (const marker of markers) { - const key = marker.trim(); - // Regex: Extract statement from marker(key) to next HotUpdater marker - const blockRegex = new RegExp(`${key}[\\s\\S]*?(?=--\\s*HotUpdater\\.|$)`); - const matchBlock = content.match(blockRegex); - if (matchBlock) { - resultMap.set(key, matchBlock[0].trim()); - } - } - - return resultMap; -} - -/** - * Read all .sql files from a directory, extract HotUpdater blocks - * and combine them into a single Map - * @param {string} dirPath - Migration directory path - * @returns {Promise>} - key: HotUpdater marker, value: block - */ -async function readHotUpdaterBlocksFromDir(dirPath: string) { - const files = (await fs.readdir(dirPath)).sort((a, b) => a.localeCompare(b)); - const migrationMap = new Map(); - - for (const file of files) { - if (!file.endsWith(".sql")) continue; - - const filePath = path.join(dirPath, file); - const content = await fs.readFile(filePath, "utf-8"); - // Extract HotUpdater blocks from file content - const parsedBlocks = parseHotUpdaterContents(content); - - // Store all extracted blocks in Map - for (const [key, block] of parsedBlocks.entries()) { - migrationMap.set(key, block); - } - } - - return migrationMap; -} - -/** - * Extract HotUpdater blocks from new SQL files (@hot-updater/postgres/sql) into a Map - * @param {string} dirPath - @hot-updater/postgres/sql directory path - * @returns {Promise>} - */ -async function readNewMigrations(dirPath: string) { - const files = await fs.readdir(dirPath); - const newMigrationMap = new Map(); - - // Select .sql files and extract HotUpdater blocks - for (const file of files) { - if (!file.endsWith(".sql")) continue; - - const filePath = path.join(dirPath, file); - const content = await fs.readFile(filePath, "utf-8"); - const parsedBlocks = parseHotUpdaterContents(content); - - for (const [key, block] of parsedBlocks.entries()) { - newMigrationMap.set(key, block); - } - } - - return newMigrationMap; -} - -/** - * Main function responsible for actual migration file creation - */ -async function main() { - // @hot-updater/postgres/sql path - const postgresPath = import.meta - .resolve("@hot-updater/postgres/sql") - .replace("file://", ""); - - // Create migrations directory (skip if exists) - const migrationsDir = path.join(process.cwd(), "supabase/migrations"); - await fs.mkdir(migrationsDir, { recursive: true }); - - // Extract all HotUpdater blocks from existing migration .sql files - const existingMigrations = await readHotUpdaterBlocksFromDir(migrationsDir); - console.log( - pc.blue("Existing migration contents:"), - Array.from(existingMigrations.keys()), - ); - - // Extract all HotUpdater blocks from new SQL files - const newMigrations = await readNewMigrations(postgresPath); - console.log( - pc.blue("New migration contents:"), - Array.from(newMigrations.keys()), - ); - - // Collect blocks that differ from existing ones - const changedBlocks: string[] = []; - for (const [key, block] of newMigrations.entries()) { - const existingBlock = existingMigrations.get(key); - if (existingBlock !== block) { - changedBlocks.push(block); - } - } - - // Create new migration file if there are changed blocks - if (changedBlocks.length > 0) { - const combinedSql = changedBlocks.join("\n\n"); - const newFileName = `${dayjs().format("YYYYMMDDHHmmss")}.sql`; - - await fs.writeFile(path.join(migrationsDir, newFileName), combinedSql); - console.log(pc.green("New migration file created:"), pc.bold(newFileName)); - } else { - console.log( - pc.yellow("No changes detected. No new migration file created."), - ); - } -} - -// Execute main function -main().catch((err) => { - console.error(pc.red("Error during migration creation:"), err); - process.exit(1); -}); diff --git a/plugins/cloudflare/supabase/functions/update-server/index.ts b/plugins/cloudflare/supabase/functions/update-server/index.ts deleted file mode 100644 index cd38c240..00000000 --- a/plugins/cloudflare/supabase/functions/update-server/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -import camelcaseKeys from "npm:camelcase-keys@9.1.3"; -import { createClient } from "jsr:@supabase/supabase-js@2.47.10"; - -// 에러 응답 생성 함수 -const createErrorResponse = (message: string, statusCode: number) => { - return new Response(JSON.stringify({ code: statusCode, message }), { - headers: { "Content-Type": "application/json" }, - status: statusCode, - }); -}; - -Deno.serve(async (req) => { - try { - // Supabase 클라이언트 초기화 - const supabase = createClient( - Deno.env.get("SUPABASE_URL") ?? "", - Deno.env.get("SUPABASE_ANON_KEY") ?? "", - { - global: { - headers: { Authorization: req.headers.get("Authorization")! }, - }, - }, - ); - - // 요청 헤더에서 필요한 정보 추출 - const bundleId = req.headers.get("x-bundle-id") as string; - const appPlatform = req.headers.get("x-app-platform") as "ios" | "android"; - const appVersion = req.headers.get("x-app-version") as string; - - // 필수 헤더 검증 - if (!bundleId || !appPlatform || !appVersion) { - return createErrorResponse( - "Missing bundleId, appPlatform, or appVersion", - 400, - ); - } - - const { data, error } = await supabase.rpc("get_update_info", { - app_platform: appPlatform, - app_version: appVersion, - bundle_id: bundleId, - }); - - if (error) { - throw error; - } - - const response = data[0] ? camelcaseKeys(data[0]) : null; - return new Response(JSON.stringify(response), { - headers: { "Content-Type": "application/json" }, - status: 200, - }); - } catch (err: unknown) { - return createErrorResponse(JSON.stringify(err), 500); - } -}); diff --git a/plugins/cloudflare/supabase/migrations/20250103114225_init.sql b/plugins/cloudflare/supabase/migrations/20250103114225_init.sql deleted file mode 100644 index 4419a030..00000000 --- a/plugins/cloudflare/supabase/migrations/20250103114225_init.sql +++ /dev/null @@ -1,228 +0,0 @@ --- HotUpdater.bundles - -CREATE TYPE platforms AS ENUM ('ios', 'android'); - -CREATE TABLE bundles ( - id uuid PRIMARY KEY, - platform platforms NOT NULL, - target_app_version text NOT NULL, - should_force_update boolean NOT NULL, - enabled boolean NOT NULL, - file_url text NOT NULL, - file_hash text NOT NULL, - git_commit_hash text, - message text -); - -CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version); - - --- HotUpdater.get_update_info - -CREATE OR REPLACE FUNCTION get_update_info ( - app_platform platforms, - app_version text, - bundle_id uuid -) -RETURNS TABLE ( - id uuid, - should_force_update boolean, - file_url text, - file_hash text, - status text -) -LANGUAGE plpgsql -AS -$$ -DECLARE - NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000'; -BEGIN - RETURN QUERY - WITH rollback_candidate AS ( - SELECT - b.id, - -- If status is 'ROLLBACK', should_force_update is always TRUE - TRUE AS should_force_update, - b.file_url, - b.file_hash, - 'ROLLBACK' AS status - FROM bundles b - WHERE b.enabled = TRUE - AND b.platform = app_platform - AND b.id < bundle_id - ORDER BY b.id DESC - LIMIT 1 - ), - update_candidate AS ( - SELECT - b.id, - b.should_force_update, - b.file_url, - b.file_hash, - 'UPDATE' AS status - FROM bundles b - WHERE b.enabled = TRUE - AND b.platform = app_platform - AND b.id >= bundle_id - AND semver_satisfies(b.target_app_version, app_version) - ORDER BY b.id DESC - LIMIT 1 - ), - final_result AS ( - SELECT * - FROM update_candidate - - UNION ALL - - SELECT * - FROM rollback_candidate - WHERE NOT EXISTS (SELECT 1 FROM update_candidate) - ) - SELECT * - FROM final_result WHERE final_result.id != bundle_id - - UNION ALL - /* - When there are no final results and bundle_id != NIL_UUID, - add one fallback row. - This fallback row is also ROLLBACK so shouldForceUpdate = TRUE. - */ - SELECT - NIL_UUID AS id, - TRUE AS should_force_update, -- Always TRUE - NULL AS file_url, - NULL AS file_hash, - 'ROLLBACK' AS status - WHERE (SELECT COUNT(*) FROM final_result) = 0 - AND bundle_id != NIL_UUID; - -END; -$$; - --- HotUpdater.semver_satisfies - -CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT) -RETURNS BOOLEAN AS $$ -DECLARE - version_parts TEXT[]; - version_major INT; - version_minor INT; - version_patch INT; - satisfies BOOLEAN := FALSE; -BEGIN - -- Split the version into major, minor, and patch - version_parts := string_to_array(version, '.'); - version_major := version_parts[1]::INT; - version_minor := version_parts[2]::INT; - version_patch := version_parts[3]::INT; - - -- Parse range expression and evaluate - IF range_expression ~ '^\d+\.\d+\.\d+$' THEN - -- Exact match - satisfies := (range_expression = version); - - ELSIF range_expression = '*' THEN - -- Matches any version - satisfies := TRUE; - - ELSIF range_expression ~ '^\d+\.x\.x$' THEN - -- Matches major.x.x - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - BEGIN - satisfies := (version_major = major_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN - -- Matches major.minor.x - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - minor_range INT := split_part(range_expression, '.', 2)::INT; - BEGIN - satisfies := (version_major = major_range AND version_minor = minor_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+$' THEN - -- Matches major.minor - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - minor_range INT := split_part(range_expression, '.', 2)::INT; - BEGIN - satisfies := (version_major = major_range AND version_minor = minor_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN - -- Matches range e.g., 1.2.3 - 1.2.7 - DECLARE - lower_bound TEXT := split_part(range_expression, ' - ', 1); - upper_bound TEXT := split_part(range_expression, ' - ', 2); - BEGIN - satisfies := (version >= lower_bound AND version <= upper_bound); - END; - - ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN - -- Matches range with inequalities - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1'); - upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1'); - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN - -- Matches ~1.2.3 (>=1.2.3 <1.3.0) - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '~', ''); - upper_bound_major INT := split_part(lower_bound, '.', 1)::INT; - upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1; - upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN - -- Matches ^1.2.3 (>=1.2.3 <2.0.0) - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '\^', ''); - upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1; - upper_bound TEXT := upper_bound_major || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - -- [Added] 1) Single major version pattern '^(\d+)$' - ELSIF range_expression ~ '^\d+$' THEN - /* - e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range - "2" would be interpreted as (>=2.0.0 <3.0.0) - */ - DECLARE - major_range INT := range_expression::INT; - lower_bound TEXT := major_range || '.0.0'; - upper_bound TEXT := (major_range + 1) || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - -- [Added] 2) major.x pattern '^(\d+)\.x$' - ELSIF range_expression ~ '^\d+\.x$' THEN - /* - e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK - effectively works like (>=2.0.0 <3.0.0) - */ - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - lower_bound TEXT := major_range || '.0.0'; - upper_bound TEXT := (major_range + 1) || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSE - RAISE EXCEPTION 'Unsupported range expression: %', range_expression; - END IF; - - RETURN satisfies; -END; -$$ LANGUAGE plpgsql; - From df5b4532c0da0e8cb8d70d50fb13f69afe7f91d6 Mon Sep 17 00:00:00 2001 From: gronxb Date: Fri, 24 Jan 2025 01:37:42 +0900 Subject: [PATCH 08/61] fix: chorE --- plugins/cloudflare/package.json | 4 ++-- pnpm-lock.yaml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index a5af19af..b728b737 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@hot-updater/core": "0.5.10", - "@hot-updater/plugin-core": "0.5.10", - "picocolors": "^1.0.0" + "@hot-updater/plugin-core": "0.5.10" }, "devDependencies": { + "picocolors": "^1.0.0", "dayjs": "^1.11.13", "mime": "^4.0.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b59221cc..b559126f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -682,9 +682,6 @@ importers: '@hot-updater/plugin-core': specifier: workspace:* version: link:../plugin-core - picocolors: - specifier: ^1.0.0 - version: 1.1.1 devDependencies: dayjs: specifier: ^1.11.13 @@ -692,6 +689,9 @@ importers: mime: specifier: ^4.0.4 version: 4.0.4 + picocolors: + specifier: ^1.0.0 + version: 1.1.1 plugins/js: dependencies: @@ -14740,9 +14740,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - bufferutil - supports-color - - utf-8-validate '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))': dependencies: @@ -14753,7 +14751,9 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' + - bufferutil - supports-color + - utf-8-validate optional: true '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))': From 9d3ee22d22ac06dcc259d558792d3dea18eee61d Mon Sep 17 00:00:00 2001 From: gronxb Date: Mon, 27 Jan 2025 14:21:43 +0900 Subject: [PATCH 09/61] feat: init worker --- .../init/cloudflareD1R2Worker/index.ts | 2 + plugins/cloudflare/package.json | 13 +- plugins/cloudflare/sql/bundles.sql | 15 + plugins/cloudflare/sql/get_update_info.sql | 81 ++ .../cloudflare/sql/get_update_info.test.ts | 66 ++ plugins/cloudflare/sql/prepareSql.ts | 11 + plugins/cloudflare/sql/semver_satisfies.sql | 126 +++ .../cloudflare/sql/semver_satisfies.test.ts | 28 + plugins/cloudflare/worker/.editorconfig | 12 + plugins/cloudflare/worker/.gitignore | 172 ++++ plugins/cloudflare/worker/.prettierrc | 6 + plugins/cloudflare/worker/src/index.ts | 18 + plugins/cloudflare/worker/test/index.spec.ts | 25 + plugins/cloudflare/worker/test/tsconfig.json | 8 + plugins/cloudflare/worker/tsconfig.json | 46 + plugins/cloudflare/worker/vitest.config.mts | 11 + .../worker/worker-configuration.d.ts | 4 + plugins/cloudflare/worker/wrangler.json | 47 + pnpm-lock.yaml | 826 +++++++++++++++++- 19 files changed, 1496 insertions(+), 21 deletions(-) create mode 100644 plugins/cloudflare/sql/bundles.sql create mode 100644 plugins/cloudflare/sql/get_update_info.sql create mode 100644 plugins/cloudflare/sql/get_update_info.test.ts create mode 100644 plugins/cloudflare/sql/prepareSql.ts create mode 100644 plugins/cloudflare/sql/semver_satisfies.sql create mode 100644 plugins/cloudflare/sql/semver_satisfies.test.ts create mode 100644 plugins/cloudflare/worker/.editorconfig create mode 100644 plugins/cloudflare/worker/.gitignore create mode 100644 plugins/cloudflare/worker/.prettierrc create mode 100644 plugins/cloudflare/worker/src/index.ts create mode 100644 plugins/cloudflare/worker/test/index.spec.ts create mode 100644 plugins/cloudflare/worker/test/tsconfig.json create mode 100644 plugins/cloudflare/worker/tsconfig.json create mode 100644 plugins/cloudflare/worker/vitest.config.mts create mode 100644 plugins/cloudflare/worker/worker-configuration.d.ts create mode 100644 plugins/cloudflare/worker/wrangler.json diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 365251b2..af0c9bd1 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -135,4 +135,6 @@ export const initCloudflareD1R2Worker = async () => { } else { p.log.info(`Selected D1: ${selectedD1}`); } + + // await execa("npx", ["-y", "wrangler", "d1", "migrate", selectedD1]); }; diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index b728b737..6ac63399 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -29,15 +29,22 @@ ], "scripts": { "build": "rslib build", - "test:type": "tsc --noEmit" + "test:type": "tsc --noEmit", + "dev": "wrangler dev worker/src/index.ts" }, "dependencies": { "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10" }, "devDependencies": { - "picocolors": "^1.0.0", + "@cloudflare/vitest-pool-workers": "^0.6.4", + "typescript": "^5.5.2", + "vitest": "2.1.8", + "wrangler": "^3.101.0", + "better-sqlite3": "^11.8.1", + "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", - "mime": "^4.0.4" + "mime": "^4.0.4", + "picocolors": "^1.0.0" } } diff --git a/plugins/cloudflare/sql/bundles.sql b/plugins/cloudflare/sql/bundles.sql new file mode 100644 index 00000000..f2fe48aa --- /dev/null +++ b/plugins/cloudflare/sql/bundles.sql @@ -0,0 +1,15 @@ +-- HotUpdater.bundles + +CREATE TABLE bundles ( + id TEXT PRIMARY KEY, + platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')), + target_app_version TEXT NOT NULL, + should_force_update INTEGER NOT NULL CHECK (should_force_update IN (0, 1)), + enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)), + file_url TEXT NOT NULL, + file_hash TEXT NOT NULL, + git_commit_hash TEXT, + message TEXT +); + +CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version); diff --git a/plugins/cloudflare/sql/get_update_info.sql b/plugins/cloudflare/sql/get_update_info.sql new file mode 100644 index 00000000..9931df29 --- /dev/null +++ b/plugins/cloudflare/sql/get_update_info.sql @@ -0,0 +1,81 @@ +-- HotUpdater.get_update_info + +CREATE OR REPLACE FUNCTION get_update_info ( + app_platform platforms, + app_version text, + bundle_id uuid +) +RETURNS TABLE ( + id uuid, + should_force_update boolean, + file_url text, + file_hash text, + status text +) +LANGUAGE plpgsql +AS +$$ +DECLARE + NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000'; +BEGIN + RETURN QUERY + WITH rollback_candidate AS ( + SELECT + b.id, + -- If status is 'ROLLBACK', should_force_update is always TRUE + TRUE AS should_force_update, + b.file_url, + b.file_hash, + 'ROLLBACK' AS status + FROM bundles b + WHERE b.enabled = TRUE + AND b.platform = app_platform + AND b.id < bundle_id + ORDER BY b.id DESC + LIMIT 1 + ), + update_candidate AS ( + SELECT + b.id, + b.should_force_update, + b.file_url, + b.file_hash, + 'UPDATE' AS status + FROM bundles b + WHERE b.enabled = TRUE + AND b.platform = app_platform + AND b.id >= bundle_id + AND semver_satisfies(b.target_app_version, app_version) + ORDER BY b.id DESC + LIMIT 1 + ), + final_result AS ( + SELECT * + FROM update_candidate + + UNION ALL + + SELECT * + FROM rollback_candidate + WHERE NOT EXISTS (SELECT 1 FROM update_candidate) + ) + SELECT * + FROM final_result WHERE final_result.id != bundle_id + + UNION ALL + /* + When there are no final results and bundle_id != NIL_UUID, + add one fallback row. + This fallback row is also ROLLBACK so shouldForceUpdate = TRUE. + */ + SELECT + NIL_UUID AS id, + TRUE AS should_force_update, -- Always TRUE + NULL AS file_url, + NULL AS file_hash, + 'ROLLBACK' AS status + WHERE (SELECT COUNT(*) FROM final_result) = 0 + AND bundle_id != NIL_UUID; + +END; +$$; \ No newline at end of file diff --git a/plugins/cloudflare/sql/get_update_info.test.ts b/plugins/cloudflare/sql/get_update_info.test.ts new file mode 100644 index 00000000..3435b082 --- /dev/null +++ b/plugins/cloudflare/sql/get_update_info.test.ts @@ -0,0 +1,66 @@ +import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core"; +import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils"; +import Database from "better-sqlite3"; +import camelcaseKeys from "camelcase-keys"; +import { afterAll, beforeEach, describe } from "vitest"; +import { prepareSql } from "./prepareSql"; + +const createInsertBundleQuery = (bundle: Bundle) => { + return ` + INSERT INTO bundles ( + id, file_url, file_hash, platform, target_app_version, + should_force_update, enabled, git_commit_hash, message + ) VALUES ( + '${bundle.id}', + '${bundle.fileUrl}', + '${bundle.fileHash}', + '${bundle.platform}', + '${bundle.targetAppVersion}', + ${bundle.shouldForceUpdate}, + ${bundle.enabled}, + ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"}, + ${bundle.message ? `'${bundle.message}'` : "null"} + ); + `; +}; + +const createGetUpdateInfo = + (db: Database) => + async ( + bundles: Bundle[], + { appVersion, bundleId, platform }: GetBundlesArgs, + ): Promise => { + db.exec(createInsertBundleQuerys(bundles)); + + const result = db + .prepare(` + SELECT * FROM get_update_info(?, ?, ?) + `) + .get(platform, appVersion, bundleId); + + return result ? (camelcaseKeys(result) as UpdateInfo) : null; + }; + +const createInsertBundleQuerys = (bundles: Bundle[]) => { + return bundles.map(createInsertBundleQuery).join("\n"); +}; + +const db = new Database(":memory:"); + +const sql = await prepareSql(); +db.exec(sql); +const getUpdateInfo = createGetUpdateInfo(db); + +describe("getUpdateInfo", () => { + beforeEach(() => { + db.exec("DELETE FROM bundles"); + }); + + afterAll(() => { + db.close(); + }); + + setupGetUpdateInfoTestSuite({ + getUpdateInfo: getUpdateInfo, + }); +}); diff --git a/plugins/cloudflare/sql/prepareSql.ts b/plugins/cloudflare/sql/prepareSql.ts new file mode 100644 index 00000000..9635752e --- /dev/null +++ b/plugins/cloudflare/sql/prepareSql.ts @@ -0,0 +1,11 @@ +import path from "path"; +import fs from "fs/promises"; + +export const prepareSql = async () => { + const files = await fs.readdir(__dirname); + const sqlFiles = files.filter((file) => file.endsWith(".sql")); + const contents = await Promise.all( + sqlFiles.map((file) => fs.readFile(path.join(__dirname, file), "utf-8")), + ); + return contents.join("\n"); +}; diff --git a/plugins/cloudflare/sql/semver_satisfies.sql b/plugins/cloudflare/sql/semver_satisfies.sql new file mode 100644 index 00000000..ce4b3b0d --- /dev/null +++ b/plugins/cloudflare/sql/semver_satisfies.sql @@ -0,0 +1,126 @@ +-- HotUpdater.semver_satisfies + +CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT) +RETURNS BOOLEAN AS $$ +DECLARE + version_parts TEXT[]; + version_major INT; + version_minor INT; + version_patch INT; + satisfies BOOLEAN := FALSE; +BEGIN + -- Split the version into major, minor, and patch + version_parts := string_to_array(version, '.'); + version_major := version_parts[1]::INT; + version_minor := version_parts[2]::INT; + version_patch := version_parts[3]::INT; + + -- Parse range expression and evaluate + IF range_expression ~ '^\d+\.\d+\.\d+$' THEN + -- Exact match + satisfies := (range_expression = version); + + ELSIF range_expression = '*' THEN + -- Matches any version + satisfies := TRUE; + + ELSIF range_expression ~ '^\d+\.x\.x$' THEN + -- Matches major.x.x + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + BEGIN + satisfies := (version_major = major_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN + -- Matches major.minor.x + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + minor_range INT := split_part(range_expression, '.', 2)::INT; + BEGIN + satisfies := (version_major = major_range AND version_minor = minor_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+$' THEN + -- Matches major.minor + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + minor_range INT := split_part(range_expression, '.', 2)::INT; + BEGIN + satisfies := (version_major = major_range AND version_minor = minor_range); + END; + + ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN + -- Matches range e.g., 1.2.3 - 1.2.7 + DECLARE + lower_bound TEXT := split_part(range_expression, ' - ', 1); + upper_bound TEXT := split_part(range_expression, ' - ', 2); + BEGIN + satisfies := (version >= lower_bound AND version <= upper_bound); + END; + + ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN + -- Matches range with inequalities + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1'); + upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1'); + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN + -- Matches ~1.2.3 (>=1.2.3 <1.3.0) + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '~', ''); + upper_bound_major INT := split_part(lower_bound, '.', 1)::INT; + upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1; + upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN + -- Matches ^1.2.3 (>=1.2.3 <2.0.0) + DECLARE + lower_bound TEXT := regexp_replace(range_expression, '\^', ''); + upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1; + upper_bound TEXT := upper_bound_major || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + -- [Added] 1) Single major version pattern '^(\d+)$' + ELSIF range_expression ~ '^\d+$' THEN + /* + e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range + "2" would be interpreted as (>=2.0.0 <3.0.0) + */ + DECLARE + major_range INT := range_expression::INT; + lower_bound TEXT := major_range || '.0.0'; + upper_bound TEXT := (major_range + 1) || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + -- [Added] 2) major.x pattern '^(\d+)\.x$' + ELSIF range_expression ~ '^\d+\.x$' THEN + /* + e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK + effectively works like (>=2.0.0 <3.0.0) + */ + DECLARE + major_range INT := split_part(range_expression, '.', 1)::INT; + lower_bound TEXT := major_range || '.0.0'; + upper_bound TEXT := (major_range + 1) || '.0.0'; + BEGIN + satisfies := (version >= lower_bound AND version < upper_bound); + END; + + ELSE + RAISE EXCEPTION 'Unsupported range expression: %', range_expression; + END IF; + + RETURN satisfies; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/plugins/cloudflare/sql/semver_satisfies.test.ts b/plugins/cloudflare/sql/semver_satisfies.test.ts new file mode 100644 index 00000000..daf8455b --- /dev/null +++ b/plugins/cloudflare/sql/semver_satisfies.test.ts @@ -0,0 +1,28 @@ +import { setupSemverSatisfiesTestSuite } from "@hot-updater/core/test-utils"; +import Database from "better-sqlite3"; +import { afterAll, describe } from "vitest"; +import { prepareSql } from "./prepareSql"; + +const db = new Database(":memory:"); +const sql = await prepareSql(); +db.exec(sql); + +const createSemverSatisfies = + (db: Database) => (targetAppVersion: string, currentVersion: string) => { + const result = db + .prepare(` + SELECT semver_satisfies(?, ?) AS actual; + `) + .get(targetAppVersion, currentVersion) as { actual: number }; + return Boolean(result.actual); + }; + +const semverSatisfies = createSemverSatisfies(db); + +describe("semverSatisfies", () => { + afterAll(() => { + db.close(); + }); + + setupSemverSatisfiesTestSuite({ semverSatisfies }); +}); diff --git a/plugins/cloudflare/worker/.editorconfig b/plugins/cloudflare/worker/.editorconfig new file mode 100644 index 00000000..a727df34 --- /dev/null +++ b/plugins/cloudflare/worker/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space diff --git a/plugins/cloudflare/worker/.gitignore b/plugins/cloudflare/worker/.gitignore new file mode 100644 index 00000000..3b0fe33c --- /dev/null +++ b/plugins/cloudflare/worker/.gitignore @@ -0,0 +1,172 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars +.wrangler/ diff --git a/plugins/cloudflare/worker/.prettierrc b/plugins/cloudflare/worker/.prettierrc new file mode 100644 index 00000000..5c7b5d3c --- /dev/null +++ b/plugins/cloudflare/worker/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 140, + "singleQuote": true, + "semi": true, + "useTabs": true +} diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts new file mode 100644 index 00000000..aad761da --- /dev/null +++ b/plugins/cloudflare/worker/src/index.ts @@ -0,0 +1,18 @@ +/** + * Welcome to Cloudflare Workers! This is your first worker. + * + * - Run `npm run dev` in your terminal to start a development server + * - Open a browser tab at http://localhost:8787/ to see your worker in action + * - Run `npm run deploy` to publish your worker + * + * Bind resources to your worker in `wrangler.json`. After adding bindings, a type definition for the + * `Env` object can be regenerated with `npm run cf-typegen`. + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +export default { + async fetch(request, env, ctx): Promise { + return new Response("Hello World!"); + }, +} satisfies ExportedHandler; diff --git a/plugins/cloudflare/worker/test/index.spec.ts b/plugins/cloudflare/worker/test/index.spec.ts new file mode 100644 index 00000000..fbee335d --- /dev/null +++ b/plugins/cloudflare/worker/test/index.spec.ts @@ -0,0 +1,25 @@ +// test/index.spec.ts +import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; +import { describe, it, expect } from 'vitest'; +import worker from '../src/index'; + +// For now, you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +describe('Hello World worker', () => { + it('responds with Hello World! (unit style)', async () => { + const request = new IncomingRequest('http://example.com'); + // Create an empty context to pass to `worker.fetch()`. + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); + }); + + it('responds with Hello World! (integration style)', async () => { + const response = await SELF.fetch('https://example.com'); + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); + }); +}); diff --git a/plugins/cloudflare/worker/test/tsconfig.json b/plugins/cloudflare/worker/test/tsconfig.json new file mode 100644 index 00000000..7fc43628 --- /dev/null +++ b/plugins/cloudflare/worker/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] + }, + "include": ["./**/*.ts", "../worker-configuration.d.ts"], + "exclude": [] +} diff --git a/plugins/cloudflare/worker/tsconfig.json b/plugins/cloudflare/worker/tsconfig.json new file mode 100644 index 00000000..33bdb791 --- /dev/null +++ b/plugins/cloudflare/worker/tsconfig.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2021", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2021"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", + /* Specify type package names to be included without being referenced in a source file. */ + "types": [ + "@cloudflare/workers-types/2023-07-01" + ], + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + }, + "exclude": ["test"], + "include": ["worker-configuration.d.ts", "src/**/*.ts"] +} diff --git a/plugins/cloudflare/worker/vitest.config.mts b/plugins/cloudflare/worker/vitest.config.mts new file mode 100644 index 00000000..999223a1 --- /dev/null +++ b/plugins/cloudflare/worker/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: './wrangler.json' }, + }, + }, + }, +}); diff --git a/plugins/cloudflare/worker/worker-configuration.d.ts b/plugins/cloudflare/worker/worker-configuration.d.ts new file mode 100644 index 00000000..7edf06f5 --- /dev/null +++ b/plugins/cloudflare/worker/worker-configuration.d.ts @@ -0,0 +1,4 @@ +// Generated by Wrangler +// After adding bindings to `wrangler.json`, regenerate this interface via `npm run cf-typegen` +interface Env { +} diff --git a/plugins/cloudflare/worker/wrangler.json b/plugins/cloudflare/worker/wrangler.json new file mode 100644 index 00000000..88243257 --- /dev/null +++ b/plugins/cloudflare/worker/wrangler.json @@ -0,0 +1,47 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker", + "main": "src/index.ts", + "compatibility_date": "2025-01-24", + "observability": { + "enabled": true + } + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" }, + + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" }, + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" }, + + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b559126f..b5fa4df2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -683,6 +683,15 @@ importers: specifier: workspace:* version: link:../plugin-core devDependencies: + '@cloudflare/vitest-pool-workers': + specifier: ^0.6.4 + version: 0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0)) + better-sqlite3: + specifier: ^11.8.1 + version: 11.8.1 + camelcase-keys: + specifier: ^9.1.3 + version: 9.1.3 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -692,6 +701,15 @@ importers: picocolors: specifier: ^1.0.0 version: 1.1.1 + typescript: + specifier: ^5.5.2 + version: 5.7.2 + vitest: + specifier: 2.1.8 + version: 2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) + wrangler: + specifier: ^3.101.0 + version: 3.105.1(@cloudflare/workers-types@4.20250124.3) plugins/js: dependencies: @@ -2258,6 +2276,50 @@ packages: '@clack/prompts@0.9.0': resolution: {integrity: sha512-nGsytiExgUr4FL0pR/LeqxA28nz3E0cW7eLTSh3Iod9TGrbBt8Y7BHbV3mmkNC4G0evdYyQ3ZsbiBkk7ektArA==} + '@cloudflare/kv-asset-handler@0.3.4': + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} + engines: {node: '>=16.13'} + + '@cloudflare/vitest-pool-workers@0.6.7': + resolution: {integrity: sha512-4tQdPmFTHn/K+sBtrDXqBEJ9dhzIC7sV56bc8MYBV3d0oQkBqiofY+iF4M9al3Oic259iIGl1h1/rt98zkus8w==} + peerDependencies: + '@vitest/runner': 2.0.x - 2.1.x + '@vitest/snapshot': 2.0.x - 2.1.x + vitest: 2.0.x - 2.1.x + + '@cloudflare/workerd-darwin-64@1.20250124.0': + resolution: {integrity: sha512-P5Z5KfVAuoCidIc0o2JPQZFLNTXDjtxN8vhtreCUr6V+xF5pqDNwQqeBDnDDF0gcszFQOYi2OZAB9e1MwssTwA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20250124.0': + resolution: {integrity: sha512-lVxf6qIfmJ5rS6rmGKV7lt6ApY6nhD4kAQTK4vKYm/npk2sXod6LASIY0U4WBCwy4N+S75a8hP2QtmQf+KV3Iw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20250124.0': + resolution: {integrity: sha512-5S4GzN08vW/CfzaM1rVAkRhPPSDX1O1t7u0pj+xdbGl4GcazBzE4ZLre+y9OMplZ9PBCkxXkRWqHXzabWA1x4A==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20250124.0': + resolution: {integrity: sha512-CHSYnutDfXgUWL9WcP0GbzIb5OyC9RZVCJGhKbDTQy6/uH7AivNcLzXtOhNdqetKjERmOxUbL9Us7vcMQLztog==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20250124.0': + resolution: {integrity: sha512-5TunEy5x4pNUQ10Z47qP5iF6m3X9uB2ZScKDLkNaWtbQ7EcMCapOWzuynVkTKIMBgDeKw6DAB8nbbkybPyMS9w==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20250124.3': + resolution: {integrity: sha512-WRZ+ol4RnMroF3tc7en6w8b0MqLrmGnLr2LIhG8EWqIoy8MeYk5uhyNXMZ0WPBhwkRtDcTRwOt61xwnJrthskA==} + '@corvu/utils@0.4.2': resolution: {integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==} peerDependencies: @@ -2283,138 +2345,280 @@ packages: '@emnapi/wasi-threads@1.0.1': resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@esbuild-plugins/node-globals-polyfill@0.2.3': + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} + peerDependencies: + esbuild: '*' + + '@esbuild-plugins/node-modules-polyfill@0.2.2': + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} + peerDependencies: + esbuild: '*' + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.17.19': + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.17.19': + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.17.19': + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.17.19': + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.17.19': + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.17.19': + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.17.19': + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.17.19': + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.17.19': + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.17.19': + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.17.19': + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.17.19': + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.17.19': + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.17.19': + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.17.19': + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.17.19': + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.17.19': + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-x64@0.17.19': + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.17.19': + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.17.19': + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.17.19': + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.17.19': + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -2442,6 +2646,10 @@ packages: '@fastify/ajv-compiler@3.5.0': resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@fastify/error@3.4.1': resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} @@ -2616,9 +2824,6 @@ packages: '@jridgewell/source-map@0.3.5': resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -4719,6 +4924,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + address@1.2.2: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} @@ -4878,6 +5088,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -5057,6 +5270,9 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + better-sqlite3@11.8.1: + resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -5064,9 +5280,18 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + birpc@0.2.14: + resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5199,6 +5424,9 @@ packages: caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + capnp-ts@0.7.0: + resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5251,6 +5479,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -5407,6 +5638,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} @@ -5433,6 +5667,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -5504,6 +5742,9 @@ packages: csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -5550,6 +5791,10 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -5565,6 +5810,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -5607,6 +5856,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + del-cli@6.0.0: resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} engines: {node: '>=18'} @@ -5646,6 +5898,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -5658,6 +5914,9 @@ packages: engines: {node: '>= 4.0.0'} hasBin: true + devalue@4.3.3: + resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -5855,6 +6114,11 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -6021,6 +6285,9 @@ packages: estree-util-visit@1.2.1: resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -6059,10 +6326,18 @@ packages: resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -6170,6 +6445,9 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -6343,6 +6621,9 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@4.1.0: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} engines: {node: '>=6'} @@ -6369,6 +6650,9 @@ packages: git-url-parse@13.1.1: resolution: {integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -6710,6 +6994,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -7467,6 +7754,9 @@ packages: peerDependencies: solid-js: ^1.4.7 + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -7897,6 +8187,11 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mime@4.0.4: resolution: {integrity: sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==} engines: {node: '>=16'} @@ -7906,6 +8201,15 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + miniflare@3.20250124.0: + resolution: {integrity: sha512-ewsetUwhj4FqeLoE3UMqYHHyCYIOPzdhlpF9CHuHpMZbfLvI9SPd+VrKrLfOgyAF97EHqVWb6WamIrLdgtj6Kg==} + engines: {node: '>=16.13'} + hasBin: true + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -7939,6 +8243,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -7948,6 +8255,9 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -7962,6 +8272,10 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -7970,6 +8284,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -7990,6 +8307,10 @@ packages: resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} engines: {node: '>=12.0.0'} + node-abi@3.73.0: + resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} + engines: {node: '>=10'} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -8124,6 +8445,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -8313,8 +8637,8 @@ packages: path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - path-to-regexp@6.2.2: - resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -8327,6 +8651,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.2: + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} @@ -8427,6 +8754,9 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} @@ -8523,6 +8853,11 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8552,6 +8887,9 @@ packages: resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} + printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + prismjs@1.27.0: resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} engines: {node: '>=6'} @@ -8643,6 +8981,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-devtools-core@4.28.4: resolution: {integrity: sha512-IUZKLv3CimeM07G3vX4H4loxVpByrzq3HvfTX7v9migalwvLs9ZY5D3S3pKR33U+GguYfBBdMMZyToFhsSE/iQ==} @@ -9014,6 +9356,16 @@ packages: engines: {node: '>=14.18'} hasBin: true + rollup-plugin-inject@3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + + rollup-plugin-node-polyfills@0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + rollup@4.22.5: resolution: {integrity: sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9356,6 +9708,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -9429,6 +9787,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -9475,6 +9837,9 @@ packages: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} engines: {node: '>=6'} + stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -9486,6 +9851,10 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -9568,6 +9937,10 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -9627,6 +10000,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -9826,6 +10202,9 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -9894,6 +10273,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-es@3.3.9: resolution: {integrity: sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==} engines: {node: '>=0.8.0'} @@ -9913,6 +10295,13 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici@5.28.5: + resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} + engines: {node: '>=14.0'} + + unenv@2.0.0-rc.0: + resolution: {integrity: sha512-H0kl2w8jFL/FAk0xvjVing4bS3jd//mbg1QChDnn58l9Sc5RtduaKmLAL8n+eBw5jJo8ZjYV7CrEGage5LAOZQ==} + unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -10285,9 +10674,24 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerd@1.20250124.0: + resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==} + engines: {node: '>=16'} + hasBin: true + workspace-tools@0.36.4: resolution: {integrity: sha512-v0UFVvw9BjHtRu2Dau5PEJKkuG8u4jPlpXZQWjSz9XgbSutpPURqtO2P0hp3cVmQVATh8lkMFCewFgJuDnyC/w==} + wrangler@3.105.1: + resolution: {integrity: sha512-Hl+wwWrMuDAcQOo+oKccf/MlAF+BHN66hbjGLo7cYhsrj1fm+w2jcFhiVTrRDpdJHPJMDfMGGbH8Gq7sexUGEQ==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250121.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -10447,6 +10851,12 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + youch@3.3.4: + resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -12598,6 +13008,47 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.3.4': + dependencies: + mime: 3.0.0 + + '@cloudflare/vitest-pool-workers@0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0))': + dependencies: + '@vitest/runner': 2.1.8 + '@vitest/snapshot': 2.1.8 + birpc: 0.2.14 + cjs-module-lexer: 1.3.1 + devalue: 4.3.3 + esbuild: 0.17.19 + miniflare: 3.20250124.0 + semver: 7.6.3 + vitest: 2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) + wrangler: 3.105.1(@cloudflare/workers-types@4.20250124.3) + zod: 3.24.1 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - supports-color + - utf-8-validate + + '@cloudflare/workerd-darwin-64@1.20250124.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20250124.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20250124.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20250124.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20250124.0': + optional: true + + '@cloudflare/workers-types@4.20250124.3': + optional: true + '@corvu/utils@0.4.2(solid-js@1.9.3)': dependencies: '@floating-ui/dom': 1.6.12 @@ -12624,72 +13075,148 @@ snapshots: dependencies: tslib: 2.8.1 + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': + dependencies: + esbuild: 0.17.19 + + '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': + dependencies: + esbuild: 0.17.19 + escape-string-regexp: 4.0.0 + rollup-plugin-node-polyfills: 0.2.1 + '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/android-arm64@0.17.19': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm@0.17.19': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-x64@0.17.19': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.17.19': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-x64@0.17.19': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.17.19': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.17.19': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/linux-arm64@0.17.19': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm@0.17.19': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-ia32@0.17.19': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-loong64@0.17.19': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-mips64el@0.17.19': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-ppc64@0.17.19': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.17.19': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-s390x@0.17.19': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-x64@0.17.19': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.17.19': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.17.19': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.17.19': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/win32-arm64@0.17.19': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-ia32@0.17.19': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-x64@0.17.19': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -12722,6 +13249,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.13.0) fast-uri: 2.3.0 + '@fastify/busboy@2.1.1': {} + '@fastify/error@3.4.1': {} '@fastify/fast-json-stringify-compiler@4.3.0': @@ -12736,7 +13265,7 @@ snapshots: dependencies: '@fastify/error': 3.4.1 fastify-plugin: 4.5.1 - path-to-regexp: 6.2.2 + path-to-regexp: 6.3.0 reusify: 1.0.4 '@fastify/sensible@5.6.0': @@ -13087,14 +13616,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping@0.3.9': dependencies: @@ -16590,10 +17117,16 @@ snapshots: dependencies: acorn: 8.10.0 + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn-walk@8.3.2: {} acorn@8.10.0: {} + acorn@8.14.0: {} + address@1.2.2: {} agent-base@7.1.1: @@ -16768,6 +17301,10 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + as-table@1.0.55: + dependencies: + printable-characters: 1.0.42 + asap@2.0.6: {} assertion-error@2.0.1: {} @@ -17037,16 +17574,29 @@ snapshots: batch@0.6.1: optional: true + better-sqlite3@11.8.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + big.js@5.2.2: {} binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + birpc@0.2.14: {} + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + body-parser@1.20.2: dependencies: bytes: 3.1.2 @@ -17201,6 +17751,13 @@ snapshots: caniuse-lite@1.0.30001680: {} + capnp-ts@0.7.0: + dependencies: + debug: 4.3.7 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + ccount@2.0.1: {} chai@5.1.2: @@ -17250,6 +17807,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@1.1.4: {} + chrome-launcher@0.15.2: dependencies: '@types/node': 22.9.0 @@ -17401,6 +17960,8 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + connect-history-api-fallback@2.0.0: optional: true @@ -17428,6 +17989,8 @@ snapshots: cookie@0.6.0: {} + cookie@0.7.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -17563,6 +18126,8 @@ snapshots: csstype@3.1.2: {} + data-uri-to-buffer@2.0.2: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -17606,12 +18171,18 @@ snapshots: dependencies: character-entities: 2.0.2 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + dedent@0.7.0: {} dedent@1.5.3: {} deep-eql@5.0.2: {} + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@3.3.0: {} @@ -17650,6 +18221,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + del-cli@6.0.0: dependencies: del: 8.0.0 @@ -17694,6 +18267,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.3: {} + detect-newline@3.1.0: {} detect-node@2.1.0: @@ -17706,6 +18281,8 @@ snapshots: transitivePeerDependencies: - supports-color + devalue@4.3.3: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -17949,6 +18526,31 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + esbuild@0.17.19: + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -18185,6 +18787,8 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 2.0.11 + estree-walker@0.6.1: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -18249,8 +18853,12 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.1 + exit-hook@2.2.1: {} + exit@0.1.2: {} + expand-template@2.0.3: {} + expect-type@1.1.0: {} expect@29.7.0: @@ -18412,6 +19020,8 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-uri-to-path@1.0.0: {} + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -18592,6 +19202,11 @@ snapshots: get-package-type@0.1.0: {} + get-source@2.0.12: + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + get-stream@4.1.0: dependencies: pump: 3.0.2 @@ -18622,6 +19237,8 @@ snapshots: dependencies: git-up: 7.0.0 + github-from-package@0.0.0: {} + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -19045,6 +19662,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + inline-style-parser@0.1.1: {} internal-slot@1.0.7: @@ -20182,6 +20801,10 @@ snapshots: dependencies: solid-js: 1.9.3 + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -21109,8 +21732,8 @@ snapshots: micromark-extension-mdxjs@1.0.1: dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) micromark-extension-mdx-expression: 1.0.8 micromark-extension-mdx-jsx: 1.0.5 micromark-extension-mdx-md: 1.0.1 @@ -21274,10 +21897,33 @@ snapshots: mime@2.6.0: {} + mime@3.0.0: {} + mime@4.0.4: {} mimic-fn@2.1.0: {} + mimic-response@3.1.0: {} + + miniflare@3.20250124.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.5 + workerd: 1.20250124.0 + ws: 8.18.0 + youch: 3.3.4 + zod: 3.24.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + minimalistic-assert@1.0.1: {} minimatch@3.1.2: @@ -21306,12 +21952,21 @@ snapshots: minipass@7.1.2: {} + mkdirp-classic@0.5.3: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 mkdirp@1.0.4: {} + mlly@1.7.4: + dependencies: + acorn: 8.14.0 + pathe: 2.0.2 + pkg-types: 1.3.1 + ufo: 1.5.4 + mri@1.2.0: {} ms@2.0.0: {} @@ -21324,6 +21979,8 @@ snapshots: thunky: 1.1.0 optional: true + mustache@4.2.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -21332,6 +21989,8 @@ snapshots: nanoid@3.3.7: {} + napi-build-utils@2.0.0: {} + natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -21344,6 +22003,10 @@ snapshots: nocache@3.0.4: {} + node-abi@3.73.0: + dependencies: + semver: 7.6.3 + node-abort-controller@3.1.1: {} node-dir@0.1.17: @@ -21507,6 +22170,8 @@ snapshots: obuf@1.1.2: {} + ohash@1.1.4: {} + on-exit-leak-free@2.1.2: {} on-finished@2.3.0: @@ -21731,7 +22396,7 @@ snapshots: path-to-regexp@0.1.7: optional: true - path-to-regexp@6.2.2: {} + path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -21739,6 +22404,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.2: {} + pathval@2.0.0: {} peberminta@0.9.0: {} @@ -21843,6 +22510,12 @@ snapshots: dependencies: find-up: 6.3.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.2 + pkg-up@3.1.0: dependencies: find-up: 3.0.0 @@ -21927,6 +22600,21 @@ snapshots: postgres-range@1.1.4: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.73.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -21959,6 +22647,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + printable-characters@1.0.42: {} + prismjs@1.27.0: {} prismjs@1.29.0: {} @@ -22041,6 +22731,13 @@ snapshots: unpipe: 1.0.0 optional: true + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-devtools-core@4.28.4: dependencies: shell-quote: 1.8.1 @@ -22723,6 +23420,20 @@ snapshots: dependencies: glob: 10.4.1 + rollup-plugin-inject@3.0.2: + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + + rollup-plugin-node-polyfills@0.2.1: + dependencies: + rollup-plugin-inject: 3.0.2 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + rollup@4.22.5: dependencies: '@types/estree': 1.0.6 @@ -23102,6 +23813,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -23179,6 +23898,8 @@ snapshots: source-map@0.7.4: {} + sourcemap-codec@1.4.8: {} + space-separated-tokens@1.1.5: {} space-separated-tokens@2.0.2: {} @@ -23236,12 +23957,19 @@ snapshots: dependencies: type-fest: 0.7.1 + stacktracey@2.1.8: + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + statuses@1.5.0: {} statuses@2.0.1: {} std-env@3.8.0: {} + stoppable@1.1.0: {} + stream-browserify@3.0.0: dependencies: inherits: 2.0.4 @@ -23343,6 +24071,8 @@ snapshots: strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} strnum@1.0.5: {} @@ -23417,6 +24147,13 @@ snapshots: tapable@2.2.1: {} + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -23450,7 +24187,7 @@ snapshots: terser@5.31.0: dependencies: '@jridgewell/source-map': 0.3.5 - acorn: 8.10.0 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -23585,7 +24322,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.8.7 - acorn: 8.10.0 + acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 @@ -23606,7 +24343,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.9.0 - acorn: 8.10.0 + acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 @@ -23627,7 +24364,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.9.0 - acorn: 8.10.0 + acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 @@ -23648,7 +24385,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.9.0 - acorn: 8.10.0 + acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 @@ -23686,6 +24423,10 @@ snapshots: tslib: 1.14.1 typescript: 5.7.2 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -23747,6 +24488,8 @@ snapshots: typescript@5.7.2: {} + ufo@1.5.4: {} + uglify-es@3.3.9: dependencies: commander: 2.13.0 @@ -23765,6 +24508,18 @@ snapshots: undici-types@6.19.8: {} + undici@5.28.5: + dependencies: + '@fastify/busboy': 2.1.1 + + unenv@2.0.0-rc.0: + dependencies: + defu: 6.1.4 + mlly: 1.7.4 + ohash: 1.1.4 + pathe: 1.1.2 + ufo: 1.5.4 + unicode-canonical-property-names-ecmascript@2.0.0: {} unicode-match-property-ecmascript@2.0.0: @@ -23972,7 +24727,7 @@ snapshots: vite@5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 + postcss: 8.4.49 rollup: 4.22.5 optionalDependencies: '@types/node': 22.9.0 @@ -24228,6 +24983,14 @@ snapshots: word-wrap@1.2.5: {} + workerd@1.20250124.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250124.0 + '@cloudflare/workerd-darwin-arm64': 1.20250124.0 + '@cloudflare/workerd-linux-64': 1.20250124.0 + '@cloudflare/workerd-linux-arm64': 1.20250124.0 + '@cloudflare/workerd-windows-64': 1.20250124.0 + workspace-tools@0.36.4: dependencies: '@yarnpkg/lockfile': 1.1.0 @@ -24238,6 +25001,25 @@ snapshots: js-yaml: 4.1.0 micromatch: 4.0.5 + wrangler@3.105.1(@cloudflare/workers-types@4.20250124.3): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + esbuild: 0.17.19 + miniflare: 3.20250124.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.0 + workerd: 1.20250124.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20250124.3 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -24355,4 +25137,12 @@ snapshots: yoctocolors@2.1.1: {} + youch@3.3.4: + dependencies: + cookie: 0.7.2 + mustache: 4.2.0 + stacktracey: 2.1.8 + + zod@3.24.1: {} + zwitch@2.0.4: {} From 0d3a3f05ddf34df324be19a046114a8c14d4aa9b Mon Sep 17 00:00:00 2001 From: gronxb Date: Mon, 27 Jan 2025 17:44:29 +0900 Subject: [PATCH 10/61] feat: d1 migrations --- .../worker/migrations/0001_init.sql | 16 ++++++++++++ plugins/cloudflare/worker/test/index.spec.ts | 25 ------------------- plugins/cloudflare/worker/test/tsconfig.json | 8 ------ plugins/cloudflare/worker/wrangler.json | 9 ++++++- 4 files changed, 24 insertions(+), 34 deletions(-) create mode 100644 plugins/cloudflare/worker/migrations/0001_init.sql delete mode 100644 plugins/cloudflare/worker/test/index.spec.ts delete mode 100644 plugins/cloudflare/worker/test/tsconfig.json diff --git a/plugins/cloudflare/worker/migrations/0001_init.sql b/plugins/cloudflare/worker/migrations/0001_init.sql new file mode 100644 index 00000000..d4e2e340 --- /dev/null +++ b/plugins/cloudflare/worker/migrations/0001_init.sql @@ -0,0 +1,16 @@ +-- Migration number: 0001 2025-01-27T08:37:50.374Z +-- HotUpdater.bundles + +CREATE TABLE bundles ( + id TEXT PRIMARY KEY, + platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')), + target_app_version TEXT NOT NULL, + should_force_update INTEGER NOT NULL CHECK (should_force_update IN (0, 1)), + enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)), + file_url TEXT NOT NULL, + file_hash TEXT NOT NULL, + git_commit_hash TEXT, + message TEXT +); + +CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version); diff --git a/plugins/cloudflare/worker/test/index.spec.ts b/plugins/cloudflare/worker/test/index.spec.ts deleted file mode 100644 index fbee335d..00000000 --- a/plugins/cloudflare/worker/test/index.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -// test/index.spec.ts -import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; -import { describe, it, expect } from 'vitest'; -import worker from '../src/index'; - -// For now, you'll need to do something like this to get a correctly-typed -// `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; - -describe('Hello World worker', () => { - it('responds with Hello World! (unit style)', async () => { - const request = new IncomingRequest('http://example.com'); - // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); - - it('responds with Hello World! (integration style)', async () => { - const response = await SELF.fetch('https://example.com'); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); diff --git a/plugins/cloudflare/worker/test/tsconfig.json b/plugins/cloudflare/worker/test/tsconfig.json deleted file mode 100644 index 7fc43628..00000000 --- a/plugins/cloudflare/worker/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] - }, - "include": ["./**/*.ts", "../worker-configuration.d.ts"], - "exclude": [] -} diff --git a/plugins/cloudflare/worker/wrangler.json b/plugins/cloudflare/worker/wrangler.json index 88243257..967ac2c7 100644 --- a/plugins/cloudflare/worker/wrangler.json +++ b/plugins/cloudflare/worker/wrangler.json @@ -9,7 +9,14 @@ "compatibility_date": "2025-01-24", "observability": { "enabled": true - } + }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "test", + "preview_database_id": "DB", + } + ], /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement From 2e0bed62d6640ae3acd9f3d06f72b7803432d517 Mon Sep 17 00:00:00 2001 From: gronxb Date: Tue, 28 Jan 2025 13:44:05 +0900 Subject: [PATCH 11/61] fix: rename file --- .../migrations/{0001_init.sql => 0001_hot-updater_init.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/cloudflare/worker/migrations/{0001_init.sql => 0001_hot-updater_init.sql} (100%) diff --git a/plugins/cloudflare/worker/migrations/0001_init.sql b/plugins/cloudflare/worker/migrations/0001_hot-updater_init.sql similarity index 100% rename from plugins/cloudflare/worker/migrations/0001_init.sql rename to plugins/cloudflare/worker/migrations/0001_hot-updater_init.sql From 67c44f1d056634e89788d57c4001a8d41ec0cb9c Mon Sep 17 00:00:00 2001 From: gronxb Date: Wed, 29 Jan 2025 18:34:59 +0900 Subject: [PATCH 12/61] feat(cloudflare): d1Database --- examples/v0.77.0/package.json | 5 +- package.json | 1 + packages/hot-updater/src/commands/init.ts | 3 +- plugins/cloudflare/package.json | 15 +- plugins/cloudflare/src/d1Database.ts | 148 +++++++++++------- plugins/cloudflare/src/index.ts | 2 - plugins/cloudflare/src/r2Storage.ts | 65 -------- plugins/cloudflare/src/types.ts | 17 -- plugins/cloudflare/src/utils/index.ts | 2 - plugins/cloudflare/src/utils/supabaseApi.ts | 50 ------ plugins/cloudflare/src/utils/templates.ts | 6 - .../cloudflare/src/utils/transformTemplate.ts | 30 ++++ plugins/cloudflare/src/utils/wrangler.ts | 57 +++++++ pnpm-lock.yaml | 35 ++++- 14 files changed, 223 insertions(+), 213 deletions(-) delete mode 100644 plugins/cloudflare/src/r2Storage.ts delete mode 100644 plugins/cloudflare/src/utils/index.ts delete mode 100644 plugins/cloudflare/src/utils/supabaseApi.ts delete mode 100644 plugins/cloudflare/src/utils/templates.ts create mode 100644 plugins/cloudflare/src/utils/transformTemplate.ts create mode 100644 plugins/cloudflare/src/utils/wrangler.ts diff --git a/examples/v0.77.0/package.json b/examples/v0.77.0/package.json index 55059499..50864ac4 100644 --- a/examples/v0.77.0/package.json +++ b/examples/v0.77.0/package.json @@ -18,6 +18,8 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", + "@hot-updater/aws": "0.6.0", + "@hot-updater/cloudflare": "0.6.0", "@hot-updater/metro": "0.6.0", "@hot-updater/supabase": "0.6.0", "@react-native-community/cli": "15.0.1", @@ -40,7 +42,8 @@ "prettier": "2.8.8", "react-native-dotenv": "^3.4.11", "react-test-renderer": "18.3.1", - "typescript": "5.0.4" + "typescript": "5.0.4", + "wrangler": "^3.105.1" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index b7bbee76..eadc219f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@hot-updater/react-native": "workspace:*", "@hot-updater/postgres": "workspace:*", "@hot-updater/supabase": "workspace:*", + "@hot-updater/cloudflare": "workspace:*", "@hot-updater/js": "workspace:*" } }, diff --git a/packages/hot-updater/src/commands/init.ts b/packages/hot-updater/src/commands/init.ts index 7b0e7055..5ac6b0b7 100644 --- a/packages/hot-updater/src/commands/init.ts +++ b/packages/hot-updater/src/commands/init.ts @@ -20,8 +20,7 @@ const PACKAGE_MAP = { }, "cloudflare-d1-r2-worker": { dependencies: [], - devDependencies: [], - // devDependencies: ["@hot-updater/cloudflare"], + devDependencies: ["wrangler", "@hot-updater/cloudflare"], }, } as const; diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 6ac63399..3d4d0f56 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -34,17 +34,20 @@ }, "dependencies": { "@hot-updater/core": "0.5.10", - "@hot-updater/plugin-core": "0.5.10" + "@hot-updater/plugin-core": "0.5.10", + "better-sqlite3": "^11.8.1" }, "devDependencies": { + "execa": "^9.5.2", "@cloudflare/vitest-pool-workers": "^0.6.4", - "typescript": "^5.5.2", - "vitest": "2.1.8", - "wrangler": "^3.101.0", - "better-sqlite3": "^11.8.1", + "@cloudflare/workers-types": "^4.20250124.3", + "@types/better-sqlite3": "^7.6.12", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", "mime": "^4.0.4", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "typescript": "^5.5.2", + "vitest": "2.1.8", + "wrangler": "^3.101.0" } } diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts index d181d8ce..62069a6a 100644 --- a/plugins/cloudflare/src/d1Database.ts +++ b/plugins/cloudflare/src/d1Database.ts @@ -4,42 +4,71 @@ import type { DatabasePlugin, DatabasePluginHooks, } from "@hot-updater/plugin-core"; +import type { BundlesTable } from "./types"; +import { D1Database } from "./utils/wrangler"; -import { createClient } from "@supabase/supabase-js"; -import type { Database } from "./types"; - -export interface SupabaseDatabaseConfig { - supabaseUrl: string; - supabaseAnonKey: string; +export interface D1DatabaseConfig { + name: string; + cloudflareApiToken: string; } export const d1Database = - (config: SupabaseDatabaseConfig, hooks?: DatabasePluginHooks) => + (config: D1DatabaseConfig, hooks?: DatabasePluginHooks) => (_: BasePluginArgs): DatabasePlugin => { - const supabase = createClient( - config.supabaseUrl, - config.supabaseAnonKey, - ); - + const db = new D1Database(config); let bundles: Bundle[] = []; return { - name: "supabaseDatabase", + name: "d1Database", async commitBundle() { - await supabase.from("bundles").upsert( - bundles.map((bundle) => ({ - id: bundle.id, - enabled: bundle.enabled, - file_url: bundle.fileUrl, - should_force_update: bundle.shouldForceUpdate, - file_hash: bundle.fileHash, - git_commit_hash: bundle.gitCommitHash, - message: bundle.message, - platform: bundle.platform, - target_app_version: bundle.targetAppVersion, - })), - { onConflict: "id" }, - ); + const command = `INSERT INTO bundles ( + id, + enabled, + file_url, + should_force_update, + file_hash, + git_commit_hash, + message, + platform, + target_app_version + ) VALUES ( + %%id%%, + %%enabled%%, + %%file_url%%, + %%should_force_update%%, + %%file_hash%%, + %%git_commit_hash%%, + %%message%%, + %%platform%%, + %%target_app_version%% + ) ON CONFLICT (id) DO UPDATE SET + enabled = %%enabled%%, + file_url = %%file_url%%, + should_force_update = %%should_force_update%%, + file_hash = %%file_hash%%, + git_commit_hash = %%git_commit_hash%%, + message = %%message%%, + platform = %%platform%%, + target_app_version = %%target_app_version%% +`; + + for (const bundle of bundles) { + await db.execute(command, { + id: `'${bundle.id}'`, + enabled: bundle.enabled ? String(1) : String(0), + file_url: `'${bundle.fileUrl}'`, + should_force_update: bundle.shouldForceUpdate + ? String(1) + : String(0), + file_hash: `'${bundle.fileHash}'`, + git_commit_hash: bundle.gitCommitHash + ? `'${bundle.gitCommitHash}'` + : String(null), + message: bundle.message ? `'${bundle.message}'` : String(null), + platform: `'${bundle.platform}'`, + target_app_version: `'${bundle.targetAppVersion}'`, + }); + } hooks?.onDatabaseUpdated?.(); }, @@ -61,25 +90,29 @@ export const d1Database = bundles = inputBundles; }, async getBundleById(bundleId) { - const { data } = await supabase - .from("bundles") - .select("*") - .eq("id", bundleId) - .single(); + const command = "SELECT * FROM bundles WHERE id = '%%bundleId%%'"; + const { results: rows } = await db.execute< + typeof command, + BundlesTable + >(command, { + bundleId, + }); - if (!data) { + if (!rows?.length) { return null; } + + const row = rows[0]; return { - enabled: data.enabled, - fileUrl: data.file_url, - shouldForceUpdate: data.should_force_update, - fileHash: data.file_hash, - gitCommitHash: data.git_commit_hash, - id: data.id, - message: data.message, - platform: data.platform, - targetAppVersion: data.target_app_version, + enabled: Boolean(row.enabled), + fileUrl: row.file_url, + shouldForceUpdate: Boolean(row.should_force_update), + fileHash: row.file_hash, + gitCommitHash: row.git_commit_hash, + id: row.id, + message: row.message, + platform: row.platform, + targetAppVersion: row.target_app_version, } as Bundle; }, async getBundles(refresh = false) { @@ -87,25 +120,26 @@ export const d1Database = return bundles; } - const { data } = await supabase - .from("bundles") - .select("*") - .order("id", { ascending: false }); + const command = "SELECT * FROM bundles ORDER BY id DESC"; + const { results: rows } = await db.execute< + typeof command, + BundlesTable + >(command); - if (!data) { + if (!rows?.length) { return []; } - return data.map((bundle) => ({ - enabled: bundle.enabled, - fileUrl: bundle.file_url, - shouldForceUpdate: bundle.should_force_update, - fileHash: bundle.file_hash, - gitCommitHash: bundle.git_commit_hash, - id: bundle.id, - message: bundle.message, - platform: bundle.platform, - targetAppVersion: bundle.target_app_version, + return rows.map((row) => ({ + enabled: Boolean(row.enabled), + fileUrl: row.file_url, + shouldForceUpdate: Boolean(row.should_force_update), + fileHash: row.file_hash, + gitCommitHash: row.git_commit_hash, + id: row.id, + message: row.message, + platform: row.platform, + targetAppVersion: row.target_app_version, })) as Bundle[]; }, }; diff --git a/plugins/cloudflare/src/index.ts b/plugins/cloudflare/src/index.ts index 8ff3c66b..5366448b 100644 --- a/plugins/cloudflare/src/index.ts +++ b/plugins/cloudflare/src/index.ts @@ -1,3 +1 @@ export * from "./d1Database"; -export * from "./r2Storage"; -export * from "./utils"; diff --git a/plugins/cloudflare/src/r2Storage.ts b/plugins/cloudflare/src/r2Storage.ts deleted file mode 100644 index 41dbd97e..00000000 --- a/plugins/cloudflare/src/r2Storage.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "path"; -import type { - BasePluginArgs, - StoragePlugin, - StoragePluginHooks, -} from "@hot-updater/plugin-core"; -import { createClient } from "@supabase/supabase-js"; -import fs from "fs/promises"; -import mime from "mime"; -import type { Database } from "./types"; - -export interface SupabaseStorageConfig { - supabaseUrl: string; - supabaseAnonKey: string; - bucketName: string; -} - -export const r2Storage = - (config: SupabaseStorageConfig, hooks?: StoragePluginHooks) => - (_: BasePluginArgs): StoragePlugin => { - const supabase = createClient( - config.supabaseUrl, - config.supabaseAnonKey, - ); - - const bucket = supabase.storage.from(config.bucketName); - return { - name: "supabaseStorage", - async deleteBundle(bundleId) { - const Key = [bundleId].join("/"); - - await bucket.remove([Key]); - return Key; - }, - async uploadBundle(bundleId, bundlePath) { - const Body = await fs.readFile(bundlePath); - const ContentType = mime.getType(bundlePath) ?? void 0; - - const filename = path.basename(bundlePath); - - const Key = [bundleId, filename].join("/"); - - const upload = await bucket.upload(Key, Body, { - contentType: ContentType, - }); - - const fullPath = upload.data?.fullPath; - if (!fullPath) { - throw new Error( - "Upload failed. The Supabase key might be incorrect. Please verify the key using the `hot-updater get-plugin-env` command.", - ); - } - - hooks?.onStorageUploaded?.(); - - const fileUrl = new URL( - `storage/v1/object/public/${fullPath}`, - config.supabaseUrl, - ).toString(); - return { - fileUrl: hooks?.transformFileUrl?.(fullPath) ?? fileUrl, - }; - }, - }; - }; diff --git a/plugins/cloudflare/src/types.ts b/plugins/cloudflare/src/types.ts index dd0c80db..0b66ab3f 100644 --- a/plugins/cloudflare/src/types.ts +++ b/plugins/cloudflare/src/types.ts @@ -14,20 +14,3 @@ type SnakeKeyObject = T extends Record : T; export type BundlesTable = SnakeKeyObject; - -export type Database = { - public: { - Tables: { - bundles: { - Row: BundlesTable; - Insert: BundlesTable; - Update: BundlesTable; - Relationships: []; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: any; - }; -}; diff --git a/plugins/cloudflare/src/utils/index.ts b/plugins/cloudflare/src/utils/index.ts deleted file mode 100644 index b8c1bf35..00000000 --- a/plugins/cloudflare/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./supabaseApi"; -export * from "./templates"; diff --git a/plugins/cloudflare/src/utils/supabaseApi.ts b/plugins/cloudflare/src/utils/supabaseApi.ts deleted file mode 100644 index 1fc0ad93..00000000 --- a/plugins/cloudflare/src/utils/supabaseApi.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createClient } from "@supabase/supabase-js"; - -export interface SupabaseApi { - listBuckets: () => Promise< - { - id: string; - name: string; - isPublic: boolean; - createdAt: string; - }[] - >; - createBucket: ( - bucketName: string, - options: { public: boolean }, - ) => Promise<{ - name: string; - }>; -} - -export const supabaseApi = ( - supabaseUrl: string, - supabaseAnonKey: string, -): SupabaseApi => { - const supabase = createClient(supabaseUrl, supabaseAnonKey); - - return { - listBuckets: async () => { - const { data, error } = await supabase.storage.listBuckets(); - if (error) { - throw error; - } - return data.map((file) => ({ - id: file.id, - name: file.name, - isPublic: file.public, - createdAt: file.created_at, - })); - }, - createBucket: async (bucketName, options) => { - const { data, error } = await supabase.storage.createBucket( - bucketName, - options, - ); - if (error) { - throw error; - } - return data; - }, - }; -}; diff --git a/plugins/cloudflare/src/utils/templates.ts b/plugins/cloudflare/src/utils/templates.ts deleted file mode 100644 index 576fff24..00000000 --- a/plugins/cloudflare/src/utils/templates.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const supabaseConfigTomlTemplate = ` -project_id = "%%projectId%%" - -[db.seed] -enabled = false -`; diff --git a/plugins/cloudflare/src/utils/transformTemplate.ts b/plugins/cloudflare/src/utils/transformTemplate.ts new file mode 100644 index 00000000..b8e340af --- /dev/null +++ b/plugins/cloudflare/src/utils/transformTemplate.ts @@ -0,0 +1,30 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type ExtractPlaceholders = + T extends `${infer _Start}%%${infer Key}%%${infer Rest}` + ? Key | ExtractPlaceholders + : never; + +export type TransformTemplateArgs = { + [Key in ExtractPlaceholders]: string; +}; + +/** + * Replaces placeholders in the format %%key%% in a template string with values from the values object. + * Uses generic type T to automatically infer placeholder keys from the template string to ensure type safety. + * + * @example + * const str = "Hello %%name%%, you are %%age%% years old." + * const result = transformTemplate(str, { name: "John", age: "20" }) + * // Result: "Hello John, you are 20 years old." + */ +export function transformTemplate( + templateString: T, + values: TransformTemplateArgs, +): string { + let result: string = templateString; + for (const key in values) { + const placeholder = `%%${key}%%`; + result = result.replace(new RegExp(placeholder, "g"), (values as any)[key]); + } + return result; +} diff --git a/plugins/cloudflare/src/utils/wrangler.ts b/plugins/cloudflare/src/utils/wrangler.ts new file mode 100644 index 00000000..4dd48b68 --- /dev/null +++ b/plugins/cloudflare/src/utils/wrangler.ts @@ -0,0 +1,57 @@ +import { getCwd } from "@hot-updater/plugin-core"; +import { type ExecaMethod, execa } from "execa"; +import { + type TransformTemplateArgs, + transformTemplate, +} from "./transformTemplate"; + +export class D1Database { + private $: ExecaMethod<{ + extendEnv: true; + env: { + CLOUDFLARE_API_TOKEN: string; + }; + }>; + + private name: string; + + constructor({ + name, + cloudflareApiToken, + }: { name: string; cloudflareApiToken: string }) { + this.name = name; + this.$ = execa({ + extendEnv: true, + cwd: getCwd(), + env: { + CLOUDFLARE_API_TOKEN: cloudflareApiToken, + }, + }); + } + async execute( + command: Command, + args?: TransformTemplateArgs, + ): Promise<{ + results: TResult[]; + success: boolean; + meta: { duration: number }; + }> { + const result = await this + .$`npx -y wrangler d1 execute ${this.name} --command ${transformTemplate( + command, + args ?? ({} as TransformTemplateArgs), + )} --json --remote`; + + const [data] = JSON.parse(result.stdout ?? "[]") as [ + { + results: TResult[]; + success: boolean; + meta: { + duration: number; + }; + }, + ]; + + return data; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5fa4df2..de716a17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: '@hot-updater/react-native': workspace:* '@hot-updater/postgres': workspace:* '@hot-updater/supabase': workspace:* + '@hot-updater/cloudflare': workspace:* '@hot-updater/js': workspace:* importers: @@ -363,6 +364,12 @@ importers: '@babel/runtime': specifier: ^7.25.0 version: 7.26.0 + '@hot-updater/aws': + specifier: workspace:* + version: link:../../plugins/aws + '@hot-updater/cloudflare': + specifier: workspace:* + version: link:../../plugins/cloudflare '@hot-updater/metro': specifier: workspace:* version: link:../../plugins/metro @@ -432,6 +439,9 @@ importers: typescript: specifier: 5.0.4 version: 5.0.4 + wrangler: + specifier: ^3.105.1 + version: 3.105.1(@cloudflare/workers-types@4.20250124.3) packages/console: dependencies: @@ -682,19 +692,28 @@ importers: '@hot-updater/plugin-core': specifier: workspace:* version: link:../plugin-core + better-sqlite3: + specifier: ^11.8.1 + version: 11.8.1 devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 version: 0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0)) - better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + '@cloudflare/workers-types': + specifier: ^4.20250124.3 + version: 4.20250124.3 + '@types/better-sqlite3': + specifier: ^7.6.12 + version: 7.6.12 camelcase-keys: specifier: ^9.1.3 version: 9.1.3 dayjs: specifier: ^1.11.13 version: 1.11.13 + execa: + specifier: ^9.5.2 + version: 9.5.2 mime: specifier: ^4.0.4 version: 4.0.4 @@ -4430,6 +4449,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/better-sqlite3@7.6.12': + resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} + '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -13046,8 +13068,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250124.0': optional: true - '@cloudflare/workers-types@4.20250124.3': - optional: true + '@cloudflare/workers-types@4.20250124.3': {} '@corvu/utils@0.4.2(solid-js@1.9.3)': dependencies: @@ -16419,6 +16440,10 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@types/better-sqlite3@7.6.12': + dependencies: + '@types/node': 22.9.0 + '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 From 867837b9d54b8a404d2165ff0b6a2d8db59fbe51 Mon Sep 17 00:00:00 2001 From: gronxb Date: Wed, 29 Jan 2025 18:39:55 +0900 Subject: [PATCH 13/61] fix: lint --- biome.json | 1 + plugins/cloudflare/worker/tsconfig.json | 72 ++++++++++----------- plugins/cloudflare/worker/vitest.config.mts | 16 ++--- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/biome.json b/biome.json index 25c85274..2fe4c6ed 100644 --- a/biome.json +++ b/biome.json @@ -9,6 +9,7 @@ "**/dist/**", "**/build/**", "node_modules/**", + "**/worker/**", "**/*.js" ] diff --git a/plugins/cloudflare/worker/tsconfig.json b/plugins/cloudflare/worker/tsconfig.json index 33bdb791..bb3e0f7e 100644 --- a/plugins/cloudflare/worker/tsconfig.json +++ b/plugins/cloudflare/worker/tsconfig.json @@ -1,46 +1,44 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ - /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "target": "es2021", - /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "lib": ["es2021"], - /* Specify what JSX code is generated. */ - "jsx": "react-jsx", + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2021", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2021"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", - /* Specify what module code is generated. */ - "module": "es2022", - /* Specify how TypeScript looks up a file from a given module specifier. */ - "moduleResolution": "Bundler", - /* Specify type package names to be included without being referenced in a source file. */ - "types": [ - "@cloudflare/workers-types/2023-07-01" - ], - /* Enable importing .json files */ - "resolveJsonModule": true, + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", + /* Specify type package names to be included without being referenced in a source file. */ + "types": ["@cloudflare/workers-types/2023-07-01"], + /* Enable importing .json files */ + "resolveJsonModule": true, - /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - "allowJs": true, - /* Enable error reporting in type-checked JavaScript files. */ - "checkJs": false, + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, - /* Disable emitting files from a compilation. */ - "noEmit": true, + /* Disable emitting files from a compilation. */ + "noEmit": true, - /* Ensure that each file can be safely transpiled without relying on other imports. */ - "isolatedModules": true, - /* Allow 'import x from y' when a module doesn't have a default export. */ - "allowSyntheticDefaultImports": true, - /* Ensure that casing is correct in imports. */ - "forceConsistentCasingInFileNames": true, + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, - /* Enable all strict type-checking options. */ - "strict": true, + /* Enable all strict type-checking options. */ + "strict": true, - /* Skip type checking all .d.ts files. */ - "skipLibCheck": true - }, - "exclude": ["test"], - "include": ["worker-configuration.d.ts", "src/**/*.ts"] + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + }, + "exclude": ["test"], + "include": ["worker-configuration.d.ts", "src/**/*.ts"] } diff --git a/plugins/cloudflare/worker/vitest.config.mts b/plugins/cloudflare/worker/vitest.config.mts index 999223a1..c2d67dc6 100644 --- a/plugins/cloudflare/worker/vitest.config.mts +++ b/plugins/cloudflare/worker/vitest.config.mts @@ -1,11 +1,11 @@ -import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { configPath: './wrangler.json' }, - }, - }, - }, + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.json" }, + }, + }, + }, }); From 59c408288a73d8d15fac43a2ebbdc5fd786dc14d Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 00:41:46 +0900 Subject: [PATCH 14/61] fix: cloudflare --- packages/console/src-server/rpc.ts | 46 +++-- plugins/cloudflare/src/d1Database.ts | 163 ++++++++++-------- .../{wrangler.ts => CloudflareD1Database.ts} | 8 +- 3 files changed, 120 insertions(+), 97 deletions(-) rename plugins/cloudflare/src/utils/{wrangler.ts => CloudflareD1Database.ts} (88%) diff --git a/packages/console/src-server/rpc.ts b/packages/console/src-server/rpc.ts index 6b67cbb2..4bbd0803 100644 --- a/packages/console/src-server/rpc.ts +++ b/packages/console/src-server/rpc.ts @@ -2,6 +2,7 @@ import { vValidator } from "@hono/valibot-validator"; import { type Bundle, type Config, + type DatabasePlugin, getCwd, loadConfig, } from "@hot-updater/plugin-core"; @@ -21,16 +22,23 @@ export const bundleSchema = v.object({ }); let config: Config | null = null; +let databasePlugin: DatabasePlugin | null = null; -export const rpc = new Hono() - .get("/loadConfig", async (c) => { +const prepareConfig = async () => { + if (!config) { config = await loadConfig(); - return c.json(true); - }) + databasePlugin = + config?.database({ + cwd: getCwd(), + }) ?? null; + } + return { config, databasePlugin }; +}; + +export const rpc = new Hono() .get("/getConfig", async (c) => { - if (!config) { - config = await loadConfig(); - } + const { config } = await prepareConfig(); + return c.json({ console: config?.console, }); @@ -39,12 +47,8 @@ export const rpc = new Hono() return c.json(config !== null); }) .get("/getBundles", async (c) => { - if (!config) { - config = await loadConfig(); - } - const databasePlugin = config?.database({ - cwd: getCwd(), - }); + const { databasePlugin } = await prepareConfig(); + const bundles = await databasePlugin?.getBundles(); return c.json((bundles ?? []) satisfies Bundle[]); }) @@ -53,12 +57,8 @@ export const rpc = new Hono() vValidator("json", v.object({ bundleId: v.string() })), async (c) => { const { bundleId } = c.req.valid("json"); - if (!config) { - config = await loadConfig(); - } - const databasePlugin = config?.database({ - cwd: getCwd(), - }); + const { databasePlugin } = await prepareConfig(); + const bundle = await databasePlugin?.getBundleById(bundleId); return c.json((bundle ?? null) satisfies Bundle | null); }, @@ -74,12 +74,8 @@ export const rpc = new Hono() ), async (c) => { const { targetBundleId, bundle } = c.req.valid("json"); - if (!config) { - config = await loadConfig(); - } - const databasePlugin = config?.database({ - cwd: getCwd(), - }); + const { databasePlugin } = await prepareConfig(); + await databasePlugin?.updateBundle(targetBundleId, bundle); await databasePlugin?.commitBundle(); return c.json(true); diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts index 62069a6a..a52c9c1f 100644 --- a/plugins/cloudflare/src/d1Database.ts +++ b/plugins/cloudflare/src/d1Database.ts @@ -5,7 +5,7 @@ import type { DatabasePluginHooks, } from "@hot-updater/plugin-core"; import type { BundlesTable } from "./types"; -import { D1Database } from "./utils/wrangler"; +import { CloudflareD1Database } from "./utils/CloudflareD1Database"; export interface D1DatabaseConfig { name: string; @@ -14,14 +14,45 @@ export interface D1DatabaseConfig { export const d1Database = (config: D1DatabaseConfig, hooks?: DatabasePluginHooks) => - (_: BasePluginArgs): DatabasePlugin => { - const db = new D1Database(config); + (plugin: BasePluginArgs): DatabasePlugin => { + const db = new CloudflareD1Database({ ...config, cwd: plugin.cwd }); let bundles: Bundle[] = []; + const changedIds = new Set(); + function markChanged(id: string) { + changedIds.add(id); + } + return { name: "d1Database", async commitBundle() { - const command = `INSERT INTO bundles ( + if (changedIds.size === 0) { + return; + } + + const changedBundles = bundles.filter((b) => changedIds.has(b.id)); + if (changedBundles.length === 0) { + return; + } + + const valuesSql = changedBundles + .map( + (b) => `( + '${b.id}', + ${b.enabled ? 1 : 0}, + '${b.fileUrl}', + ${b.shouldForceUpdate ? 1 : 0}, + '${b.fileHash}', + ${b.gitCommitHash ? `'${b.gitCommitHash}'` : "NULL"}, + ${b.message ? `'${b.message}'` : "NULL"}, + '${b.platform}', + '${b.targetAppVersion}' + )`, + ) + .join(",\n"); + + const command = ` + INSERT OR REPLACE INTO bundles ( id, enabled, file_url, @@ -31,66 +62,47 @@ export const d1Database = message, platform, target_app_version - ) VALUES ( - %%id%%, - %%enabled%%, - %%file_url%%, - %%should_force_update%%, - %%file_hash%%, - %%git_commit_hash%%, - %%message%%, - %%platform%%, - %%target_app_version%% - ) ON CONFLICT (id) DO UPDATE SET - enabled = %%enabled%%, - file_url = %%file_url%%, - should_force_update = %%should_force_update%%, - file_hash = %%file_hash%%, - git_commit_hash = %%git_commit_hash%%, - message = %%message%%, - platform = %%platform%%, - target_app_version = %%target_app_version%% -`; - - for (const bundle of bundles) { - await db.execute(command, { - id: `'${bundle.id}'`, - enabled: bundle.enabled ? String(1) : String(0), - file_url: `'${bundle.fileUrl}'`, - should_force_update: bundle.shouldForceUpdate - ? String(1) - : String(0), - file_hash: `'${bundle.fileHash}'`, - git_commit_hash: bundle.gitCommitHash - ? `'${bundle.gitCommitHash}'` - : String(null), - message: bundle.message ? `'${bundle.message}'` : String(null), - platform: `'${bundle.platform}'`, - target_app_version: `'${bundle.targetAppVersion}'`, - }); - } + ) + VALUES + ${valuesSql};`; + + await db.execute(command); + + changedIds.clear(); hooks?.onDatabaseUpdated?.(); }, - async updateBundle(targetBundleId: string, newBundle: Partial) { - bundles = await this.getBundles(); - const targetIndex = bundles.findIndex((u) => u.id === targetBundleId); - if (targetIndex === -1) { - throw new Error("target bundle version not found"); + async updateBundle(targetBundleId: string, newBundle: Partial) { + const index = bundles.findIndex((b) => b.id === targetBundleId); + if (index === -1) { + throw new Error(`Cannot find bundle with id ${targetBundleId}`); } - - Object.assign(bundles[targetIndex], newBundle); + Object.assign(bundles[index], newBundle); + markChanged(targetBundleId); }, - async appendBundle(inputBundle) { - bundles = await this.getBundles(); + + async appendBundle(inputBundle: Bundle) { bundles.unshift(inputBundle); + + markChanged(inputBundle.id); }, - async setBundles(inputBundles) { + + async setBundles(inputBundles: Bundle[]) { bundles = inputBundles; + for (const b of inputBundles) { + markChanged(b.id); + } }, - async getBundleById(bundleId) { - const command = "SELECT * FROM bundles WHERE id = '%%bundleId%%'"; + + async getBundleById(bundleId: string) { + const found = bundles.find((b) => b.id === bundleId); + if (found) { + return found; + } + + const command = + "SELECT * FROM bundles WHERE id = '%%bundleId%%' LIMIT 1"; const { results: rows } = await db.execute< typeof command, BundlesTable @@ -115,32 +127,47 @@ export const d1Database = targetAppVersion: row.target_app_version, } as Bundle; }, + async getBundles(refresh = false) { if (bundles.length > 0 && !refresh) { return bundles; } - const command = "SELECT * FROM bundles ORDER BY id DESC"; + const command = ` + SELECT + id, + enabled, + file_url, + should_force_update, + file_hash, + git_commit_hash, + message, + platform, + target_app_version + FROM bundles + ORDER BY id DESC + `; const { results: rows } = await db.execute< typeof command, BundlesTable >(command); if (!rows?.length) { - return []; + bundles = []; + } else { + bundles = rows.map((row) => ({ + id: row.id, + enabled: Boolean(row.enabled), + fileUrl: row.file_url, + shouldForceUpdate: Boolean(row.should_force_update), + fileHash: row.file_hash, + gitCommitHash: row.git_commit_hash, + message: row.message, + platform: row.platform, + targetAppVersion: row.target_app_version, + })); } - - return rows.map((row) => ({ - enabled: Boolean(row.enabled), - fileUrl: row.file_url, - shouldForceUpdate: Boolean(row.should_force_update), - fileHash: row.file_hash, - gitCommitHash: row.git_commit_hash, - id: row.id, - message: row.message, - platform: row.platform, - targetAppVersion: row.target_app_version, - })) as Bundle[]; + return bundles; }, }; }; diff --git a/plugins/cloudflare/src/utils/wrangler.ts b/plugins/cloudflare/src/utils/CloudflareD1Database.ts similarity index 88% rename from plugins/cloudflare/src/utils/wrangler.ts rename to plugins/cloudflare/src/utils/CloudflareD1Database.ts index 4dd48b68..fda65751 100644 --- a/plugins/cloudflare/src/utils/wrangler.ts +++ b/plugins/cloudflare/src/utils/CloudflareD1Database.ts @@ -1,11 +1,10 @@ -import { getCwd } from "@hot-updater/plugin-core"; import { type ExecaMethod, execa } from "execa"; import { type TransformTemplateArgs, transformTemplate, } from "./transformTemplate"; -export class D1Database { +export class CloudflareD1Database { private $: ExecaMethod<{ extendEnv: true; env: { @@ -17,12 +16,13 @@ export class D1Database { constructor({ name, + cwd, cloudflareApiToken, - }: { name: string; cloudflareApiToken: string }) { + }: { name: string; cwd: string; cloudflareApiToken: string }) { this.name = name; this.$ = execa({ extendEnv: true, - cwd: getCwd(), + cwd, env: { CLOUDFLARE_API_TOKEN: cloudflareApiToken, }, From 413f7f021b312d348a2f8accc286d601d2638407 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 00:46:13 +0900 Subject: [PATCH 15/61] fix: todo --- .../hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index af0c9bd1..01433a93 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -95,6 +95,7 @@ export const initCloudflareD1R2Worker = async () => { if (p.isCancel(name)) { process.exit(1); } + // TODO: allow public access const newR2 = await cloudflareApi.createR2Bucket(name); p.log.info(`Created new R2 Bucket: ${newR2}`); } else { From 3c6f6f13e692c8df3e66751a020e02b9ed1076a0 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 00:47:15 +0900 Subject: [PATCH 16/61] fix: test --- .../cloudflare/sql/get_update_info.test.ts | 66 ------------------- .../cloudflare/sql/semver_satisfies.test.ts | 28 -------- 2 files changed, 94 deletions(-) delete mode 100644 plugins/cloudflare/sql/get_update_info.test.ts delete mode 100644 plugins/cloudflare/sql/semver_satisfies.test.ts diff --git a/plugins/cloudflare/sql/get_update_info.test.ts b/plugins/cloudflare/sql/get_update_info.test.ts deleted file mode 100644 index 3435b082..00000000 --- a/plugins/cloudflare/sql/get_update_info.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core"; -import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils"; -import Database from "better-sqlite3"; -import camelcaseKeys from "camelcase-keys"; -import { afterAll, beforeEach, describe } from "vitest"; -import { prepareSql } from "./prepareSql"; - -const createInsertBundleQuery = (bundle: Bundle) => { - return ` - INSERT INTO bundles ( - id, file_url, file_hash, platform, target_app_version, - should_force_update, enabled, git_commit_hash, message - ) VALUES ( - '${bundle.id}', - '${bundle.fileUrl}', - '${bundle.fileHash}', - '${bundle.platform}', - '${bundle.targetAppVersion}', - ${bundle.shouldForceUpdate}, - ${bundle.enabled}, - ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"}, - ${bundle.message ? `'${bundle.message}'` : "null"} - ); - `; -}; - -const createGetUpdateInfo = - (db: Database) => - async ( - bundles: Bundle[], - { appVersion, bundleId, platform }: GetBundlesArgs, - ): Promise => { - db.exec(createInsertBundleQuerys(bundles)); - - const result = db - .prepare(` - SELECT * FROM get_update_info(?, ?, ?) - `) - .get(platform, appVersion, bundleId); - - return result ? (camelcaseKeys(result) as UpdateInfo) : null; - }; - -const createInsertBundleQuerys = (bundles: Bundle[]) => { - return bundles.map(createInsertBundleQuery).join("\n"); -}; - -const db = new Database(":memory:"); - -const sql = await prepareSql(); -db.exec(sql); -const getUpdateInfo = createGetUpdateInfo(db); - -describe("getUpdateInfo", () => { - beforeEach(() => { - db.exec("DELETE FROM bundles"); - }); - - afterAll(() => { - db.close(); - }); - - setupGetUpdateInfoTestSuite({ - getUpdateInfo: getUpdateInfo, - }); -}); diff --git a/plugins/cloudflare/sql/semver_satisfies.test.ts b/plugins/cloudflare/sql/semver_satisfies.test.ts deleted file mode 100644 index daf8455b..00000000 --- a/plugins/cloudflare/sql/semver_satisfies.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { setupSemverSatisfiesTestSuite } from "@hot-updater/core/test-utils"; -import Database from "better-sqlite3"; -import { afterAll, describe } from "vitest"; -import { prepareSql } from "./prepareSql"; - -const db = new Database(":memory:"); -const sql = await prepareSql(); -db.exec(sql); - -const createSemverSatisfies = - (db: Database) => (targetAppVersion: string, currentVersion: string) => { - const result = db - .prepare(` - SELECT semver_satisfies(?, ?) AS actual; - `) - .get(targetAppVersion, currentVersion) as { actual: number }; - return Boolean(result.actual); - }; - -const semverSatisfies = createSemverSatisfies(db); - -describe("semverSatisfies", () => { - afterAll(() => { - db.close(); - }); - - setupSemverSatisfiesTestSuite({ semverSatisfies }); -}); From e7a055c87ce5dbec3a16d7ba9a954c86b48e7c30 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 00:53:21 +0900 Subject: [PATCH 17/61] fix: comment --- .../src/commands/init/cloudflareD1R2Worker/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 01433a93..05c2bbdc 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -63,6 +63,12 @@ const cloudflareApi = { }; export const initCloudflareD1R2Worker = async () => { + // TODO: + // 1. Get Cloudflare API Token (R2 + D1 + Worker) + // 2. Create R2 Bucket (allow public access) + // 3. Create D1 Database + // 4. Create Worker + const s = p.spinner(); const createKey = `create/${Math.random().toString(36).substring(2, 15)}`; From 82c3c35bf106d6179c98894768b71b4918c5b769 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 00:59:14 +0900 Subject: [PATCH 18/61] refactor: move --- .../CloudflareD1Database.ts => context/CloudflareD1.ts} | 2 +- plugins/cloudflare/src/d1Database.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename plugins/cloudflare/src/{utils/CloudflareD1Database.ts => context/CloudflareD1.ts} (97%) diff --git a/plugins/cloudflare/src/utils/CloudflareD1Database.ts b/plugins/cloudflare/src/context/CloudflareD1.ts similarity index 97% rename from plugins/cloudflare/src/utils/CloudflareD1Database.ts rename to plugins/cloudflare/src/context/CloudflareD1.ts index fda65751..5ed66f33 100644 --- a/plugins/cloudflare/src/utils/CloudflareD1Database.ts +++ b/plugins/cloudflare/src/context/CloudflareD1.ts @@ -4,7 +4,7 @@ import { transformTemplate, } from "./transformTemplate"; -export class CloudflareD1Database { +export class CloudflareD1 { private $: ExecaMethod<{ extendEnv: true; env: { diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts index a52c9c1f..0fd16892 100644 --- a/plugins/cloudflare/src/d1Database.ts +++ b/plugins/cloudflare/src/d1Database.ts @@ -4,8 +4,8 @@ import type { DatabasePlugin, DatabasePluginHooks, } from "@hot-updater/plugin-core"; +import { CloudflareD1 } from "./context/CloudflareD1"; import type { BundlesTable } from "./types"; -import { CloudflareD1Database } from "./utils/CloudflareD1Database"; export interface D1DatabaseConfig { name: string; @@ -15,7 +15,7 @@ export interface D1DatabaseConfig { export const d1Database = (config: D1DatabaseConfig, hooks?: DatabasePluginHooks) => (plugin: BasePluginArgs): DatabasePlugin => { - const db = new CloudflareD1Database({ ...config, cwd: plugin.cwd }); + const db = new CloudflareD1({ ...config, cwd: plugin.cwd }); let bundles: Bundle[] = []; const changedIds = new Set(); From 00f7d48b7bfc9552e2cc8652577e1df969b0c5c3 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 01:02:10 +0900 Subject: [PATCH 19/61] fix: folder --- plugins/cloudflare/src/context/CloudflareD1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cloudflare/src/context/CloudflareD1.ts b/plugins/cloudflare/src/context/CloudflareD1.ts index 5ed66f33..fcc1516a 100644 --- a/plugins/cloudflare/src/context/CloudflareD1.ts +++ b/plugins/cloudflare/src/context/CloudflareD1.ts @@ -2,7 +2,7 @@ import { type ExecaMethod, execa } from "execa"; import { type TransformTemplateArgs, transformTemplate, -} from "./transformTemplate"; +} from "../utils/transformTemplate"; export class CloudflareD1 { private $: ExecaMethod<{ From d0b80529f236a8b59df0622740b8fcc522a35963 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 01:49:10 +0900 Subject: [PATCH 20/61] feat: migration cloudflare api --- plugins/cloudflare/package.json | 6 +- .../cloudflare/src/context/CloudflareD1.ts | 57 ---------- plugins/cloudflare/src/d1Database.ts | 105 ++++++++++++------ pnpm-lock.yaml | 86 +++++++++++++- 4 files changed, 156 insertions(+), 98 deletions(-) delete mode 100644 plugins/cloudflare/src/context/CloudflareD1.ts diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 3d4d0f56..cfc29080 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -35,16 +35,18 @@ "dependencies": { "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10", - "better-sqlite3": "^11.8.1" + "cloudflare": "^4.0.0" }, "devDependencies": { - "execa": "^9.5.2", "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", + "better-sqlite3": "^11.8.1", "@types/better-sqlite3": "^7.6.12", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", + "execa": "^9.5.2", "mime": "^4.0.4", + "pg-minify": "^1.6.5", "picocolors": "^1.0.0", "typescript": "^5.5.2", "vitest": "2.1.8", diff --git a/plugins/cloudflare/src/context/CloudflareD1.ts b/plugins/cloudflare/src/context/CloudflareD1.ts deleted file mode 100644 index fcc1516a..00000000 --- a/plugins/cloudflare/src/context/CloudflareD1.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { type ExecaMethod, execa } from "execa"; -import { - type TransformTemplateArgs, - transformTemplate, -} from "../utils/transformTemplate"; - -export class CloudflareD1 { - private $: ExecaMethod<{ - extendEnv: true; - env: { - CLOUDFLARE_API_TOKEN: string; - }; - }>; - - private name: string; - - constructor({ - name, - cwd, - cloudflareApiToken, - }: { name: string; cwd: string; cloudflareApiToken: string }) { - this.name = name; - this.$ = execa({ - extendEnv: true, - cwd, - env: { - CLOUDFLARE_API_TOKEN: cloudflareApiToken, - }, - }); - } - async execute( - command: Command, - args?: TransformTemplateArgs, - ): Promise<{ - results: TResult[]; - success: boolean; - meta: { duration: number }; - }> { - const result = await this - .$`npx -y wrangler d1 execute ${this.name} --command ${transformTemplate( - command, - args ?? ({} as TransformTemplateArgs), - )} --json --remote`; - - const [data] = JSON.parse(result.stdout ?? "[]") as [ - { - results: TResult[]; - success: boolean; - meta: { - duration: number; - }; - }, - ]; - - return data; - } -} diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts index 0fd16892..248301da 100644 --- a/plugins/cloudflare/src/d1Database.ts +++ b/plugins/cloudflare/src/d1Database.ts @@ -1,21 +1,28 @@ +import minify from "pg-minify"; + import type { BasePluginArgs, Bundle, DatabasePlugin, DatabasePluginHooks, } from "@hot-updater/plugin-core"; -import { CloudflareD1 } from "./context/CloudflareD1"; +import Cloudflare from "cloudflare"; + import type { BundlesTable } from "./types"; export interface D1DatabaseConfig { - name: string; + databaseId: string; + accountId: string; cloudflareApiToken: string; } export const d1Database = (config: D1DatabaseConfig, hooks?: DatabasePluginHooks) => - (plugin: BasePluginArgs): DatabasePlugin => { - const db = new CloudflareD1({ ...config, cwd: plugin.cwd }); + (_: BasePluginArgs): DatabasePlugin => { + const cloudflare = new Cloudflare({ + apiToken: config.cloudflareApiToken, + }); + let bundles: Bundle[] = []; const changedIds = new Set(); @@ -35,23 +42,25 @@ export const d1Database = return; } + const params: (string | number | boolean | null)[] = []; const valuesSql = changedBundles - .map( - (b) => `( - '${b.id}', - ${b.enabled ? 1 : 0}, - '${b.fileUrl}', - ${b.shouldForceUpdate ? 1 : 0}, - '${b.fileHash}', - ${b.gitCommitHash ? `'${b.gitCommitHash}'` : "NULL"}, - ${b.message ? `'${b.message}'` : "NULL"}, - '${b.platform}', - '${b.targetAppVersion}' - )`, - ) + .map((b) => { + params.push( + b.id, + b.enabled ? 1 : 0, + b.fileUrl, + b.shouldForceUpdate ? 1 : 0, + b.fileHash, + b.gitCommitHash || null, + b.message || null, + b.platform, + b.targetAppVersion, + ); + return "(?, ?, ?, ?, ?, ?, ?, ?, ?)"; + }) .join(",\n"); - const command = ` + const sql = minify(/* sql */ ` INSERT OR REPLACE INTO bundles ( id, enabled, @@ -64,9 +73,15 @@ export const d1Database = target_app_version ) VALUES - ${valuesSql};`; + ${valuesSql};`); + + await cloudflare.d1.database.query(config.databaseId, { + account_id: config.accountId, + sql, - await db.execute(command); + // why this type is string[] ? + params: params as string[], + }); changedIds.clear(); @@ -101,15 +116,24 @@ export const d1Database = return found; } - const command = - "SELECT * FROM bundles WHERE id = '%%bundleId%%' LIMIT 1"; - const { results: rows } = await db.execute< - typeof command, - BundlesTable - >(command, { - bundleId, - }); + const sql = minify( + /* sql */ ` + SELECT * FROM bundles WHERE id = ? LIMIT 1`, + ); + const [response] = await cloudflare.d1.database.query( + config.databaseId, + { + account_id: config.accountId, + sql, + params: [bundleId], + }, + ); + + if (!response.success) { + return null; + } + const rows = response.results as BundlesTable[]; if (!rows?.length) { return null; } @@ -133,7 +157,8 @@ export const d1Database = return bundles; } - const command = ` + const sql = minify( + /* sql */ ` SELECT id, enabled, @@ -146,12 +171,24 @@ export const d1Database = target_app_version FROM bundles ORDER BY id DESC - `; - const { results: rows } = await db.execute< - typeof command, - BundlesTable - >(command); + `, + ); + + const [response] = await cloudflare.d1.database.query( + config.databaseId, + { + account_id: config.accountId, + sql, + params: [], + }, + ); + + if (!response.success) { + bundles = []; + return bundles; + } + const rows = response.results as BundlesTable[]; if (!rows?.length) { bundles = []; } else { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de716a17..b7e04276 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -692,9 +692,9 @@ importers: '@hot-updater/plugin-core': specifier: workspace:* version: link:../plugin-core - better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + cloudflare: + specifier: ^4.0.0 + version: 4.0.0(encoding@0.1.13) devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 @@ -705,6 +705,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 + better-sqlite3: + specifier: ^11.8.1 + version: 11.8.1 camelcase-keys: specifier: ^9.1.3 version: 9.1.3 @@ -717,6 +720,9 @@ importers: mime: specifier: ^4.0.4 version: 4.0.4 + pg-minify: + specifier: ^1.6.5 + version: 1.6.5 picocolors: specifier: ^1.0.0 version: 1.1.1 @@ -4531,6 +4537,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} @@ -4959,6 +4968,10 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -5567,6 +5580,9 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cloudflare@4.0.0: + resolution: {integrity: sha512-0o3rUVLTZoHKcM+83UbkeeFdWdsaEaF2e8QA7e8hNwTku8EwO14JoYyeBF+DJZnRdnoJGPbAjA9lDKQ7f6ylUg==} + clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -6565,6 +6581,9 @@ packages: resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -6573,6 +6592,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -6941,6 +6964,9 @@ packages: resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + hyperdyperid@1.2.0: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} @@ -8340,6 +8366,10 @@ packages: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -8696,6 +8726,10 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-minify@1.6.5: + resolution: {integrity: sha512-u0UE8veaCnMfJmoklqneeBBopOAPG3/6DHqGVHYAhz8DkJXh9dnjPlz25fRxn4e+6XVzdOp7kau63Rp52fZ3WQ==} + engines: {node: '>=14.0.0'} + pg-numeric@1.0.2: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} @@ -10587,6 +10621,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -15288,7 +15326,9 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' + - bufferutil - supports-color + - utf-8-validate '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))': dependencies: @@ -15299,9 +15339,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - bufferutil - supports-color - - utf-8-validate optional: true '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))': @@ -16555,6 +16593,11 @@ snapshots: '@types/ms@0.7.34': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.9.0 + form-data: 4.0.0 + '@types/node-forge@1.3.11': dependencies: '@types/node': 22.9.0 @@ -17161,6 +17204,10 @@ snapshots: - supports-color optional: true + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -17909,6 +17956,18 @@ snapshots: clone@1.0.4: {} + cloudflare@4.0.0(encoding@0.1.13): + dependencies: + '@types/node': 18.19.33 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + clsx@2.0.0: {} clsx@2.1.1: {} @@ -19155,6 +19214,8 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -19163,6 +19224,11 @@ snapshots: format@0.2.2: {} + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + forwarded@0.2.0: {} fraction.js@4.3.7: {} @@ -19626,6 +19692,10 @@ snapshots: human-signals@8.0.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + hyperdyperid@1.2.0: {} iconv-lite@0.4.24: @@ -22038,6 +22108,8 @@ snapshots: dependencies: minimatch: 3.1.2 + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -22448,6 +22520,8 @@ snapshots: pg-int8@1.0.1: {} + pg-minify@1.6.5: {} + pg-numeric@1.0.2: {} pg-pool@3.7.0(pg@8.13.1): @@ -24827,6 +24901,8 @@ snapshots: web-namespaces@2.0.1: {} + web-streams-polyfill@4.0.0-beta.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: From 1fb9d49d4c5f2f0262882f647bf2ec19e44ffc2b Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 02:18:03 +0900 Subject: [PATCH 21/61] feat: r2 storage --- packages/console/src-server/rpc.ts | 4 +- packages/hot-updater/src/commands/deploy.ts | 20 ++-- packages/hot-updater/src/commands/prune.ts | 4 +- plugins/cloudflare/package.json | 4 +- plugins/cloudflare/src/index.ts | 1 + plugins/cloudflare/src/r2Storage.ts | 113 ++++++++++++++++++++ plugins/plugin-core/src/types.ts | 6 +- pnpm-lock.yaml | 10 +- 8 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 plugins/cloudflare/src/r2Storage.ts diff --git a/packages/console/src-server/rpc.ts b/packages/console/src-server/rpc.ts index 4bbd0803..45651eeb 100644 --- a/packages/console/src-server/rpc.ts +++ b/packages/console/src-server/rpc.ts @@ -28,9 +28,9 @@ const prepareConfig = async () => { if (!config) { config = await loadConfig(); databasePlugin = - config?.database({ + (await config?.database({ cwd: getCwd(), - }) ?? null; + })) ?? null; } return { config, databasePlugin }; }; diff --git a/packages/hot-updater/src/commands/deploy.ts b/packages/hot-updater/src/commands/deploy.ts index 547ac3de..a96bbb6b 100644 --- a/packages/hot-updater/src/commands/deploy.ts +++ b/packages/hot-updater/src/commands/deploy.ts @@ -96,15 +96,17 @@ export const deploy = async (options: DeployOptions) => { let fileUrl: string; let fileHash: string; - const buildPlugin = config.build({ - cwd, - }); - const storagePlugin = config.storage({ - cwd, - }); - const databasePlugin = config.database({ - cwd, - }); + const [buildPlugin, storagePlugin, databasePlugin] = await Promise.all([ + config.build({ + cwd, + }), + config.storage({ + cwd, + }), + config.database({ + cwd, + }), + ]); try { await p.tasks([ diff --git a/packages/hot-updater/src/commands/prune.ts b/packages/hot-updater/src/commands/prune.ts index fd50e243..a86725df 100644 --- a/packages/hot-updater/src/commands/prune.ts +++ b/packages/hot-updater/src/commands/prune.ts @@ -14,7 +14,7 @@ export const prune = async () => { const cwd = getCwd(); - const databasePlugin = config.database({ + const databasePlugin = await config.database({ cwd, }); @@ -35,7 +35,7 @@ export const prune = async () => { await databasePlugin.commitBundle(); await databasePlugin.onUnmount?.(); - const storagePlugin = config.storage({ + const storagePlugin = await config.storage({ cwd, }); diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index cfc29080..bd58782d 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -35,7 +35,9 @@ "dependencies": { "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10", - "cloudflare": "^4.0.0" + "cloudflare": "^4.0.0", + "@aws-sdk/client-s3": "^3.685.0", + "@aws-sdk/lib-storage": "^3.685.0" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", diff --git a/plugins/cloudflare/src/index.ts b/plugins/cloudflare/src/index.ts index 5366448b..b1d97b6c 100644 --- a/plugins/cloudflare/src/index.ts +++ b/plugins/cloudflare/src/index.ts @@ -1 +1,2 @@ export * from "./d1Database"; +export * from "./r2Storage"; diff --git a/plugins/cloudflare/src/r2Storage.ts b/plugins/cloudflare/src/r2Storage.ts new file mode 100644 index 00000000..780b3f3a --- /dev/null +++ b/plugins/cloudflare/src/r2Storage.ts @@ -0,0 +1,113 @@ +import path from "path"; +import { + DeleteObjectsCommand, + ListObjectsV2Command, + S3Client, +} from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; +import type { + BasePluginArgs, + StoragePlugin, + StoragePluginHooks, +} from "@hot-updater/plugin-core"; +import fs from "fs/promises"; +import mime from "mime"; + +import Cloudflare from "cloudflare"; + +export interface R2StorageConfig { + cloudflareApiToken: string; + accountId: string; + bucketName: string; + accessKeyId: string; +} + +export const r2Storage = + (config: R2StorageConfig, hooks?: StoragePluginHooks) => + async (_: BasePluginArgs): Promise => { + const { bucketName } = config; + const cloudflare = new Cloudflare({ + apiToken: config.cloudflareApiToken, + }); + + const credentials = await cloudflare.r2.temporaryCredentials.create({ + account_id: config.accountId, + bucket: config.bucketName, + ttlSeconds: 60 * 10, // 10 minutes + parentAccessKeyId: config.accessKeyId, + permission: "object-read-write", + }); + + if (!credentials.accessKeyId || !credentials.secretAccessKey) { + throw new Error("Failed to create temporary credentials"); + } + + const client = new S3Client({ + region: "us-east-1", + credentials: { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + }, + endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`, + }); + + return { + name: "r2Storage", + async deleteBundle(bundleId) { + const Key = [bundleId].join("/"); + + const listCommand = new ListObjectsV2Command({ + Bucket: bucketName, + Prefix: bundleId, + }); + const listResponse = await client.send(listCommand); + + if (listResponse.Contents && listResponse.Contents.length > 0) { + const objectsToDelete = listResponse.Contents.map((obj) => ({ + Key: obj.Key, + })); + + const deleteParams = { + Bucket: bucketName, + Delete: { + Objects: objectsToDelete, + Quiet: true, + }, + }; + + const deleteCommand = new DeleteObjectsCommand(deleteParams); + await client.send(deleteCommand); + return Key; + } + + throw new Error("Bundle Not Found"); + }, + async uploadBundle(bundleId, bundlePath) { + const Body = await fs.readFile(bundlePath); + const ContentType = mime.getType(bundlePath) ?? void 0; + + const filename = path.basename(bundlePath); + + const Key = [bundleId, filename].join("/"); + const upload = new Upload({ + client, + params: { + ContentType, + Bucket: bucketName, + Key, + Body, + }, + }); + const response = await upload.done(); + if (!response.Location || !response.Key) { + throw new Error("Upload Failed"); + } + + hooks?.onStorageUploaded?.(); + return { + fileUrl: hooks?.transformFileUrl?.(response.Key) ?? response.Location, + }; + }, + }; + }; diff --git a/plugins/plugin-core/src/types.ts b/plugins/plugin-core/src/types.ts index 4be8b48b..3203894c 100644 --- a/plugins/plugin-core/src/types.ts +++ b/plugins/plugin-core/src/types.ts @@ -68,7 +68,7 @@ export type Config = { */ port?: number; }; - build: (args: BasePluginArgs) => BuildPlugin; - storage: (args: BasePluginArgs) => StoragePlugin; - database: (args: BasePluginArgs) => DatabasePlugin; + build: (args: BasePluginArgs) => Promise | BuildPlugin; + storage: (args: BasePluginArgs) => Promise | StoragePlugin; + database: (args: BasePluginArgs) => Promise | DatabasePlugin; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7e04276..ab980c5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -686,6 +686,12 @@ importers: plugins/cloudflare: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.685.0 + version: 3.685.0 + '@aws-sdk/lib-storage': + specifier: ^3.685.0 + version: 3.685.0(@aws-sdk/client-s3@3.685.0) '@hot-updater/core': specifier: workspace:* version: link:../../packages/core @@ -15326,9 +15332,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - bufferutil - supports-color - - utf-8-validate '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))': dependencies: @@ -15339,7 +15343,9 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' + - bufferutil - supports-color + - utf-8-validate optional: true '@react-native/metro-config@0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))': From 88dc7e609068b460875fb076f10f27f3c9012ed6 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 02:39:23 +0900 Subject: [PATCH 22/61] fix: cf --- .../init/cloudflareD1R2Worker/index.ts | 7 +++++ plugins/cloudflare/src/d1Database.ts | 30 ++++++++----------- plugins/cloudflare/src/r2Storage.ts | 4 +-- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 05c2bbdc..00085d5e 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -64,6 +64,13 @@ const cloudflareApi = { export const initCloudflareD1R2Worker = async () => { // TODO: + // 자동으로 채울 수 있는 토큰 + // cloudflareApiToken | ❌ + // accountId (Common) | ✅ 지원 | wrangler whoami + // databaseId (D1) | ✅ 지원 | wrangler d1 list or cloudflare api + // bucketName (R2) | ✅ 지원 | wrangler r2 bucket list or cloudflare api + // accessKeyId (R2) | ❌ + // 1. Get Cloudflare API Token (R2 + D1 + Worker) // 2. Create R2 Bucket (allow public access) // 3. Create D1 Database diff --git a/plugins/cloudflare/src/d1Database.ts b/plugins/cloudflare/src/d1Database.ts index 248301da..b455bbb9 100644 --- a/plugins/cloudflare/src/d1Database.ts +++ b/plugins/cloudflare/src/d1Database.ts @@ -19,7 +19,7 @@ export interface D1DatabaseConfig { export const d1Database = (config: D1DatabaseConfig, hooks?: DatabasePluginHooks) => (_: BasePluginArgs): DatabasePlugin => { - const cloudflare = new Cloudflare({ + const cf = new Cloudflare({ apiToken: config.cloudflareApiToken, }); @@ -75,7 +75,7 @@ export const d1Database = VALUES ${valuesSql};`); - await cloudflare.d1.database.query(config.databaseId, { + await cf.d1.database.query(config.databaseId, { account_id: config.accountId, sql, @@ -120,14 +120,11 @@ export const d1Database = /* sql */ ` SELECT * FROM bundles WHERE id = ? LIMIT 1`, ); - const [response] = await cloudflare.d1.database.query( - config.databaseId, - { - account_id: config.accountId, - sql, - params: [bundleId], - }, - ); + const [response] = await cf.d1.database.query(config.databaseId, { + account_id: config.accountId, + sql, + params: [bundleId], + }); if (!response.success) { return null; @@ -174,14 +171,11 @@ export const d1Database = `, ); - const [response] = await cloudflare.d1.database.query( - config.databaseId, - { - account_id: config.accountId, - sql, - params: [], - }, - ); + const [response] = await cf.d1.database.query(config.databaseId, { + account_id: config.accountId, + sql, + params: [], + }); if (!response.success) { bundles = []; diff --git a/plugins/cloudflare/src/r2Storage.ts b/plugins/cloudflare/src/r2Storage.ts index 780b3f3a..8000c4af 100644 --- a/plugins/cloudflare/src/r2Storage.ts +++ b/plugins/cloudflare/src/r2Storage.ts @@ -26,11 +26,11 @@ export const r2Storage = (config: R2StorageConfig, hooks?: StoragePluginHooks) => async (_: BasePluginArgs): Promise => { const { bucketName } = config; - const cloudflare = new Cloudflare({ + const cf = new Cloudflare({ apiToken: config.cloudflareApiToken, }); - const credentials = await cloudflare.r2.temporaryCredentials.create({ + const credentials = await cf.r2.temporaryCredentials.create({ account_id: config.accountId, bucket: config.bucketName, ttlSeconds: 60 * 10, // 10 minutes From b778377b50c9d064921706a87096686769aaca44 Mon Sep 17 00:00:00 2001 From: gronxb Date: Thu, 30 Jan 2025 19:30:40 +0900 Subject: [PATCH 23/61] feat(r2Storage): use wrangler --- packages/hot-updater/package.json | 3 + .../getWranglerLoginAuthToken.ts | 43 +++++++ .../init/cloudflareD1R2Worker/index.ts | 59 ++++++++- plugins/cloudflare/src/r2Storage.ts | 119 +++++++----------- .../cloudflare/src/utils/createWrangler.ts | 16 +++ plugins/cloudflare/worker/wrangler.json | 7 -- pnpm-lock.yaml | 42 +++++++ 7 files changed, 203 insertions(+), 86 deletions(-) create mode 100644 packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts create mode 100644 plugins/cloudflare/src/utils/createWrangler.ts diff --git a/packages/hot-updater/package.json b/packages/hot-updater/package.json index edb0eb08..e275976e 100644 --- a/packages/hot-updater/package.json +++ b/packages/hot-updater/package.json @@ -63,6 +63,8 @@ "@types/plist": "^3.0.5", "@types/semver": "^7.5.8", "boxen": "^8.0.1", + "cloudflare": "^4.0.0", + "xdg-app-paths": "^8.3.0", "execa": "^9.5.2", "is-port-reachable": "^4.0.0", "jszip": "^3.10.1", @@ -71,6 +73,7 @@ "plist": "^3.1.0", "read-package-up": "^11.0.0", "semver": "^7.6.3", + "toml": "^3.0.0", "uuidv7": "^1.0.2" }, "peerDependencies": { diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts new file mode 100644 index 00000000..834e7988 --- /dev/null +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts @@ -0,0 +1,43 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import toml from "toml"; + +import xdgAppPaths from "xdg-app-paths"; + +const isDirectory = (configPath: string) => { + try { + return fs.statSync(configPath).isDirectory(); + } catch (error) { + return false; + } +}; + +const getGlobalWranglerConfigPath = () => { + const configDir = xdgAppPaths(".wrangler").config(); + const legacyConfigDir = path.join(os.homedir(), ".wrangler"); + + if (isDirectory(legacyConfigDir)) { + return legacyConfigDir; + } + + return configDir; +}; + +export const getWranglerLoginAuthToken = (): { + oauth_token: string; + expiration_time: string; + refresh_token: string; + scopes: string[]; +} => { + try { + const wranglerConfigPath = getGlobalWranglerConfigPath(); + const wranglerConfig = fs.readFileSync( + path.join(wranglerConfigPath, "config", "default.toml"), + "utf8", + ); + return toml.parse(wranglerConfig); + } catch (error) { + throw new Error("'npx wrangler login' is required to use this command"); + } +}; diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 00085d5e..0ac3ecf0 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -2,6 +2,9 @@ import * as p from "@clack/prompts"; import { execa } from "execa"; import { parseR2Output } from "./parseR2Output"; +import { Cloudflare } from "cloudflare"; +import { getWranglerLoginAuthToken } from "./getWranglerLoginAuthToken"; + const cloudflareApi = { getR2List: async () => { const { stdout } = await execa( @@ -65,16 +68,60 @@ const cloudflareApi = { export const initCloudflareD1R2Worker = async () => { // TODO: // 자동으로 채울 수 있는 토큰 - // cloudflareApiToken | ❌ - // accountId (Common) | ✅ 지원 | wrangler whoami - // databaseId (D1) | ✅ 지원 | wrangler d1 list or cloudflare api - // bucketName (R2) | ✅ 지원 | wrangler r2 bucket list or cloudflare api - // accessKeyId (R2) | ❌ + // npx wrangler login --scopes account:read user:read d1:write workers:write + // https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/get/ + // cloudflareApiToken | ❌ | npx wrangler login --scopes "d1:write" Account API Tokens Write 로 로그인해서 꺼내오고 토큰 발급 및 생성하기 + // accountId (Common) | ✅ 지원 | await cf.accounts.list(); + // databaseId (D1) | ✅ 지원 | cf.d1.database.list, cf.d1.database.create + // bucketName (R2) | ✅ 지원 | cf.r2.buckets.list , cf.r2.buckets.create // 1. Get Cloudflare API Token (R2 + D1 + Worker) // 2. Create R2 Bucket (allow public access) // 3. Create D1 Database - // 4. Create Worker + // 4. Select API Token + // 5. Create Worker + + const wranglerConfigPath = getWranglerLoginAuthToken(); + console.log(wranglerConfigPath.oauth_token); + + const cf = new Cloudflare({ + apiToken: wranglerConfigPath.oauth_token, + }); + + const accounts = await cf.accounts.list(); + + const d1List2 = await cf.d1.database.list({ + account_id: accounts.result[0]!.id, + }); + console.log("d1List2", d1List2.result); + + // const r2List2 = await cf.r2.buckets.list({ + // account_id: accounts.result[0]!.id, + // }); + + console.log( + "r2List2", + await cf.r2.buckets.get("bundle", { account_id: accounts.result[0]!.id }), + ); + console.log( + "r2List2", + await cf.r2.buckets.domains.managed.list("bundle", { + account_id: accounts.result[0]!.id, + }), + ); + + await cf.r2.buckets.domains.managed.update("bundle", { + account_id: accounts.result[0]!.id, + enabled: true, + }); + + console.log("cf.apiToken", cf.apiToken, cf.apiKey); + cf.r2.buckets.lifecycle.get; + + const tokens = await cf.accounts.tokens.list({ + account_id: accounts.result[0]!.id, + }); + console.log("tokens", tokens.result); const s = p.spinner(); const createKey = `create/${Math.random().toString(36).substring(2, 15)}`; diff --git a/plugins/cloudflare/src/r2Storage.ts b/plugins/cloudflare/src/r2Storage.ts index 8000c4af..f41b2693 100644 --- a/plugins/cloudflare/src/r2Storage.ts +++ b/plugins/cloudflare/src/r2Storage.ts @@ -1,112 +1,85 @@ import path from "path"; -import { - DeleteObjectsCommand, - ListObjectsV2Command, - S3Client, -} from "@aws-sdk/client-s3"; -import { Upload } from "@aws-sdk/lib-storage"; +import { createWrangler } from "./utils/createWrangler"; + +import mime from "mime"; + import type { BasePluginArgs, StoragePlugin, StoragePluginHooks, } from "@hot-updater/plugin-core"; -import fs from "fs/promises"; -import mime from "mime"; import Cloudflare from "cloudflare"; +import { ExecaError } from "execa"; export interface R2StorageConfig { cloudflareApiToken: string; accountId: string; bucketName: string; - accessKeyId: string; } export const r2Storage = (config: R2StorageConfig, hooks?: StoragePluginHooks) => - async (_: BasePluginArgs): Promise => { - const { bucketName } = config; + (_: BasePluginArgs): StoragePlugin => { + const { bucketName, cloudflareApiToken, accountId } = config; const cf = new Cloudflare({ - apiToken: config.cloudflareApiToken, - }); - - const credentials = await cf.r2.temporaryCredentials.create({ - account_id: config.accountId, - bucket: config.bucketName, - ttlSeconds: 60 * 10, // 10 minutes - parentAccessKeyId: config.accessKeyId, - permission: "object-read-write", + apiToken: cloudflareApiToken, }); - - if (!credentials.accessKeyId || !credentials.secretAccessKey) { - throw new Error("Failed to create temporary credentials"); - } - - const client = new S3Client({ - region: "us-east-1", - credentials: { - accessKeyId: credentials.accessKeyId, - secretAccessKey: credentials.secretAccessKey, - sessionToken: credentials.sessionToken, - }, - endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`, + const wrangler = createWrangler({ + cloudflareApiToken: cloudflareApiToken, + cwd: process.cwd(), }); return { name: "r2Storage", async deleteBundle(bundleId) { - const Key = [bundleId].join("/"); - - const listCommand = new ListObjectsV2Command({ - Bucket: bucketName, - Prefix: bundleId, - }); - const listResponse = await client.send(listCommand); - - if (listResponse.Contents && listResponse.Contents.length > 0) { - const objectsToDelete = listResponse.Contents.map((obj) => ({ - Key: obj.Key, - })); - - const deleteParams = { - Bucket: bucketName, - Delete: { - Objects: objectsToDelete, - Quiet: true, - }, - }; - - const deleteCommand = new DeleteObjectsCommand(deleteParams); - await client.send(deleteCommand); - return Key; - } + await wrangler( + "r2", + "object", + "delete", + [bucketName, bundleId].join("/"), + ); throw new Error("Bundle Not Found"); }, async uploadBundle(bundleId, bundlePath) { - const Body = await fs.readFile(bundlePath); - const ContentType = mime.getType(bundlePath) ?? void 0; + const contentType = mime.getType(bundlePath) ?? void 0; const filename = path.basename(bundlePath); const Key = [bundleId, filename].join("/"); - const upload = new Upload({ - client, - params: { - ContentType, - Bucket: bucketName, - Key, - Body, - }, - }); - const response = await upload.done(); - if (!response.Location || !response.Key) { - throw new Error("Upload Failed"); + + try { + await wrangler( + "r2", + "object", + "put", + [bucketName, Key].join("/"), + "--file", + bundlePath, + ...(contentType ? ["--content-type", contentType] : []), + ); + } catch (error) { + if (error instanceof ExecaError) { + throw new Error(error.stderr); + } + + throw error; } hooks?.onStorageUploaded?.(); + + if (hooks?.transformFileUrl) { + return { + fileUrl: hooks.transformFileUrl(Key), + }; + } + + const publicUrl = await cf.r2.buckets.domains.managed.list(bucketName, { + account_id: accountId, + }); return { - fileUrl: hooks?.transformFileUrl?.(response.Key) ?? response.Location, + fileUrl: `https://${publicUrl.domain}/${Key}`, }; }, }; diff --git a/plugins/cloudflare/src/utils/createWrangler.ts b/plugins/cloudflare/src/utils/createWrangler.ts new file mode 100644 index 00000000..41bed44b --- /dev/null +++ b/plugins/cloudflare/src/utils/createWrangler.ts @@ -0,0 +1,16 @@ +import { execa } from "execa"; + +export const createWrangler = ({ + cloudflareApiToken, + cwd, +}: { cloudflareApiToken: string; cwd: string }) => { + const $ = execa({ + extendsEnv: true, + cwd, + env: { + CLOUDFLARE_API_TOKEN: cloudflareApiToken, + }, + }); + + return (...command: string[]) => $("npx", ["wrangler", ...command]); +}; diff --git a/plugins/cloudflare/worker/wrangler.json b/plugins/cloudflare/worker/wrangler.json index 967ac2c7..8d616cec 100644 --- a/plugins/cloudflare/worker/wrangler.json +++ b/plugins/cloudflare/worker/wrangler.json @@ -10,13 +10,6 @@ "observability": { "enabled": true }, - "d1_databases": [ - { - "binding": "DB", - "database_name": "test", - "preview_database_id": "DB", - } - ], /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab980c5a..317d5d60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -612,6 +612,9 @@ importers: boxen: specifier: ^8.0.1 version: 8.0.1 + cloudflare: + specifier: ^4.0.0 + version: 4.0.0(encoding@0.1.13) execa: specifier: ^9.5.2 version: 9.5.2 @@ -636,9 +639,15 @@ importers: semver: specifier: ^7.6.3 version: 7.6.3 + toml: + specifier: ^3.0.0 + version: 3.0.0 uuidv7: specifier: ^1.0.2 version: 1.0.2 + xdg-app-paths: + specifier: ^8.3.0 + version: 8.3.0 packages/react-native: dependencies: @@ -8557,6 +8566,10 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + os-paths@7.4.0: + resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==} + engines: {node: '>= 4.0'} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -10188,6 +10201,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} @@ -10846,6 +10862,14 @@ packages: utf-8-validate: optional: true + xdg-app-paths@8.3.0: + resolution: {integrity: sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ==} + engines: {node: '>= 4.0'} + + xdg-portable@10.6.0: + resolution: {integrity: sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw==} + engines: {node: '>= 4.0'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -22349,6 +22373,10 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + os-paths@7.4.0: + optionalDependencies: + fsevents: 2.3.3 + os-tmpdir@1.0.2: {} oxc-resolver@1.12.0: @@ -24369,6 +24397,8 @@ snapshots: toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.0.0: dependencies: tldts: 6.1.48 @@ -25183,6 +25213,18 @@ snapshots: ws@8.18.0: {} + xdg-app-paths@8.3.0: + dependencies: + xdg-portable: 10.6.0 + optionalDependencies: + fsevents: 2.3.3 + + xdg-portable@10.6.0: + dependencies: + os-paths: 7.4.0 + optionalDependencies: + fsevents: 2.3.3 + xml-name-validator@5.0.0: optional: true From dedbbcc2cdbeca1bd2db50cd05097dd6616b1b2a Mon Sep 17 00:00:00 2001 From: gronxb Date: Fri, 31 Jan 2025 12:28:07 +0900 Subject: [PATCH 24/61] feat: init get cloudflare token --- .../guide/cloudflare/cloudflare-api-token.png | Bin 0 -> 409732 bytes package.json | 3 +- packages/hot-updater/package.json | 6 +- packages/hot-updater/rslib.config.ts | 16 - packages/hot-updater/src/commands/init.ts | 6 +- .../init/cloudflareD1R2Worker/index.ts | 206 ++--- plugins/cloudflare/package.json | 19 +- plugins/cloudflare/rslib.config.ts | 14 - .../src/utils}/getWranglerLoginAuthToken.ts | 0 plugins/cloudflare/src/utils/index.ts | 3 + plugins/cloudflare/tsup.config.ts | 10 + pnpm-lock.yaml | 858 +++++++++++++++++- 12 files changed, 944 insertions(+), 197 deletions(-) create mode 100644 docs/docs/guide/cloudflare/cloudflare-api-token.png delete mode 100644 plugins/cloudflare/rslib.config.ts rename {packages/hot-updater/src/commands/init/cloudflareD1R2Worker => plugins/cloudflare/src/utils}/getWranglerLoginAuthToken.ts (100%) create mode 100644 plugins/cloudflare/src/utils/index.ts create mode 100644 plugins/cloudflare/tsup.config.ts diff --git a/docs/docs/guide/cloudflare/cloudflare-api-token.png b/docs/docs/guide/cloudflare/cloudflare-api-token.png new file mode 100644 index 0000000000000000000000000000000000000000..e4bd745825b41c2af2aa301cd0699d9e42d7304a GIT binary patch literal 409732 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/*I$aZBywO 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 N&MoPisW { }); switch (provider) { - case "supabase": + case "supabase": { await initSupabase(); break; - case "cloudflare-d1-r2-worker": + } + case "cloudflare-d1-r2-worker": { await initCloudflareD1R2Worker(); break; + } default: throw new Error("Invalid provider"); } diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 0ac3ecf0..046ba819 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -1,75 +1,13 @@ import * as p from "@clack/prompts"; +import { getCwd } from "@hot-updater/plugin-core"; import { execa } from "execa"; -import { parseR2Output } from "./parseR2Output"; - -import { Cloudflare } from "cloudflare"; -import { getWranglerLoginAuthToken } from "./getWranglerLoginAuthToken"; - -const cloudflareApi = { - getR2List: async () => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "r2", "bucket", "list"], - {}, - ); - if (!stdout) { - throw new Error(`Failed to run 'wrangler r2 bucket list'`); - } - - return parseR2Output(stdout); - }, - createR2Bucket: async (bucketName: string) => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "r2", "bucket", "create", bucketName], - {}, - ); - if (!stdout) { - throw new Error( - `Failed to run 'wrangler r2 bucket create ${bucketName}'`, - ); - } - - return true; - }, - getD1List: async () => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "d1", "list", "--json"], - {}, - ); - return JSON.parse(stdout) as { - uuid: string; - name: string; - created_at: string; - version: string; - num_tables: number | null; - file_size: number | null; - }[]; - }, - createD1Database: async (databaseName: string) => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "d1", "create", databaseName], - {}, - ); - return stdout; - }, - getR2AccessTokens: async () => { - const { stdout } = await execa( - "npx", - ["-y", "wrangler", "r2", "bucket", "access-tokens", "list"], - {}, - ); - return stdout; - }, -}; export const initCloudflareD1R2Worker = async () => { // TODO: // 자동으로 채울 수 있는 토큰 // npx wrangler login --scopes account:read user:read d1:write workers:write // https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/get/ + // cloudflareApiToken | ❌ | npx wrangler login --scopes "d1:write" Account API Tokens Write 로 로그인해서 꺼내오고 토큰 발급 및 생성하기 // accountId (Common) | ✅ 지원 | await cf.accounts.list(); // databaseId (D1) | ✅ 지원 | cf.d1.database.list, cf.d1.database.create @@ -81,61 +19,74 @@ export const initCloudflareD1R2Worker = async () => { // 4. Select API Token // 5. Create Worker - const wranglerConfigPath = getWranglerLoginAuthToken(); - console.log(wranglerConfigPath.oauth_token); - - const cf = new Cloudflare({ - apiToken: wranglerConfigPath.oauth_token, - }); + const cwd = getCwd(); + + await execa( + "npx", + [ + "wrangler", + "login", + "--scopes", + "account:read", + "user:read", + "d1:write", + "workers:write", + ], + { cwd }, + ); - const accounts = await cf.accounts.list(); + const { Cloudflare, getWranglerLoginAuthToken } = await import( + "@hot-updater/cloudflare/utils" + ); - const d1List2 = await cf.d1.database.list({ - account_id: accounts.result[0]!.id, - }); - console.log("d1List2", d1List2.result); + const auth = getWranglerLoginAuthToken(); - // const r2List2 = await cf.r2.buckets.list({ - // account_id: accounts.result[0]!.id, + // const wrangler = await createWrangler({ + // cloudflareApiToken: auth.oauth_token, + // cwd: getCwd(), // }); - console.log( - "r2List2", - await cf.r2.buckets.get("bundle", { account_id: accounts.result[0]!.id }), - ); - console.log( - "r2List2", - await cf.r2.buckets.domains.managed.list("bundle", { - account_id: accounts.result[0]!.id, - }), - ); - - await cf.r2.buckets.domains.managed.update("bundle", { - account_id: accounts.result[0]!.id, - enabled: true, + const cf = new Cloudflare({ + apiToken: auth.oauth_token, }); - console.log("cf.apiToken", cf.apiToken, cf.apiKey); - cf.r2.buckets.lifecycle.get; + const s = p.spinner(); + const createKey = `create/${Math.random().toString(36).substring(2, 15)}`; - const tokens = await cf.accounts.tokens.list({ - account_id: accounts.result[0]!.id, + s.start("Checking Account List..."); + const accounts = await cf.accounts.list(); + s.stop(); + + const accountId = await p.select({ + message: "Account List", + options: accounts.result.map((account) => ({ + value: account.id, + label: `${account.name} (${account.id})`, + })), }); - console.log("tokens", tokens.result); - const s = p.spinner(); - const createKey = `create/${Math.random().toString(36).substring(2, 15)}`; + if (p.isCancel(accountId)) { + process.exit(1); + } - s.start("Checking R2 List..."); - const r2List = await cloudflareApi.getR2List(); + s.start("Checking R2 Buckets..."); + const buckets = + ( + await cf.r2.buckets.list({ + account_id: accountId, + }) + ).buckets ?? []; + const availableBuckets = buckets.filter((bucket) => bucket.name) as { + name: string; + }[]; s.stop(); - const selectedR2 = await p.select({ + let selectedBucketName = await p.select({ message: "R2 List", options: [ - ...r2List.map((r2) => ({ - value: r2.name, - label: r2.name, + ...availableBuckets.map((bucket) => ({ + value: bucket.name, + label: bucket.name, })), { value: createKey, @@ -144,34 +95,52 @@ export const initCloudflareD1R2Worker = async () => { ], }); - if (p.isCancel(selectedR2)) { + if (p.isCancel(selectedBucketName)) { process.exit(1); } - if (selectedR2 === createKey) { + if (selectedBucketName === createKey) { const name = await p.text({ message: "Enter the name of the new R2 Bucket", }); if (p.isCancel(name)) { process.exit(1); } - // TODO: allow public access - const newR2 = await cloudflareApi.createR2Bucket(name); - p.log.info(`Created new R2 Bucket: ${newR2}`); - } else { - p.log.info(`Selected R2: ${selectedR2}`); + const newR2 = await cf.r2.buckets.create({ + account_id: accountId, + name, + }); + if (!newR2.name) { + throw new Error("Failed to create new R2 Bucket"); + } + + selectedBucketName = newR2.name; } + p.log.info(`Selected R2: ${selectedBucketName}`); + + s.start("Making R2 bucket publicly accessible..."); + await cf.r2.buckets.domains.managed.update(selectedBucketName, { + account_id: accountId, + enabled: true, + }); + s.stop(); s.start("Checking D1 List..."); - const d1List = await cloudflareApi.getD1List(); + const d1List = + (await cf.d1.database.list({ account_id: accountId })).result ?? []; s.stop(); + const availableD1List = d1List.filter((d1) => d1.name || d1.uuid) as { + name: string; + uuid: string; + }[]; + const selectedD1 = await p.select({ message: "D1 List", options: [ - ...d1List.map((d1) => ({ - value: d1.name, - label: d1.name, + ...availableD1List.map((d1) => ({ + value: d1.uuid, + label: `${d1.name} (${d1.uuid})`, })), { value: createKey, @@ -191,11 +160,12 @@ export const initCloudflareD1R2Worker = async () => { if (p.isCancel(name)) { process.exit(1); } - const newD1 = await cloudflareApi.createD1Database(name); + const newD1 = await cf.d1.database.create({ + account_id: accountId, + name, + }); p.log.info(`Created new D1 Database: ${newD1}`); } else { p.log.info(`Selected D1: ${selectedD1}`); } - - // await execa("npx", ["-y", "wrangler", "d1", "migrate", selectedD1]); }; diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index bd58782d..7a6d1d60 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -10,6 +10,11 @@ ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" + }, + "./utils": { + "types": "./dist/utils/index.d.ts", + "import": "./dist/utils/index.js", + "require": "./dist/utils/index.cjs" } }, "license": "MIT", @@ -28,30 +33,32 @@ "package.json" ], "scripts": { - "build": "rslib build", + "build": "tsup", "test:type": "tsc --noEmit", "dev": "wrangler dev worker/src/index.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.685.0", + "@aws-sdk/lib-storage": "^3.685.0", "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10", - "cloudflare": "^4.0.0", - "@aws-sdk/client-s3": "^3.685.0", - "@aws-sdk/lib-storage": "^3.685.0" + "cloudflare": "^4.0.0" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", - "better-sqlite3": "^11.8.1", "@types/better-sqlite3": "^7.6.12", + "better-sqlite3": "^11.8.1", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", "execa": "^9.5.2", "mime": "^4.0.4", "pg-minify": "^1.6.5", "picocolors": "^1.0.0", + "toml": "^3.0.0", "typescript": "^5.5.2", "vitest": "2.1.8", - "wrangler": "^3.101.0" + "wrangler": "^3.101.0", + "xdg-app-paths": "^8.3.0" } } diff --git a/plugins/cloudflare/rslib.config.ts b/plugins/cloudflare/rslib.config.ts deleted file mode 100644 index cd505720..00000000 --- a/plugins/cloudflare/rslib.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "@rslib/core"; - -export default defineConfig({ - lib: [ - { - format: "esm", - dts: true, - }, - { - format: "cjs", - dts: true, - }, - ], -}); diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts b/plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts similarity index 100% rename from packages/hot-updater/src/commands/init/cloudflareD1R2Worker/getWranglerLoginAuthToken.ts rename to plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts diff --git a/plugins/cloudflare/src/utils/index.ts b/plugins/cloudflare/src/utils/index.ts new file mode 100644 index 00000000..eea9497c --- /dev/null +++ b/plugins/cloudflare/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./getWranglerLoginAuthToken.js"; +export * from "./createWrangler.js"; +export { Cloudflare } from "cloudflare"; diff --git a/plugins/cloudflare/tsup.config.ts b/plugins/cloudflare/tsup.config.ts new file mode 100644 index 00000000..831c9b72 --- /dev/null +++ b/plugins/cloudflare/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/utils/index.ts"], + format: ["esm"], + dts: true, + banner: { + js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 317d5d60..4c545963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,14 +28,17 @@ importers: specifier: ^20.3.0 version: 20.3.0(@babel/traverse@7.25.9)(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(nx@20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)))(typescript@5.7.2) '@rslib/core': - specifier: ^0.2.2 - version: 0.2.2(typescript@5.7.2) + specifier: ^0.4.0 + version: 0.4.0(typescript@5.7.2) nx: specifier: 20.3.0 version: 20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)) rimraf: specifier: ^5.0.7 version: 5.0.7 + tsup: + specifier: ^8.3.6 + version: 8.3.6(@swc/core@1.7.42(@swc/helpers@0.5.15))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -53,7 +56,7 @@ importers: version: 18.19.33 rspress: specifier: ^1.40.0 - version: 1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + version: 1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) examples/v0.71.19: dependencies: @@ -169,7 +172,7 @@ importers: version: 7.26.0 '@callstack/repack': specifier: ^4.0.0 - version: 4.0.0(@react-native-community/cli-debugger-ui@15.0.1)(@react-native-community/cli-server-api@15.0.1)(@react-native-community/cli-types@15.0.1)(@react-native-community/cli@15.0.1(typescript@5.7.2))(encoding@0.1.13)(react-native@0.74.1(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))(@types/react@18.3.11)(encoding@0.1.13)(react@18.2.0))(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + version: 4.0.0(@react-native-community/cli-debugger-ui@15.0.1)(@react-native-community/cli-server-api@15.0.1)(@react-native-community/cli-types@15.0.1)(@react-native-community/cli@15.0.1(typescript@5.7.2))(encoding@0.1.13)(react-native@0.74.1(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))(@types/react@18.3.11)(encoding@0.1.13)(react@18.2.0))(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@hot-updater/aws': specifier: workspace:* version: link:../../plugins/aws @@ -214,7 +217,7 @@ importers: version: 29.7.0(@babel/core@7.26.0) babel-loader: specifier: ^9.1.3 - version: 9.1.3(@babel/core@7.26.0)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + version: 9.1.3(@babel/core@7.26.0)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -241,10 +244,10 @@ importers: version: 18.2.0(react@18.2.0) terser-webpack-plugin: specifier: ^5.3.10 - version: 5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + version: 5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) webpack: specifier: ^5.91.0 - version: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + version: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) examples/v0.76.1-new-arch: dependencies: @@ -554,6 +557,9 @@ importers: '@clack/prompts': specifier: ^0.9.0 version: 0.9.0 + '@hot-updater/cloudflare': + specifier: workspace:* + version: link:../../plugins/cloudflare '@hot-updater/console': specifier: workspace:* version: link:../console @@ -741,6 +747,9 @@ importers: picocolors: specifier: ^1.0.0 version: 1.1.1 + toml: + specifier: ^3.0.0 + version: 3.0.0 typescript: specifier: ^5.5.2 version: 5.7.2 @@ -750,6 +759,9 @@ importers: wrangler: specifier: ^3.101.0 version: 3.105.1(@cloudflare/workers-types@4.20250124.3) + xdg-app-paths: + specifier: ^8.3.0 + version: 8.3.0 plugins/js: dependencies: @@ -2401,6 +2413,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.17.19': resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -2413,6 +2431,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.17.19': resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} engines: {node: '>=12'} @@ -2425,6 +2449,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.17.19': resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} engines: {node: '>=12'} @@ -2437,6 +2467,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.17.19': resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} engines: {node: '>=12'} @@ -2449,6 +2485,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.17.19': resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} engines: {node: '>=12'} @@ -2461,6 +2503,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.17.19': resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} engines: {node: '>=12'} @@ -2473,6 +2521,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.17.19': resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} engines: {node: '>=12'} @@ -2485,6 +2539,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.17.19': resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} engines: {node: '>=12'} @@ -2497,6 +2557,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.17.19': resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} engines: {node: '>=12'} @@ -2509,6 +2575,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.17.19': resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} engines: {node: '>=12'} @@ -2521,6 +2593,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.17.19': resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} engines: {node: '>=12'} @@ -2533,6 +2611,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.17.19': resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} engines: {node: '>=12'} @@ -2545,6 +2629,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.17.19': resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} engines: {node: '>=12'} @@ -2557,6 +2647,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.17.19': resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} engines: {node: '>=12'} @@ -2569,6 +2665,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.17.19': resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} engines: {node: '>=12'} @@ -2581,6 +2683,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.17.19': resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} engines: {node: '>=12'} @@ -2593,6 +2701,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.17.19': resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} engines: {node: '>=12'} @@ -2605,6 +2725,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.17.19': resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} engines: {node: '>=12'} @@ -2617,6 +2749,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.17.19': resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} engines: {node: '>=12'} @@ -2629,6 +2767,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.17.19': resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} engines: {node: '>=12'} @@ -2641,6 +2785,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.17.19': resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} engines: {node: '>=12'} @@ -2653,6 +2803,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.17.19': resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} engines: {node: '>=12'} @@ -2665,6 +2821,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2921,18 +3083,33 @@ packages: peerDependencies: react: '>=16' + '@module-federation/error-codes@0.8.4': + resolution: {integrity: sha512-55LYmrDdKb4jt+qr8qE8U3al62ZANp3FhfVaNPOaAmdTh0jHdD8M3yf5HKFlr5xVkVO4eV/F/J2NCfpbh+pEXQ==} + '@module-federation/runtime-tools@0.5.1': resolution: {integrity: sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==} + '@module-federation/runtime-tools@0.8.4': + resolution: {integrity: sha512-fjVOsItJ1u5YY6E9FnS56UDwZgqEQUrWFnouRiPtK123LUuqUI9FH4redZoKWlE1PB0ir1Z3tnqy8eFYzPO38Q==} + '@module-federation/runtime@0.5.1': resolution: {integrity: sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==} + '@module-federation/runtime@0.8.4': + resolution: {integrity: sha512-yZeZ7z2Rx4gv/0E97oLTF3V6N25vglmwXGgoeju/W2YjsFvWzVtCDI7zRRb0mJhU6+jmSM8jP1DeQGbea/AiZQ==} + '@module-federation/sdk@0.5.1': resolution: {integrity: sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==} + '@module-federation/sdk@0.8.4': + resolution: {integrity: sha512-waABomIjg/5m1rPDBWYG4KUhS5r7OUUY7S+avpaVIY/tkPWB3ibRDKy2dNLLAMaLKq0u+B1qIdEp4NIWkqhqpg==} + '@module-federation/webpack-bundler-runtime@0.5.1': resolution: {integrity: sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==} + '@module-federation/webpack-bundler-runtime@0.8.4': + resolution: {integrity: sha512-HggROJhvHPUX7uqBD/XlajGygMNM1DG0+4OAkk8MBQe4a18QzrRNzZt6XQbRTSG4OaEoyRWhQHvYD3Yps405tQ==} + '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -3643,81 +3820,176 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.32.1': + resolution: {integrity: sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.22.5': resolution: {integrity: sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.32.1': + resolution: {integrity: sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.22.5': resolution: {integrity: sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.32.1': + resolution: {integrity: sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.22.5': resolution: {integrity: sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.32.1': + resolution: {integrity: sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.32.1': + resolution: {integrity: sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.32.1': + resolution: {integrity: sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.22.5': resolution: {integrity: sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.32.1': + resolution: {integrity: sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.22.5': resolution: {integrity: sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.32.1': + resolution: {integrity: sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.22.5': resolution: {integrity: sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.32.1': + resolution: {integrity: sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.22.5': resolution: {integrity: sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.32.1': + resolution: {integrity: sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.32.1': + resolution: {integrity: sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': resolution: {integrity: sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.32.1': + resolution: {integrity: sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.22.5': resolution: {integrity: sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.32.1': + resolution: {integrity: sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.22.5': resolution: {integrity: sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.32.1': + resolution: {integrity: sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.22.5': resolution: {integrity: sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.32.1': + resolution: {integrity: sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.22.5': resolution: {integrity: sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.32.1': + resolution: {integrity: sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.22.5': resolution: {integrity: sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.32.1': + resolution: {integrity: sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.22.5': resolution: {integrity: sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.32.1': + resolution: {integrity: sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.22.5': resolution: {integrity: sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.32.1': + resolution: {integrity: sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==} + cpu: [x64] + os: [win32] + '@rsbuild/core@1.1.13': resolution: {integrity: sha512-XBL2hrin8731W6iTGGL+x3cv07n4vm2D7u6XHRwtQkRfySzAqGx7ThlQLdNX/dJwfsoQrYQuWl/qzaljjXtGtg==} engines: {node: '>=16.7.0'} @@ -3728,6 +4000,11 @@ packages: engines: {node: '>=16.7.0'} hasBin: true + '@rsbuild/core@1.2.3': + resolution: {integrity: sha512-lUCt8gQe9E2PI3srcEJ1Na3GQYmsYuvAqK0f/k00HM0pEjrbOFC9Xq2kR85UoXHFqlTCIw/fLLDe91PKRCbKAw==} + engines: {node: '>=16.7.0'} + hasBin: true + '@rsbuild/plugin-babel@1.0.3': resolution: {integrity: sha512-3S/ykXv7KRo0FxVpkjoHFUwB04nKINIET1kuv4xiRaDmeww1Tp0wl9h4u8a7d7gU/4FllyoUflY8TVhci/o05g==} peerDependencies: @@ -3753,8 +4030,8 @@ packages: peerDependencies: '@rsbuild/core': 1.x - '@rslib/core@0.2.2': - resolution: {integrity: sha512-u4qKfoO2YAdtoga6NqCDcTfvqyaTZj/L0kZjDrbThMcD51qUb8HiCS8pX5Hwj5v4doGkk+rHeQnw0Ad7HyMPMQ==} + '@rslib/core@0.4.0': + resolution: {integrity: sha512-dONhz7PsooyGUdSQbJRzlsCXgOKrP5NyW1dzcNBvfLHr13nd2anCCZzFdQXb/g7772h5moj+EkTEo982AmUeJQ==} engines: {node: '>=16.0.0'} hasBin: true peerDependencies: @@ -3776,6 +4053,11 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@1.2.2': + resolution: {integrity: sha512-h23F8zEkXWhwMeScm0ZnN78Zh7hCDalxIWsm7bBS0eKadnlegUDwwCF8WE+8NjWr7bRzv0p3QBWlS5ufkcL4eA==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@1.1.4': resolution: {integrity: sha512-ECmcTJecXwqhqqpOjfjIcwDp8UqJ3D1Crc2COG+wUfwejtbqs9twUTaufZz7U7xna+oyfJ4GRNSKS9xs4N2kPQ==} cpu: [x64] @@ -3786,6 +4068,11 @@ packages: cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@1.2.2': + resolution: {integrity: sha512-vG5s7FkEvwrGLfksyDRHwKAHUkhZt1zHZZXJQn4gZKjTBonje8ezdc7IFlDiWpC4S+oBYp73nDWkUzkGRbSdcQ==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@1.1.4': resolution: {integrity: sha512-QVRFV6+z1DopeGn8rLRbBNrWXuBvp7J19lTtvx9F5mItzTiBVHmNqFt31namm59xkhHJ3leng7l2F39qcOMOIA==} cpu: [arm64] @@ -3796,6 +4083,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-VykY/kiYOzO8E1nYzfJ9+gQEHxb5B6lt5wa8M6xFi5B6jEGU+OsaGskmAZB9/GFImeFDHxDPvhUalI4R9p8O2Q==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-arm64-musl@1.1.4': resolution: {integrity: sha512-UCateQWfEpEyFOC/tkuicXpIm81s5DavcwKjX3wwh1JB/KO6UxGHK8F75BV4K3Coo/UEiNCvL8xrB7eDjMDwYg==} cpu: [arm64] @@ -3806,6 +4098,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-musl@1.2.2': + resolution: {integrity: sha512-Z5vAC4wGfXi8XXZ6hs8Q06TYjr3zHf819HB4DI5i4C1eQTeKdZSyoFD0NHFG23bP4NWJffp8KhmoObcy9jBT5Q==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-x64-gnu@1.1.4': resolution: {integrity: sha512-Nz5Bt1PDxebVRU321h9AGkLRQL5n9Xgt+rluWAXLVtyxM9aPavmvu1n6/G9stXwQdnUVrZIUs7EzhIhWBe5R/A==} cpu: [x64] @@ -3816,6 +4113,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-o3pDaL+cH5EeRbDE9gZcdZpBgp5iXvYZBBhe8vZQllYgI4zN5MJEuleV7WplG3UwTXlgZg3Kht4RORSOPn96vg==} + cpu: [x64] + os: [linux] + '@rspack/binding-linux-x64-musl@1.1.4': resolution: {integrity: sha512-1XG795a+M4vE7JLyeDa1oktr4WU/I5IQ9d8Vk3PVtF59IxnKpjOb3vdGhXP7Ke2zWP6C2YQzWotez6AbO3uWeA==} cpu: [x64] @@ -3826,6 +4128,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-RE3e0xe4DdchHssttKzryDwjLkbrNk/4H59TkkWeGYJcLw41tmcOZVFQUOwKLUvXWVyif/vjvV/w1SMlqB4wQg==} + cpu: [x64] + os: [linux] + '@rspack/binding-win32-arm64-msvc@1.1.4': resolution: {integrity: sha512-1hXOgHxnrBmjBNUluy9MbFMQi4lnAS199JD5UHRc2mx5i+D8cjAiBsWHzIcK4xzsxu2IYgVGsJcdIA8/zV/PVQ==} cpu: [arm64] @@ -3836,6 +4143,11 @@ packages: cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@1.2.2': + resolution: {integrity: sha512-R+PKBYn6uzTaDdVqTHvjqiJPBr5ZHg1wg5UmFDLNH9OklzVFyQh1JInSdJRb7lzfzTRz6bEkkwUFBPQK/CGScw==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@1.1.4': resolution: {integrity: sha512-Jfq6gF5QKOYZsgDYRx+fWipDHmtHxq6jniws1WAE8F5w1qIt/dAHzW3a6+3VwCih9lQEEH3Rimy/ECB0oergng==} cpu: [ia32] @@ -3846,6 +4158,11 @@ packages: cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@1.2.2': + resolution: {integrity: sha512-dBqz3sRAGZ2f31FgzKLDvIRfq2haRP3X3XVCT0PsiMcvt7QJng+26aYYMy2THatd/nM8IwExYeitHWeiMBoruw==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@1.1.4': resolution: {integrity: sha512-d9HUGVfNFhB+r32kDPkzHeMmgwemTFsDymnWJUgUrojzTJ326JrDmEXVeCnAHfwBSC9w4/mp4H+4iGhIdKSo9w==} cpu: [x64] @@ -3856,12 +4173,20 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-eeAvaN831KG553cMSHkVldyk6YQn4ujgRHov6r1wtREq7CD3/ka9LMkJUepCN85K7XtwYT0N4KpFIQyf5GTGoA==} + cpu: [x64] + os: [win32] + '@rspack/binding@1.1.4': resolution: {integrity: sha512-XdR/4DACpLqNjxEw+ikg5FN2dQbOAxj7fDlndNio0l+m8ThPiIsetkJ2FNSlxt/K4SVnIrwAkhE5kabVNKR4EA==} '@rspack/binding@1.1.8': resolution: {integrity: sha512-+/JzXx1HctfgPj+XtsCTbRkxiaOfAXGZZLEvs7jgp04WgWRSZ5u97WRCePNPvy+sCfOEH/2zw2ZK36Z7oQRGhQ==} + '@rspack/binding@1.2.2': + resolution: {integrity: sha512-GCZwpGFYlLTdJ2soPLwjw9z4LSZ+GdpbHNfBt3Cm/f/bAF8n6mZc7dHUqN893RFh7MPU17HNEL3fMw7XR+6pHg==} + '@rspack/core@1.1.4': resolution: {integrity: sha512-JUU1pS11TY3/MwnezBfLLp3+7zfkd4Adzo8Pv4f4R1KoJyX1FYBFBcKnfZBlaGYi2C2e5ZDrrHxQlrPmygjbuw==} engines: {node: '>=16.0.0'} @@ -3880,6 +4205,18 @@ packages: '@swc/helpers': optional: true + '@rspack/core@1.2.2': + resolution: {integrity: sha512-EeHAmY65Uj62hSbUKesbrcWGE7jfUI887RD03G++Gj8jS4WPHEu1TFODXNOXg6pa7zyIvs2BK0Bm16Kwz8AEaQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@rspack/tracing': ^1.x + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@rspack/tracing': + optional: true + '@swc/helpers': + optional: true + '@rspack/lite-tapable@1.0.1': resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} engines: {node: '>=16.0.0'} @@ -5413,6 +5750,12 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -5529,6 +5872,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -5702,6 +6049,10 @@ packages: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -5739,6 +6090,9 @@ packages: core-js@3.39.0: resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + core-js@3.40.0: + resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -6177,6 +6531,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7365,6 +7724,10 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-rslog@0.0.6: + resolution: {integrity: sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A==} + engines: {node: '>=14.17.6'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7564,6 +7927,10 @@ packages: joi@17.11.0: resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7724,6 +8091,10 @@ packages: resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -7778,6 +8149,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -8868,6 +9242,24 @@ packages: ts-node: optional: true + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss-nested@6.0.1: resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} @@ -9251,6 +9643,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + readline@1.3.0: resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} @@ -9446,11 +9842,16 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.32.1: + resolution: {integrity: sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} - rsbuild-plugin-dts@0.2.2: - resolution: {integrity: sha512-RwkVcMwig1+UHkVJFaD6tagjxZOQqIenbkLS+J85bEdKO/ra+YiLC1Gq3DItEv/hU02u5WPgJmQhaQWKb17T9w==} + rsbuild-plugin-dts@0.4.0: + resolution: {integrity: sha512-Yjr+6iSY/TgV2jd+bY8J0wFe3RSBZaj766ygIMmILBacWUmaxnyIXnXjiIR3KDO7fxSwUYZzqqvpSgrRoVCk6g==} engines: {node: '>=16.0.0'} peerDependencies: '@microsoft/api-extractor': ^7 @@ -9862,6 +10263,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -10211,6 +10616,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -10221,6 +10629,10 @@ packages: peerDependencies: tslib: '2' + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -10274,6 +10686,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.3.6: + resolution: {integrity: sha512-XkVtlDV/58S9Ye0JxUUTcrQk4S+EqlOHKzg6Roa62rdjL1nGWNUstG0xgI4vanHdfIpjP448J8vlN0oK6XOJ5g==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsutils@3.21.0: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -10650,6 +11081,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -10716,6 +11150,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -13045,11 +13482,11 @@ snapshots: - supports-color - utf-8-validate - '@callstack/repack@4.0.0(@react-native-community/cli-debugger-ui@15.0.1)(@react-native-community/cli-server-api@15.0.1)(@react-native-community/cli-types@15.0.1)(@react-native-community/cli@15.0.1(typescript@5.7.2))(encoding@0.1.13)(react-native@0.74.1(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))(@types/react@18.3.11)(encoding@0.1.13)(react@18.2.0))(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)))': + '@callstack/repack@4.0.0(@react-native-community/cli-debugger-ui@15.0.1)(@react-native-community/cli-server-api@15.0.1)(@react-native-community/cli-types@15.0.1)(@react-native-community/cli@15.0.1(typescript@5.7.2))(encoding@0.1.13)(react-native@0.74.1(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0))(@types/react@18.3.11)(encoding@0.1.13)(react@18.2.0))(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2))': dependencies: '@callstack/repack-dev-server': 4.0.0(@react-native-community/cli-debugger-ui@15.0.1)(@react-native-community/cli-server-api@15.0.1)(encoding@0.1.13) '@discoveryjs/json-ext': 0.5.7 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.13(react-refresh@0.14.2)(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.13(react-refresh@0.14.2)(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@react-native-community/cli': 15.0.1(typescript@5.7.2) '@react-native-community/cli-types': 15.0.1 colorette: 1.4.0 @@ -13072,7 +13509,7 @@ snapshots: schema-utils: 3.3.0 shallowequal: 1.1.0 tapable: 2.2.1 - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) transitivePeerDependencies: - '@react-native-community/cli-debugger-ui' - '@react-native-community/cli-server-api' @@ -13177,138 +13614,213 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.24.2': + optional: true + '@esbuild/android-arm64@0.17.19': optional: true '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.24.2': + optional: true + '@esbuild/android-arm@0.17.19': optional: true '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.24.2': + optional: true + '@esbuild/android-x64@0.17.19': optional: true '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.24.2': + optional: true + '@esbuild/darwin-arm64@0.17.19': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.24.2': + optional: true + '@esbuild/darwin-x64@0.17.19': optional: true '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.24.2': + optional: true + '@esbuild/freebsd-arm64@0.17.19': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.24.2': + optional: true + '@esbuild/freebsd-x64@0.17.19': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.24.2': + optional: true + '@esbuild/linux-arm64@0.17.19': optional: true '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.24.2': + optional: true + '@esbuild/linux-arm@0.17.19': optional: true '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.24.2': + optional: true + '@esbuild/linux-ia32@0.17.19': optional: true '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.24.2': + optional: true + '@esbuild/linux-loong64@0.17.19': optional: true '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.24.2': + optional: true + '@esbuild/linux-mips64el@0.17.19': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.24.2': + optional: true + '@esbuild/linux-ppc64@0.17.19': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.24.2': + optional: true + '@esbuild/linux-riscv64@0.17.19': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.24.2': + optional: true + '@esbuild/linux-s390x@0.17.19': optional: true '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.24.2': + optional: true + '@esbuild/linux-x64@0.17.19': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + '@esbuild/netbsd-x64@0.17.19': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + '@esbuild/openbsd-x64@0.17.19': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.24.2': + optional: true + '@esbuild/sunos-x64@0.17.19': optional: true '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.24.2': + optional: true + '@esbuild/win32-arm64@0.17.19': optional: true '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.24.2': + optional: true + '@esbuild/win32-ia32@0.17.19': optional: true '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.24.2': + optional: true + '@esbuild/win32-x64@0.17.19': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.24.2': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -13761,11 +14273,11 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@mdx-js/loader@2.3.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)))': + '@mdx-js/loader@2.3.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2))': dependencies: '@mdx-js/mdx': 2.3.0 source-map: 0.7.4 - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) transitivePeerDependencies: - supports-color @@ -13797,22 +14309,43 @@ snapshots: '@types/react': 18.3.11 react: 18.3.1 + '@module-federation/error-codes@0.8.4': {} + '@module-federation/runtime-tools@0.5.1': dependencies: '@module-federation/runtime': 0.5.1 '@module-federation/webpack-bundler-runtime': 0.5.1 + '@module-federation/runtime-tools@0.8.4': + dependencies: + '@module-federation/runtime': 0.8.4 + '@module-federation/webpack-bundler-runtime': 0.8.4 + '@module-federation/runtime@0.5.1': dependencies: '@module-federation/sdk': 0.5.1 + '@module-federation/runtime@0.8.4': + dependencies: + '@module-federation/error-codes': 0.8.4 + '@module-federation/sdk': 0.8.4 + '@module-federation/sdk@0.5.1': {} + '@module-federation/sdk@0.8.4': + dependencies: + isomorphic-rslog: 0.0.6 + '@module-federation/webpack-bundler-runtime@0.5.1': dependencies: '@module-federation/runtime': 0.5.1 '@module-federation/sdk': 0.5.1 + '@module-federation/webpack-bundler-runtime@0.8.4': + dependencies: + '@module-federation/runtime': 0.8.4 + '@module-federation/sdk': 0.8.4 + '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.3.1 @@ -13978,7 +14511,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pmmmwh/react-refresh-webpack-plugin@0.5.13(react-refresh@0.14.2)(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.13(react-refresh@0.14.2)(type-fest@4.23.0)(webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2))': dependencies: ansi-html-community: 0.0.8 core-js-pure: 3.37.1 @@ -13988,10 +14521,10 @@ snapshots: react-refresh: 0.14.2 schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) optionalDependencies: type-fest: 4.23.0 - webpack-dev-server: 5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + webpack-dev-server: 5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@react-native-community/cli-clean@10.1.1(encoding@0.1.13)': dependencies: @@ -15515,51 +16048,108 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.22.5': optional: true + '@rollup/rollup-android-arm-eabi@4.32.1': + optional: true + '@rollup/rollup-android-arm64@4.22.5': optional: true + '@rollup/rollup-android-arm64@4.32.1': + optional: true + '@rollup/rollup-darwin-arm64@4.22.5': optional: true + '@rollup/rollup-darwin-arm64@4.32.1': + optional: true + '@rollup/rollup-darwin-x64@4.22.5': optional: true + '@rollup/rollup-darwin-x64@4.32.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.32.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.32.1': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.22.5': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.32.1': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.22.5': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.32.1': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.22.5': optional: true + '@rollup/rollup-linux-arm64-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-arm64-musl@4.22.5': optional: true + '@rollup/rollup-linux-arm64-musl@4.32.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.22.5': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.22.5': optional: true + '@rollup/rollup-linux-s390x-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-x64-gnu@4.22.5': optional: true + '@rollup/rollup-linux-x64-gnu@4.32.1': + optional: true + '@rollup/rollup-linux-x64-musl@4.22.5': optional: true + '@rollup/rollup-linux-x64-musl@4.32.1': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.22.5': optional: true + '@rollup/rollup-win32-arm64-msvc@4.32.1': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.22.5': optional: true + '@rollup/rollup-win32-ia32-msvc@4.32.1': + optional: true + '@rollup/rollup-win32-x64-msvc@4.22.5': optional: true + '@rollup/rollup-win32-x64-msvc@4.32.1': + optional: true + '@rsbuild/core@1.1.13': dependencies: '@rspack/core': 1.1.8(@swc/helpers@0.5.15) @@ -15574,6 +16164,15 @@ snapshots: '@swc/helpers': 0.5.15 core-js: 3.39.0 + '@rsbuild/core@1.2.3': + dependencies: + '@rspack/core': 1.2.2(@swc/helpers@0.5.15) + '@rspack/lite-tapable': 1.0.1 + '@swc/helpers': 0.5.15 + core-js: 3.40.0 + transitivePeerDependencies: + - '@rspack/tracing' + '@rsbuild/plugin-babel@1.0.3(@rsbuild/core@1.1.6)': dependencies: '@babel/core': 7.26.0 @@ -15620,13 +16219,15 @@ snapshots: - solid-js - supports-color - '@rslib/core@0.2.2(typescript@5.7.2)': + '@rslib/core@0.4.0(typescript@5.7.2)': dependencies: - '@rsbuild/core': 1.1.13 - rsbuild-plugin-dts: 0.2.2(@rsbuild/core@1.1.13)(typescript@5.7.2) + '@rsbuild/core': 1.2.3 + rsbuild-plugin-dts: 0.4.0(@rsbuild/core@1.2.3)(typescript@5.7.2) tinyglobby: 0.2.10 optionalDependencies: typescript: 5.7.2 + transitivePeerDependencies: + - '@rspack/tracing' '@rspack/binding-darwin-arm64@1.1.4': optional: true @@ -15634,54 +16235,81 @@ snapshots: '@rspack/binding-darwin-arm64@1.1.8': optional: true + '@rspack/binding-darwin-arm64@1.2.2': + optional: true + '@rspack/binding-darwin-x64@1.1.4': optional: true '@rspack/binding-darwin-x64@1.1.8': optional: true + '@rspack/binding-darwin-x64@1.2.2': + optional: true + '@rspack/binding-linux-arm64-gnu@1.1.4': optional: true '@rspack/binding-linux-arm64-gnu@1.1.8': optional: true + '@rspack/binding-linux-arm64-gnu@1.2.2': + optional: true + '@rspack/binding-linux-arm64-musl@1.1.4': optional: true '@rspack/binding-linux-arm64-musl@1.1.8': optional: true + '@rspack/binding-linux-arm64-musl@1.2.2': + optional: true + '@rspack/binding-linux-x64-gnu@1.1.4': optional: true '@rspack/binding-linux-x64-gnu@1.1.8': optional: true + '@rspack/binding-linux-x64-gnu@1.2.2': + optional: true + '@rspack/binding-linux-x64-musl@1.1.4': optional: true '@rspack/binding-linux-x64-musl@1.1.8': optional: true + '@rspack/binding-linux-x64-musl@1.2.2': + optional: true + '@rspack/binding-win32-arm64-msvc@1.1.4': optional: true '@rspack/binding-win32-arm64-msvc@1.1.8': optional: true + '@rspack/binding-win32-arm64-msvc@1.2.2': + optional: true + '@rspack/binding-win32-ia32-msvc@1.1.4': optional: true '@rspack/binding-win32-ia32-msvc@1.1.8': optional: true + '@rspack/binding-win32-ia32-msvc@1.2.2': + optional: true + '@rspack/binding-win32-x64-msvc@1.1.4': optional: true '@rspack/binding-win32-x64-msvc@1.1.8': optional: true + '@rspack/binding-win32-x64-msvc@1.2.2': + optional: true + '@rspack/binding@1.1.4': optionalDependencies: '@rspack/binding-darwin-arm64': 1.1.4 @@ -15706,6 +16334,18 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.1.8 '@rspack/binding-win32-x64-msvc': 1.1.8 + '@rspack/binding@1.2.2': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.2.2 + '@rspack/binding-darwin-x64': 1.2.2 + '@rspack/binding-linux-arm64-gnu': 1.2.2 + '@rspack/binding-linux-arm64-musl': 1.2.2 + '@rspack/binding-linux-x64-gnu': 1.2.2 + '@rspack/binding-linux-x64-musl': 1.2.2 + '@rspack/binding-win32-arm64-msvc': 1.2.2 + '@rspack/binding-win32-ia32-msvc': 1.2.2 + '@rspack/binding-win32-x64-msvc': 1.2.2 + '@rspack/core@1.1.4(@swc/helpers@0.5.15)': dependencies: '@module-federation/runtime-tools': 0.5.1 @@ -15724,6 +16364,15 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.15 + '@rspack/core@1.2.2(@swc/helpers@0.5.15)': + dependencies: + '@module-federation/runtime-tools': 0.8.4 + '@rspack/binding': 1.2.2 + '@rspack/lite-tapable': 1.0.1 + caniuse-lite: 1.0.30001680 + optionalDependencies: + '@swc/helpers': 0.5.15 + '@rspack/lite-tapable@1.0.1': {} '@rspack/plugin-react-refresh@1.0.1(react-refresh@0.16.0)': @@ -15733,9 +16382,9 @@ snapshots: optionalDependencies: react-refresh: 0.16.0 - '@rspress/core@1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)))': + '@rspress/core@1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2))': dependencies: - '@mdx-js/loader': 2.3.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + '@mdx-js/loader': 2.3.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@mdx-js/mdx': 2.3.0 '@mdx-js/react': 2.3.0(react@18.3.1) '@rsbuild/core': 1.1.13 @@ -17479,12 +18128,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.1.3(@babel/core@7.26.0)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))): + babel-loader@9.1.3(@babel/core@7.26.0)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@babel/core': 7.26.0 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) babel-plugin-const-enum@1.2.0(@babel/core@7.26.0): dependencies: @@ -17805,6 +18454,11 @@ snapshots: dependencies: run-applescript: 7.0.0 + bundle-require@5.1.0(esbuild@0.24.2): + dependencies: + esbuild: 0.24.2 + load-tsconfig: 0.2.5 + bytes@3.0.0: {} bytes@3.1.2: @@ -17909,6 +18563,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.1 + chownr@1.1.4: {} chrome-launcher@0.15.2: @@ -18088,6 +18746,8 @@ snapshots: transitivePeerDependencies: - supports-color + consola@3.4.0: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -18121,6 +18781,8 @@ snapshots: core-js@3.39.0: {} + core-js@3.40.0: {} + core-util-is@1.0.3: {} cosmiconfig-typescript-loader@5.0.0(@types/node@22.7.5)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): @@ -18691,6 +19353,34 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -20034,6 +20724,8 @@ snapshots: isobject@3.0.1: {} + isomorphic-rslog@0.0.6: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -20574,6 +21266,8 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -20843,6 +21537,8 @@ snapshots: lines-and-columns@2.0.3: {} + load-tsconfig@0.2.5: {} + loader-runner@4.3.0: {} loader-utils@2.0.4: @@ -20888,6 +21584,8 @@ snapshots: lodash.once@4.1.1: {} + lodash.sortby@4.7.0: {} + lodash.throttle@4.1.1: {} lodash@4.17.21: {} @@ -22681,6 +23379,14 @@ snapshots: postcss: 8.4.47 ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.8.7)(typescript@5.6.3) + postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.6.1): + dependencies: + lilconfig: 3.1.2 + optionalDependencies: + jiti: 1.21.6 + postcss: 8.4.49 + yaml: 2.6.1 + postcss-nested@6.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -23339,6 +24045,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.1: {} + readline@1.3.0: {} real-require@0.2.0: {} @@ -23589,12 +24297,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.22.5 fsevents: 2.3.3 + rollup@4.32.1: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.32.1 + '@rollup/rollup-android-arm64': 4.32.1 + '@rollup/rollup-darwin-arm64': 4.32.1 + '@rollup/rollup-darwin-x64': 4.32.1 + '@rollup/rollup-freebsd-arm64': 4.32.1 + '@rollup/rollup-freebsd-x64': 4.32.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.32.1 + '@rollup/rollup-linux-arm-musleabihf': 4.32.1 + '@rollup/rollup-linux-arm64-gnu': 4.32.1 + '@rollup/rollup-linux-arm64-musl': 4.32.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.32.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.32.1 + '@rollup/rollup-linux-riscv64-gnu': 4.32.1 + '@rollup/rollup-linux-s390x-gnu': 4.32.1 + '@rollup/rollup-linux-x64-gnu': 4.32.1 + '@rollup/rollup-linux-x64-musl': 4.32.1 + '@rollup/rollup-win32-arm64-msvc': 4.32.1 + '@rollup/rollup-win32-ia32-msvc': 4.32.1 + '@rollup/rollup-win32-x64-msvc': 4.32.1 + fsevents: 2.3.3 + rrweb-cssom@0.7.1: optional: true - rsbuild-plugin-dts@0.2.2(@rsbuild/core@1.1.13)(typescript@5.7.2): + rsbuild-plugin-dts@0.4.0(@rsbuild/core@1.2.3)(typescript@5.7.2): dependencies: - '@rsbuild/core': 1.1.13 + '@rsbuild/core': 1.2.3 magic-string: 0.30.17 picocolors: 1.1.1 tinyglobby: 0.2.10 @@ -23605,10 +24338,10 @@ snapshots: dependencies: fs-extra: 11.2.0 - rspress@1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))): + rspress@1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@rsbuild/core': 1.1.13 - '@rspress/core': 1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + '@rspress/core': 1.40.0(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@rspress/shared': 1.40.0 cac: 6.7.14 chalk: 5.4.1 @@ -24031,6 +24764,10 @@ snapshots: source-map@0.7.4: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + sourcemap-codec@1.4.8: {} space-separated-tokens@1.1.5: {} @@ -24306,16 +25043,17 @@ snapshots: dependencies: rimraf: 2.6.3 - terser-webpack-plugin@5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))): + terser-webpack-plugin@5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.0 - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) optionalDependencies: '@swc/core': 1.7.42(@swc/helpers@0.5.15) + esbuild: 0.24.2 terser@5.31.0: dependencies: @@ -24406,6 +25144,10 @@ snapshots: tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -24415,6 +25157,8 @@ snapshots: dependencies: tslib: 2.8.1 + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -24543,6 +25287,34 @@ snapshots: tslib@2.8.1: {} + tsup@8.3.6(@swc/core@1.7.42(@swc/helpers@0.5.15))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.24.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.0 + debug: 4.3.7 + esbuild: 0.24.2 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.6.1) + resolve-from: 5.0.0 + rollup: 4.32.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.10 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.7.42(@swc/helpers@0.5.15) + postcss: 8.4.49 + typescript: 5.7.2 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsutils@3.21.0(typescript@4.8.4): dependencies: tslib: 1.14.1 @@ -24941,10 +25713,12 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: optional: true - webpack-dev-middleware@7.4.2(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))): + webpack-dev-middleware@7.4.2(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: colorette: 2.0.20 memfs: 4.17.0 @@ -24953,10 +25727,10 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.2.0 optionalDependencies: - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) optional: true - webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))): + webpack-dev-server@5.0.4(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -24986,10 +25760,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + webpack-dev-middleware: 7.4.2(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) ws: 8.18.0 optionalDependencies: - webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)) + webpack: 5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2) transitivePeerDependencies: - bufferutil - debug @@ -24999,7 +25773,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15)): + webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -25022,7 +25796,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))) + terser-webpack-plugin: 5.3.10(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.91.0(@swc/core@1.7.42(@swc/helpers@0.5.15))(esbuild@0.24.2)) watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -25061,6 +25835,12 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 From 773b4db21aa3d525706bbc0185b1047d6699a5d6 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sat, 1 Feb 2025 13:30:04 +0900 Subject: [PATCH 25/61] get: cloudflare token --- .../init/cloudflareD1R2Worker/index.ts | 51 +++++++++++++------ .../src/utils/getWranglerLoginAuthToken.ts | 4 +- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts index 046ba819..1184a4a5 100644 --- a/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts +++ b/packages/hot-updater/src/commands/init/cloudflareD1R2Worker/index.ts @@ -1,3 +1,4 @@ +import { link } from "@/components/banner"; import * as p from "@clack/prompts"; import { getCwd } from "@hot-updater/plugin-core"; import { execa } from "execa"; @@ -21,25 +22,31 @@ export const initCloudflareD1R2Worker = async () => { const cwd = getCwd(); - await execa( - "npx", - [ - "wrangler", - "login", - "--scopes", - "account:read", - "user:read", - "d1:write", - "workers:write", - ], - { cwd }, - ); - const { Cloudflare, getWranglerLoginAuthToken } = await import( "@hot-updater/cloudflare/utils" ); - const auth = getWranglerLoginAuthToken(); + let auth = getWranglerLoginAuthToken(); + + if (!auth) { + await execa( + "npx", + [ + "wrangler", + "login", + "--scopes", + "account:read", + "user:read", + "d1:write", + "workers:write", + ], + { cwd }, + ); + auth = getWranglerLoginAuthToken(); + } + if (!auth) { + throw new Error("'npx wrangler login' is required to use this command"); + } // const wrangler = await createWrangler({ // cloudflareApiToken: auth.oauth_token, @@ -68,6 +75,20 @@ export const initCloudflareD1R2Worker = async () => { if (p.isCancel(accountId)) { process.exit(1); } + p.log.step( + `Please visit this link to create an API Token: ${link( + `https://dash.cloudflare.com/${accountId}/api-tokens`, + )}`, + ); + p.log.step("You need edit permissions for both D1 and R2"); + + const apiToken = await p.text({ + message: "Enter the API Token", + }); + + if (p.isCancel(apiToken)) { + process.exit(1); + } s.start("Checking R2 Buckets..."); const buckets = diff --git a/plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts b/plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts index 834e7988..705a0329 100644 --- a/plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts +++ b/plugins/cloudflare/src/utils/getWranglerLoginAuthToken.ts @@ -29,7 +29,7 @@ export const getWranglerLoginAuthToken = (): { expiration_time: string; refresh_token: string; scopes: string[]; -} => { +} | null => { try { const wranglerConfigPath = getGlobalWranglerConfigPath(); const wranglerConfig = fs.readFileSync( @@ -38,6 +38,6 @@ export const getWranglerLoginAuthToken = (): { ); return toml.parse(wranglerConfig); } catch (error) { - throw new Error("'npx wrangler login' is required to use this command"); + return null; } }; From 1b9bc52f19136e011c752332ff3e5a4b8bc549cb Mon Sep 17 00:00:00 2001 From: gronxb Date: Sat, 1 Feb 2025 13:30:56 +0900 Subject: [PATCH 26/61] chore: .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 06f20288..2e09d157 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,6 @@ docs/.vitepress/cache bundle.zip -generated/ \ No newline at end of file +generated/ + +.wrangler/ \ No newline at end of file From dc41a661bfe4d384890afbaeb927ac71feb3cb35 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sat, 1 Feb 2025 13:59:48 +0900 Subject: [PATCH 27/61] fix: semver --- packages/hot-updater/rslib.config.ts | 16 ++++ plugins/cloudflare/package.json | 4 +- plugins/cloudflare/tsup.config.ts | 2 +- .../cloudflare/worker/src/getUpdateInfo.ts | 80 +++++++++++++++++++ plugins/cloudflare/worker/src/index.ts | 14 +++- .../worker/worker-configuration.d.ts | 1 + pnpm-lock.yaml | 6 ++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 plugins/cloudflare/worker/src/getUpdateInfo.ts diff --git a/packages/hot-updater/rslib.config.ts b/packages/hot-updater/rslib.config.ts index 02b4dd3a..5db2365d 100644 --- a/packages/hot-updater/rslib.config.ts +++ b/packages/hot-updater/rslib.config.ts @@ -20,5 +20,21 @@ export default defineConfig({ }, }, }, + { + format: "cjs", + dts: true, + source: { + entry: { + index: "./src/index.ts", + config: "./src/config.ts", + "plugins/babel": "./src/plugins/babel.ts", + }, + }, + shims: { + cjs: { + "import.meta.url": true, + }, + }, + }, ], }); diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 7a6d1d60..d1f5a951 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -42,12 +42,14 @@ "@aws-sdk/lib-storage": "^3.685.0", "@hot-updater/core": "0.5.10", "@hot-updater/plugin-core": "0.5.10", - "cloudflare": "^4.0.0" + "cloudflare": "^4.0.0", + "semver": "^7.6.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", "@types/better-sqlite3": "^7.6.12", + "@types/semver": "^7.5.8", "better-sqlite3": "^11.8.1", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", diff --git a/plugins/cloudflare/tsup.config.ts b/plugins/cloudflare/tsup.config.ts index 831c9b72..74da0a25 100644 --- a/plugins/cloudflare/tsup.config.ts +++ b/plugins/cloudflare/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts", "src/utils/index.ts"], - format: ["esm"], + format: ["esm", "cjs"], dts: true, banner: { js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`, diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts new file mode 100644 index 00000000..7f2e70a5 --- /dev/null +++ b/plugins/cloudflare/worker/src/getUpdateInfo.ts @@ -0,0 +1,80 @@ +import { NIL_UUID, type UpdateInfo }from "@hot-updater/core"; +import semver from "semver"; + +export async function getUpdateInfo( + env: Env, + appPlatform: "ios" | "android", + appVersion: string, + bundleId: string, + ): Promise { + let finalResult: UpdateInfo | null = null; + + const updateStmt = env.DB.prepare( + `SELECT id, target_app_version, file_url, file_hash, should_force_update + FROM bundles + WHERE enabled = 1 AND platform = ? AND id >= ? + ORDER BY id DESC` + ); + const updateResult = await updateStmt.bind(appPlatform, bundleId).all(); + const updateRows = updateResult.results as Array<{ + id: string; + target_app_version: string; + file_url: string | null; + file_hash: string | null; + should_force_update: boolean; + }>; + + for (const row of updateRows) { + if (semver.satisfies(row.target_app_version, appVersion)) { + finalResult = { + id: row.id, + shouldForceUpdate: row.should_force_update, + fileUrl: row.file_url, + fileHash: row.file_hash, + status: "UPDATE", + }; + break; + } + } + + if (!finalResult) { + const rollbackStmt = env.DB.prepare( + `SELECT id, file_url, file_hash + FROM bundles + WHERE enabled = 1 AND platform = ? AND id < ? + ORDER BY id DESC + LIMIT 1` + ); + const rollbackRow = await rollbackStmt.bind(appPlatform, bundleId).first<{ + id: string; + file_url: string | null; + file_hash: string | null; + }>(); + + if (rollbackRow) { + finalResult = { + id: rollbackRow.id, + shouldForceUpdate: true, // 롤백인 경우 항상 강제 업데이트 + fileUrl: rollbackRow.file_url, + fileHash: rollbackRow.file_hash, + status: "ROLLBACK", + }; + } + } + + if (finalResult && finalResult.id === bundleId) { + finalResult = null; + } + + if (!finalResult && bundleId !== NIL_UUID) { + finalResult = { + id: NIL_UUID, + shouldForceUpdate: true, + fileUrl: null, + fileHash: null, + status: "ROLLBACK", + }; + } + + return finalResult; + } \ No newline at end of file diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts index aad761da..f25c2f94 100644 --- a/plugins/cloudflare/worker/src/index.ts +++ b/plugins/cloudflare/worker/src/index.ts @@ -11,8 +11,20 @@ * Learn more at https://developers.cloudflare.com/workers/ */ +import { getUpdateInfo } from "./getUpdateInfo"; + export default { async fetch(request, env, ctx): Promise { - return new Response("Hello World!"); + + const bundleId = request.headers.get("x-bundle-id") as string; + const appPlatform = request.headers.get("x-app-platform") as "ios" | "android"; + const appVersion = request.headers.get("x-app-version") as string; + + if (!bundleId || !appPlatform || !appVersion) { + return new Response(JSON.stringify({ error: "Missing bundleId, appPlatform, or appVersion" }), { status: 400 }); + } + + const updaterInfo = await getUpdateInfo(env, appPlatform, appVersion, bundleId); + return new Response(JSON.stringify(updaterInfo)); }, } satisfies ExportedHandler; diff --git a/plugins/cloudflare/worker/worker-configuration.d.ts b/plugins/cloudflare/worker/worker-configuration.d.ts index 7edf06f5..71dea791 100644 --- a/plugins/cloudflare/worker/worker-configuration.d.ts +++ b/plugins/cloudflare/worker/worker-configuration.d.ts @@ -1,4 +1,5 @@ // Generated by Wrangler // After adding bindings to `wrangler.json`, regenerate this interface via `npm run cf-typegen` interface Env { + DB: D1Database; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c545963..c3ed0aa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -716,6 +716,9 @@ importers: cloudflare: specifier: ^4.0.0 version: 4.0.0(encoding@0.1.13) + semver: + specifier: ^7.6.3 + version: 7.6.3 devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 @@ -726,6 +729,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 better-sqlite3: specifier: ^11.8.1 version: 11.8.1 From b6de9be669e8e6aaa1bdad5cf56aaf2b07db14f6 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sat, 1 Feb 2025 14:15:24 +0900 Subject: [PATCH 28/61] feat: cloudflare worket end --- packages/core/src/types.ts | 15 ++++ plugins/cloudflare/package.json | 5 +- .../cloudflare/worker/src/getUpdateInfo.ts | 80 ------------------- plugins/cloudflare/worker/src/index.ts | 18 ++++- plugins/js/src/getUpdateInfo.ts | 4 +- plugins/supabase/src/types.ts | 24 +----- pnpm-lock.yaml | 17 ++-- 7 files changed, 49 insertions(+), 114 deletions(-) delete mode 100644 plugins/cloudflare/worker/src/getUpdateInfo.ts diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 714bdd48..fd8a2e6d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -39,6 +39,21 @@ export interface Bundle { message: string | null; } +type SnakeCase = S extends `${infer T}${infer U}` + ? `${T extends Capitalize ? "_" : ""}${Lowercase}${SnakeCase}` + : S; + +// Utility type to recursively map object keys to snake_case +type SnakeKeyObject = T extends Record + ? { + [K in keyof T as SnakeCase>]: T[K] extends object + ? SnakeKeyObject + : T[K]; + } + : T; + +export type SnakeCaseBundle = SnakeKeyObject; + export type BundleArg = | string | Bundle[] diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index d1f5a951..01003214 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -41,15 +41,14 @@ "@aws-sdk/client-s3": "^3.685.0", "@aws-sdk/lib-storage": "^3.685.0", "@hot-updater/core": "0.5.10", + "@hot-updater/js": "0.6.0", "@hot-updater/plugin-core": "0.5.10", - "cloudflare": "^4.0.0", - "semver": "^7.6.3" + "cloudflare": "^4.0.0" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", "@types/better-sqlite3": "^7.6.12", - "@types/semver": "^7.5.8", "better-sqlite3": "^11.8.1", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts deleted file mode 100644 index 7f2e70a5..00000000 --- a/plugins/cloudflare/worker/src/getUpdateInfo.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { NIL_UUID, type UpdateInfo }from "@hot-updater/core"; -import semver from "semver"; - -export async function getUpdateInfo( - env: Env, - appPlatform: "ios" | "android", - appVersion: string, - bundleId: string, - ): Promise { - let finalResult: UpdateInfo | null = null; - - const updateStmt = env.DB.prepare( - `SELECT id, target_app_version, file_url, file_hash, should_force_update - FROM bundles - WHERE enabled = 1 AND platform = ? AND id >= ? - ORDER BY id DESC` - ); - const updateResult = await updateStmt.bind(appPlatform, bundleId).all(); - const updateRows = updateResult.results as Array<{ - id: string; - target_app_version: string; - file_url: string | null; - file_hash: string | null; - should_force_update: boolean; - }>; - - for (const row of updateRows) { - if (semver.satisfies(row.target_app_version, appVersion)) { - finalResult = { - id: row.id, - shouldForceUpdate: row.should_force_update, - fileUrl: row.file_url, - fileHash: row.file_hash, - status: "UPDATE", - }; - break; - } - } - - if (!finalResult) { - const rollbackStmt = env.DB.prepare( - `SELECT id, file_url, file_hash - FROM bundles - WHERE enabled = 1 AND platform = ? AND id < ? - ORDER BY id DESC - LIMIT 1` - ); - const rollbackRow = await rollbackStmt.bind(appPlatform, bundleId).first<{ - id: string; - file_url: string | null; - file_hash: string | null; - }>(); - - if (rollbackRow) { - finalResult = { - id: rollbackRow.id, - shouldForceUpdate: true, // 롤백인 경우 항상 강제 업데이트 - fileUrl: rollbackRow.file_url, - fileHash: rollbackRow.file_hash, - status: "ROLLBACK", - }; - } - } - - if (finalResult && finalResult.id === bundleId) { - finalResult = null; - } - - if (!finalResult && bundleId !== NIL_UUID) { - finalResult = { - id: NIL_UUID, - shouldForceUpdate: true, - fileUrl: null, - fileHash: null, - status: "ROLLBACK", - }; - } - - return finalResult; - } \ No newline at end of file diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts index f25c2f94..dd2b2a2d 100644 --- a/plugins/cloudflare/worker/src/index.ts +++ b/plugins/cloudflare/worker/src/index.ts @@ -11,11 +11,12 @@ * Learn more at https://developers.cloudflare.com/workers/ */ -import { getUpdateInfo } from "./getUpdateInfo"; +import { Bundle, SnakeCaseBundle } from "@hot-updater/core"; +import { getUpdateInfo } from "@hot-updater/js"; +import camelcaseKeys from "camelcase-keys"; export default { async fetch(request, env, ctx): Promise { - const bundleId = request.headers.get("x-bundle-id") as string; const appPlatform = request.headers.get("x-app-platform") as "ios" | "android"; const appVersion = request.headers.get("x-app-version") as string; @@ -24,7 +25,18 @@ export default { return new Response(JSON.stringify({ error: "Missing bundleId, appPlatform, or appVersion" }), { status: 400 }); } - const updaterInfo = await getUpdateInfo(env, appPlatform, appVersion, bundleId); + const bundleStmt = env.DB.prepare( + `SELECT * + FROM bundles + WHERE enabled = 1 AND platform = ? AND id >= ? + ORDER BY id DESC` + ); + const bundleResult = await bundleStmt.bind(appPlatform, bundleId).all(); + + const transform = (bundle:SnakeCaseBundle) => camelcaseKeys(bundle) as Bundle + + const bundles = bundleResult.results.map(transform); + const updaterInfo = await getUpdateInfo(bundles, {platform: appPlatform, bundleId, appVersion}); return new Response(JSON.stringify(updaterInfo)); }, } satisfies ExportedHandler; diff --git a/plugins/js/src/getUpdateInfo.ts b/plugins/js/src/getUpdateInfo.ts index 4055a88b..d8e271d3 100644 --- a/plugins/js/src/getUpdateInfo.ts +++ b/plugins/js/src/getUpdateInfo.ts @@ -51,7 +51,7 @@ export const getUpdateInfo = async ( if (latestBundle.id.localeCompare(bundleId) > 0) { return { id: latestBundle.id, - shouldForceUpdate: latestBundle.shouldForceUpdate, + shouldForceUpdate: Boolean(latestBundle.shouldForceUpdate), fileUrl: latestBundle.fileUrl, fileHash: latestBundle.fileHash, status: "UPDATE" as UpdateStatus, @@ -69,7 +69,7 @@ export const getUpdateInfo = async ( if (latestBundle.id.localeCompare(bundleId) > 0) { return { id: latestBundle.id, - shouldForceUpdate: latestBundle.shouldForceUpdate, + shouldForceUpdate: Boolean(latestBundle.shouldForceUpdate), fileUrl: latestBundle.fileUrl, fileHash: latestBundle.fileHash, status: "UPDATE" as UpdateStatus, diff --git a/plugins/supabase/src/types.ts b/plugins/supabase/src/types.ts index dd0c80db..1a2f0142 100644 --- a/plugins/supabase/src/types.ts +++ b/plugins/supabase/src/types.ts @@ -1,27 +1,11 @@ -import type { Bundle } from "@hot-updater/core"; - -type SnakeCase = S extends `${infer T}${infer U}` - ? `${T extends Capitalize ? "_" : ""}${Lowercase}${SnakeCase}` - : S; - -// Utility type to recursively map object keys to snake_case -type SnakeKeyObject = T extends Record - ? { - [K in keyof T as SnakeCase>]: T[K] extends object - ? SnakeKeyObject - : T[K]; - } - : T; - -export type BundlesTable = SnakeKeyObject; - +import type { SnakeCaseBundle } from "@hot-updater/core"; export type Database = { public: { Tables: { bundles: { - Row: BundlesTable; - Insert: BundlesTable; - Update: BundlesTable; + Row: SnakeCaseBundle; + Insert: SnakeCaseBundle; + Update: SnakeCaseBundle; Relationships: []; }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3ed0aa2..3f5fb867 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,15 +710,15 @@ importers: '@hot-updater/core': specifier: workspace:* version: link:../../packages/core + '@hot-updater/js': + specifier: workspace:* + version: link:../js '@hot-updater/plugin-core': specifier: workspace:* version: link:../plugin-core cloudflare: specifier: ^4.0.0 version: 4.0.0(encoding@0.1.13) - semver: - specifier: ^7.6.3 - version: 7.6.3 devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 @@ -729,9 +729,6 @@ importers: '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 - '@types/semver': - specifier: ^7.5.8 - version: 7.5.8 better-sqlite3: specifier: ^11.8.1 version: 11.8.1 @@ -741,6 +738,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + es-toolkit: + specifier: ^1.32.0 + version: 1.32.0 execa: specifier: ^9.5.2 version: 9.5.2 @@ -6527,6 +6527,9 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + es-toolkit@1.32.0: + resolution: {integrity: sha512-ZfSfHP1l6ubgW/B/FRtqb9bYdMvI6jizbOSfbwwJNcOQ1QE6TFsC3jpQkZ900uUPSR3t3SU5Ds7UWKnYz+uP8Q==} + esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -19308,6 +19311,8 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + es-toolkit@1.32.0: {} + esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 From 5eb120b2c7b5d5c3ffbec961543469e0addf216a Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 10:07:24 +0900 Subject: [PATCH 29/61] feat: worker get updater info --- .../src-server/{type.test.ts => type.spec.ts} | 0 plugins/cloudflare/sql/get_update_info.sql | 81 -------- plugins/cloudflare/sql/semver_satisfies.sql | 126 ------------ plugins/cloudflare/tsconfig.json | 8 +- .../cloudflare/{worker => }/vitest.config.mts | 3 +- plugins/cloudflare/vitest.global-setup.mts | 13 ++ .../{worker => }/worker-configuration.d.ts | 0 plugins/cloudflare/worker/.editorconfig | 12 -- plugins/cloudflare/worker/.gitignore | 172 ---------------- plugins/cloudflare/worker/.prettierrc | 6 - .../worker/src/getUpdateInfo.spec.ts | 71 +++++++ .../cloudflare/worker/src/getUpdateInfo.ts | 185 ++++++++++++++++++ plugins/cloudflare/worker/src/index.ts | 182 +++++++++++++++-- plugins/cloudflare/worker/tsconfig.json | 44 ----- ...lback.test.ts => checkForRollback.spec.ts} | 0 ...dateInfo.test.ts => getUpdateInfo.spec.ts} | 0 ...e_info.test.ts => get_update_info.spec.ts} | 0 ...sfies.test.ts => semver_satisfies.spec.ts} | 0 pnpm-lock.yaml | 8 - 19 files changed, 447 insertions(+), 464 deletions(-) rename packages/console/src-server/{type.test.ts => type.spec.ts} (100%) delete mode 100644 plugins/cloudflare/sql/get_update_info.sql delete mode 100644 plugins/cloudflare/sql/semver_satisfies.sql rename plugins/cloudflare/{worker => }/vitest.config.mts (63%) create mode 100644 plugins/cloudflare/vitest.global-setup.mts rename plugins/cloudflare/{worker => }/worker-configuration.d.ts (100%) delete mode 100644 plugins/cloudflare/worker/.editorconfig delete mode 100644 plugins/cloudflare/worker/.gitignore delete mode 100644 plugins/cloudflare/worker/.prettierrc create mode 100644 plugins/cloudflare/worker/src/getUpdateInfo.spec.ts create mode 100644 plugins/cloudflare/worker/src/getUpdateInfo.ts delete mode 100644 plugins/cloudflare/worker/tsconfig.json rename plugins/js/src/{checkForRollback.test.ts => checkForRollback.spec.ts} (100%) rename plugins/js/src/{getUpdateInfo.test.ts => getUpdateInfo.spec.ts} (100%) rename plugins/postgres/sql/{get_update_info.test.ts => get_update_info.spec.ts} (100%) rename plugins/postgres/sql/{semver_satisfies.test.ts => semver_satisfies.spec.ts} (100%) diff --git a/packages/console/src-server/type.test.ts b/packages/console/src-server/type.spec.ts similarity index 100% rename from packages/console/src-server/type.test.ts rename to packages/console/src-server/type.spec.ts diff --git a/plugins/cloudflare/sql/get_update_info.sql b/plugins/cloudflare/sql/get_update_info.sql deleted file mode 100644 index 9931df29..00000000 --- a/plugins/cloudflare/sql/get_update_info.sql +++ /dev/null @@ -1,81 +0,0 @@ --- HotUpdater.get_update_info - -CREATE OR REPLACE FUNCTION get_update_info ( - app_platform platforms, - app_version text, - bundle_id uuid -) -RETURNS TABLE ( - id uuid, - should_force_update boolean, - file_url text, - file_hash text, - status text -) -LANGUAGE plpgsql -AS -$$ -DECLARE - NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000'; -BEGIN - RETURN QUERY - WITH rollback_candidate AS ( - SELECT - b.id, - -- If status is 'ROLLBACK', should_force_update is always TRUE - TRUE AS should_force_update, - b.file_url, - b.file_hash, - 'ROLLBACK' AS status - FROM bundles b - WHERE b.enabled = TRUE - AND b.platform = app_platform - AND b.id < bundle_id - ORDER BY b.id DESC - LIMIT 1 - ), - update_candidate AS ( - SELECT - b.id, - b.should_force_update, - b.file_url, - b.file_hash, - 'UPDATE' AS status - FROM bundles b - WHERE b.enabled = TRUE - AND b.platform = app_platform - AND b.id >= bundle_id - AND semver_satisfies(b.target_app_version, app_version) - ORDER BY b.id DESC - LIMIT 1 - ), - final_result AS ( - SELECT * - FROM update_candidate - - UNION ALL - - SELECT * - FROM rollback_candidate - WHERE NOT EXISTS (SELECT 1 FROM update_candidate) - ) - SELECT * - FROM final_result WHERE final_result.id != bundle_id - - UNION ALL - /* - When there are no final results and bundle_id != NIL_UUID, - add one fallback row. - This fallback row is also ROLLBACK so shouldForceUpdate = TRUE. - */ - SELECT - NIL_UUID AS id, - TRUE AS should_force_update, -- Always TRUE - NULL AS file_url, - NULL AS file_hash, - 'ROLLBACK' AS status - WHERE (SELECT COUNT(*) FROM final_result) = 0 - AND bundle_id != NIL_UUID; - -END; -$$; \ No newline at end of file diff --git a/plugins/cloudflare/sql/semver_satisfies.sql b/plugins/cloudflare/sql/semver_satisfies.sql deleted file mode 100644 index ce4b3b0d..00000000 --- a/plugins/cloudflare/sql/semver_satisfies.sql +++ /dev/null @@ -1,126 +0,0 @@ --- HotUpdater.semver_satisfies - -CREATE OR REPLACE FUNCTION semver_satisfies(range_expression TEXT, version TEXT) -RETURNS BOOLEAN AS $$ -DECLARE - version_parts TEXT[]; - version_major INT; - version_minor INT; - version_patch INT; - satisfies BOOLEAN := FALSE; -BEGIN - -- Split the version into major, minor, and patch - version_parts := string_to_array(version, '.'); - version_major := version_parts[1]::INT; - version_minor := version_parts[2]::INT; - version_patch := version_parts[3]::INT; - - -- Parse range expression and evaluate - IF range_expression ~ '^\d+\.\d+\.\d+$' THEN - -- Exact match - satisfies := (range_expression = version); - - ELSIF range_expression = '*' THEN - -- Matches any version - satisfies := TRUE; - - ELSIF range_expression ~ '^\d+\.x\.x$' THEN - -- Matches major.x.x - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - BEGIN - satisfies := (version_major = major_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+\.x$' THEN - -- Matches major.minor.x - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - minor_range INT := split_part(range_expression, '.', 2)::INT; - BEGIN - satisfies := (version_major = major_range AND version_minor = minor_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+$' THEN - -- Matches major.minor - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - minor_range INT := split_part(range_expression, '.', 2)::INT; - BEGIN - satisfies := (version_major = major_range AND version_minor = minor_range); - END; - - ELSIF range_expression ~ '^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$' THEN - -- Matches range e.g., 1.2.3 - 1.2.7 - DECLARE - lower_bound TEXT := split_part(range_expression, ' - ', 1); - upper_bound TEXT := split_part(range_expression, ' - ', 2); - BEGIN - satisfies := (version >= lower_bound AND version <= upper_bound); - END; - - ELSIF range_expression ~ '^>=\d+\.\d+\.\d+ <\d+\.\d+\.\d+$' THEN - -- Matches range with inequalities - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '>=([\d\.]+) <.*', '\1'); - upper_bound TEXT := regexp_replace(range_expression, '.*<([\d\.]+)', '\1'); - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSIF range_expression ~ '^~\d+\.\d+\.\d+$' THEN - -- Matches ~1.2.3 (>=1.2.3 <1.3.0) - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '~', ''); - upper_bound_major INT := split_part(lower_bound, '.', 1)::INT; - upper_bound_minor INT := split_part(lower_bound, '.', 2)::INT + 1; - upper_bound TEXT := upper_bound_major || '.' || upper_bound_minor || '.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSIF range_expression ~ '^\^\d+\.\d+\.\d+$' THEN - -- Matches ^1.2.3 (>=1.2.3 <2.0.0) - DECLARE - lower_bound TEXT := regexp_replace(range_expression, '\^', ''); - upper_bound_major INT := split_part(lower_bound, '.', 1)::INT + 1; - upper_bound TEXT := upper_bound_major || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - -- [Added] 1) Single major version pattern '^(\d+)$' - ELSIF range_expression ~ '^\d+$' THEN - /* - e.g.) "1" is interpreted as (>=1.0.0 <2.0.0) in semver range - "2" would be interpreted as (>=2.0.0 <3.0.0) - */ - DECLARE - major_range INT := range_expression::INT; - lower_bound TEXT := major_range || '.0.0'; - upper_bound TEXT := (major_range + 1) || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - -- [Added] 2) major.x pattern '^(\d+)\.x$' - ELSIF range_expression ~ '^\d+\.x$' THEN - /* - e.g.) "2.x" => as long as major=2 matches, any minor and patch is OK - effectively works like (>=2.0.0 <3.0.0) - */ - DECLARE - major_range INT := split_part(range_expression, '.', 1)::INT; - lower_bound TEXT := major_range || '.0.0'; - upper_bound TEXT := (major_range + 1) || '.0.0'; - BEGIN - satisfies := (version >= lower_bound AND version < upper_bound); - END; - - ELSE - RAISE EXCEPTION 'Unsupported range expression: %', range_expression; - END IF; - - RETURN satisfies; -END; -$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/plugins/cloudflare/tsconfig.json b/plugins/cloudflare/tsconfig.json index 7ccc5491..0f50f74f 100644 --- a/plugins/cloudflare/tsconfig.json +++ b/plugins/cloudflare/tsconfig.json @@ -1,5 +1,11 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"], + "compilerOptions": { + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers" + ] + }, + "include": ["src", "worker-configuration.d.ts", "worker"], "exclude": ["lib", "supabase"] } diff --git a/plugins/cloudflare/worker/vitest.config.mts b/plugins/cloudflare/vitest.config.mts similarity index 63% rename from plugins/cloudflare/worker/vitest.config.mts rename to plugins/cloudflare/vitest.config.mts index c2d67dc6..6abd7e3a 100644 --- a/plugins/cloudflare/worker/vitest.config.mts +++ b/plugins/cloudflare/vitest.config.mts @@ -2,9 +2,10 @@ import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { + globalSetup: "./vitest.global-setup.mts", poolOptions: { workers: { - wrangler: { configPath: "./wrangler.json" }, + wrangler: { configPath: "./worker/wrangler.json" }, }, }, }, diff --git a/plugins/cloudflare/vitest.global-setup.mts b/plugins/cloudflare/vitest.global-setup.mts new file mode 100644 index 00000000..4a35709b --- /dev/null +++ b/plugins/cloudflare/vitest.global-setup.mts @@ -0,0 +1,13 @@ +import type { TestProject } from "vitest/node"; +import { prepareSql } from "./sql/prepareSql"; + +export default async function setup(project: TestProject) { + const sql = await prepareSql(); + project.provide("prepareSql", sql); +} + +declare module "vitest" { + export interface ProvidedContext { + prepareSql: string; + } +} diff --git a/plugins/cloudflare/worker/worker-configuration.d.ts b/plugins/cloudflare/worker-configuration.d.ts similarity index 100% rename from plugins/cloudflare/worker/worker-configuration.d.ts rename to plugins/cloudflare/worker-configuration.d.ts diff --git a/plugins/cloudflare/worker/.editorconfig b/plugins/cloudflare/worker/.editorconfig deleted file mode 100644 index a727df34..00000000 --- a/plugins/cloudflare/worker/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = tab -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.yml] -indent_style = space diff --git a/plugins/cloudflare/worker/.gitignore b/plugins/cloudflare/worker/.gitignore deleted file mode 100644 index 3b0fe33c..00000000 --- a/plugins/cloudflare/worker/.gitignore +++ /dev/null @@ -1,172 +0,0 @@ -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -\*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -\*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -\*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -\*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.cache -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -.cache/ - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp -.cache - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.\* - -# wrangler project - -.dev.vars -.wrangler/ diff --git a/plugins/cloudflare/worker/.prettierrc b/plugins/cloudflare/worker/.prettierrc deleted file mode 100644 index 5c7b5d3c..00000000 --- a/plugins/cloudflare/worker/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 140, - "singleQuote": true, - "semi": true, - "useTabs": true -} diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts new file mode 100644 index 00000000..1d707913 --- /dev/null +++ b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts @@ -0,0 +1,71 @@ +import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core"; +import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils"; +import { beforeAll, beforeEach, describe, inject } from "vitest"; +import { getUpdateInfo as getUpdateInfoFromWorker } from "./getUpdateInfo"; +import { env } from "cloudflare:test"; + +declare module "vitest" { + export interface ProvidedContext { + prepareSql: string; + } +} + +declare module 'cloudflare:test' { + interface ProvidedEnv { + DB: D1Database + } +} + +const createInsertBundleQuery = (bundle: Bundle) => { + return ` + INSERT INTO bundles ( + id, file_url, file_hash, platform, target_app_version, + should_force_update, enabled, git_commit_hash, message + ) VALUES ( + '${bundle.id}', + '${bundle.fileUrl}', + '${bundle.fileHash}', + '${bundle.platform}', + '${bundle.targetAppVersion}', + ${bundle.shouldForceUpdate}, + ${bundle.enabled}, + ${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"}, + ${bundle.message ? `'${bundle.message}'` : "null"} + ); + `; +}; + +const createGetUpdateInfo = (db: D1Database) => +async ( + bundles: Bundle[], + { appVersion, bundleId, platform }: GetBundlesArgs, +): Promise => { + if(bundles.length > 0) { + await db.prepare(createInsertBundleQuerys(bundles)).run(); + } + return await getUpdateInfoFromWorker(db, { + appVersion, + bundleId, + platform, + }) as UpdateInfo | null; +}; + +const createInsertBundleQuerys = (bundles: Bundle[]) => { + return bundles.map(createInsertBundleQuery).join("\n"); +}; + +const getUpdateInfo = createGetUpdateInfo(env.DB); + +describe("getUpdateInfo", async () => { + beforeAll(async () => { + await env.DB.prepare(inject("prepareSql")).run(); + }); + + beforeEach(async () => { + await env.DB.prepare("DELETE FROM bundles").run(); + }); + + setupGetUpdateInfoTestSuite({ + getUpdateInfo: getUpdateInfo, + }); +}); diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts new file mode 100644 index 00000000..6c3b608c --- /dev/null +++ b/plugins/cloudflare/worker/src/getUpdateInfo.ts @@ -0,0 +1,185 @@ +import { Platform, UpdateInfo, UpdateStatus } from "@hot-updater/core"; + +export const getUpdateInfo = async ( + DB: D1Database, + {platform, appVersion, bundleId}:{platform: Platform, appVersion: string, bundleId: string}, +) => { + const sql = /* sql */` + WITH input AS ( + SELECT + ? AS app_platform, -- e.g. 'ios' or 'android' + ? AS app_version, -- e.g. '1.2.3' + ? AS bundle_id, -- current bundle id (string) + '00000000-0000-0000-0000-000000000000' AS nil_uuid + ), + update_candidates AS ( + SELECT + b.id, + b.should_force_update, + b.file_url, + b.file_hash, + 'UPDATE' AS status, + CASE + -- (1) Exact wildcard: "*" + WHEN b.target_app_version = '*' THEN 1 + + -- (2) Exact version: "N.M.P" (e.g. "1.2.3") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' + AND b.target_app_version NOT LIKE '%x%' AND b.target_app_version NOT LIKE '%X%' + THEN CASE WHEN b.target_app_version = input.app_version THEN 1 ELSE 0 END + + -- (3) major.x.x pattern (e.g. "1.x.x") + WHEN b.target_app_version GLOB '[0-9]*.[xX].[xX]' + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (4) major.minor.x pattern (e.g. "1.2.x") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1, + instr(substr(b.target_app_version, instr(b.target_app_version, '.')+1), '.') - 1) AS INTEGER) + = + CAST(substr(input.app_version, instr(input.app_version, '.')+1, + instr(substr(input.app_version, instr(input.app_version, '.')+1), '.') - 1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (5) major.minor pattern (e.g. "1.2") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*' + AND NOT(b.target_app_version GLOB '*.[0-9]*.[0-9]*') + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1) AS INTEGER) = + CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (6) dash range: "N.M.P - N.M.P" (e.g. "1.2.3 - 1.2.7") + WHEN b.target_app_version GLOB '* - *' + THEN CASE + WHEN input.app_version >= trim(substr(b.target_app_version, 1, instr(b.target_app_version, '-') - 1)) + AND input.app_version <= trim(substr(b.target_app_version, instr(b.target_app_version, '-')+1)) + THEN 1 ELSE 0 END + + -- (7) inequality range: ">=N.M.P =1.2.3 <2.0.0") + WHEN b.target_app_version GLOB '>=* <*' + THEN CASE + WHEN input.app_version >= trim(substr(b.target_app_version, 3, instr(b.target_app_version, ' ') - 3)) + AND input.app_version < trim(substr(b.target_app_version, instr(b.target_app_version, '<')+1)) + THEN 1 ELSE 0 END + + -- (8) tilde(~) pattern: "~N.M.P" (e.g. "~1.2.3" ⇒ >=1.2.3 AND <1.3.0) + WHEN b.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(b.target_app_version, 2) + AND input.app_version < ( + CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS TEXT) + || '.' || + CAST(CAST(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1, + instr(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1),'.') - 1) AS INTEGER) + 1 AS TEXT) + || '.0' + ) + THEN 1 ELSE 0 END + + -- (9) caret(^) pattern: "^N.M.P" (e.g. "^1.2.3" ⇒ >=1.2.3 AND <2.0.0) + WHEN b.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(b.target_app_version, 2) + AND input.app_version < ( + CAST(CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS INTEGER) + 1 AS TEXT) + || '.0.0' + ) + THEN 1 ELSE 0 END + + -- (10) single major version: "N" (e.g. "1" ⇒ >=1.0.0 AND <2.0.0) + WHEN b.target_app_version GLOB '[0-9]+' + THEN CASE + WHEN input.app_version >= (b.target_app_version || '.0.0') + AND input.app_version < (CAST(b.target_app_version AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + -- (11) major.x pattern: "N.x" (e.g. "1.x" ⇒ >=1.0.0 AND <2.0.0) + WHEN b.target_app_version GLOB '[0-9]+\.x' + THEN CASE + WHEN input.app_version >= (substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) || '.0.0') + AND input.app_version < (CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + ELSE 0 + END AS version_match + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id >= input.bundle_id + ORDER BY b.id DESC + ), + update_candidate AS ( + SELECT id, should_force_update, file_url, file_hash, status + FROM update_candidates + WHERE version_match = 1 + LIMIT 1 + ), + rollback_candidate AS ( + SELECT + b.id, + 1 AS should_force_update, + b.file_url, + b.file_hash, + 'ROLLBACK' AS status + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id < input.bundle_id + ORDER BY b.id DESC + LIMIT 1 + ), + final_result AS ( + SELECT * FROM update_candidate + UNION ALL + SELECT * FROM rollback_candidate + WHERE NOT EXISTS (SELECT 1 FROM update_candidate) + ) + SELECT id, should_force_update, file_url, file_hash, status + FROM final_result, input + WHERE id <> bundle_id + + UNION ALL + + SELECT + nil_uuid AS id, + 1 AS should_force_update, + NULL AS file_url, + NULL AS file_hash, + 'ROLLBACK' AS status + FROM input + WHERE (SELECT COUNT(*) FROM final_result) = 0 + AND bundle_id <> nil_uuid; + `; + + const result = await DB.prepare(sql) + .bind(platform, appVersion, bundleId) + .first<{ + id: string; + should_force_update: number; + file_url: string | null; + file_hash: string | null; + status: UpdateStatus; + }>(); + + if (!result) { + return null; + } + + return { + id: result.id, + shouldForceUpdate: Boolean(result.should_force_update), + fileUrl: result.file_url, + fileHash: result.file_hash, + status: result.status, + } as UpdateInfo; +}; diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts index dd2b2a2d..4cdb3888 100644 --- a/plugins/cloudflare/worker/src/index.ts +++ b/plugins/cloudflare/worker/src/index.ts @@ -25,18 +25,174 @@ export default { return new Response(JSON.stringify({ error: "Missing bundleId, appPlatform, or appVersion" }), { status: 400 }); } - const bundleStmt = env.DB.prepare( - `SELECT * - FROM bundles - WHERE enabled = 1 AND platform = ? AND id >= ? - ORDER BY id DESC` - ); - const bundleResult = await bundleStmt.bind(appPlatform, bundleId).all(); - - const transform = (bundle:SnakeCaseBundle) => camelcaseKeys(bundle) as Bundle - - const bundles = bundleResult.results.map(transform); - const updaterInfo = await getUpdateInfo(bundles, {platform: appPlatform, bundleId, appVersion}); - return new Response(JSON.stringify(updaterInfo)); + // SQLite 전용 SQL 쿼리 – PostgreSQL의 semver_satisfies 모든 로직을 CASE 식으로 구현 + const sql = ` + WITH input AS ( + SELECT + ? AS app_platform, -- 예: 'ios' 또는 'android' + ? AS app_version, -- 예: '1.2.3' + ? AS bundle_id, -- 현재 번들의 id (문자열) + '00000000-0000-0000-0000-000000000000' AS nil_uuid + ), + update_candidates AS ( + SELECT + b.id, + b.should_force_update, + b.file_url, + b.file_hash, + 'UPDATE' AS status, + -- semver 조건 평가: PostgreSQL의 semver_satisfies의 모든 패턴을 구현 + CASE + -- (1) 정확한 와일드카드: "*" + WHEN b.target_app_version = '*' THEN 1 + + -- (2) 정확한 버전: "N.M.P" (예: "1.2.3") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' + AND b.target_app_version NOT LIKE '%x%' AND b.target_app_version NOT LIKE '%X%' + THEN CASE WHEN b.target_app_version = input.app_version THEN 1 ELSE 0 END + + -- (3) major.x.x 패턴 (예: "1.x.x") + WHEN b.target_app_version GLOB '[0-9]*.[xX].[xX]' + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (4) major.minor.x 패턴 (예: "1.2.x") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1, + instr(substr(b.target_app_version, instr(b.target_app_version, '.')+1), '.') - 1) AS INTEGER) + = + CAST(substr(input.app_version, instr(input.app_version, '.')+1, + instr(substr(input.app_version, instr(input.app_version, '.')+1), '.') - 1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (5) major.minor 패턴 (예: "1.2") + WHEN b.target_app_version GLOB '[0-9]*.[0-9]*' + AND NOT(b.target_app_version GLOB '*.[0-9]*.[0-9]*') + THEN CASE + WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1) AS INTEGER) = + CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (6) dash 범위: "N.M.P - N.M.P" (예: "1.2.3 - 1.2.7") + WHEN b.target_app_version GLOB '* - *' + THEN CASE + WHEN input.app_version >= trim(substr(b.target_app_version, 1, instr(b.target_app_version, '-') - 1)) + AND input.app_version <= trim(substr(b.target_app_version, instr(b.target_app_version, '-')+1)) + THEN 1 ELSE 0 END + + -- (7) 부등호 범위: ">=N.M.P =1.2.3 <2.0.0") + WHEN b.target_app_version GLOB '>=* <*' + THEN CASE + WHEN input.app_version >= trim(substr(b.target_app_version, 3, instr(b.target_app_version, ' ') - 3)) + AND input.app_version < trim(substr(b.target_app_version, instr(b.target_app_version, '<')+1)) + THEN 1 ELSE 0 END + + -- (8) tilde(~) 패턴: "~N.M.P" (예: "~1.2.3" ⇒ >=1.2.3 AND <1.3.0) + WHEN b.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(b.target_app_version, 2) + AND input.app_version < ( + CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS TEXT) + || '.' || + CAST(CAST(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1, + instr(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1),'.') - 1) AS INTEGER) + 1 AS TEXT) + || '.0' + ) + THEN 1 ELSE 0 END + + -- (9) caret(^) 패턴: "^N.M.P" (예: "^1.2.3" ⇒ >=1.2.3 AND <2.0.0) + WHEN b.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(b.target_app_version, 2) + AND input.app_version < ( + CAST(CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS INTEGER) + 1 AS TEXT) + || '.0.0' + ) + THEN 1 ELSE 0 END + + -- (10) 단일 major 버전: "N" (예: "1" ⇒ >=1.0.0 AND <2.0.0) + WHEN b.target_app_version GLOB '[0-9]+' + THEN CASE + WHEN input.app_version >= (b.target_app_version || '.0.0') + AND input.app_version < (CAST(b.target_app_version AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + -- (11) major.x 패턴: "N.x" (예: "1.x" ⇒ >=1.0.0 AND <2.0.0) + WHEN b.target_app_version GLOB '[0-9]+\.x' + THEN CASE + WHEN input.app_version >= (substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) || '.0.0') + AND input.app_version < (CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + ELSE 0 + END AS version_match + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id >= input.bundle_id + ORDER BY b.id DESC + ), + update_candidate AS ( + SELECT id, should_force_update, file_url, file_hash, status + FROM update_candidates + WHERE version_match = 1 + LIMIT 1 + ), + rollback_candidate AS ( + SELECT + b.id, + 1 AS should_force_update, + b.file_url, + b.file_hash, + 'ROLLBACK' AS status + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id < input.bundle_id + ORDER BY b.id DESC + LIMIT 1 + ), + final_result AS ( + SELECT * FROM update_candidate + UNION ALL + SELECT * FROM rollback_candidate + WHERE NOT EXISTS (SELECT 1 FROM update_candidate) + ) + SELECT id, should_force_update, file_url, file_hash, status + FROM final_result, input + WHERE id <> bundle_id + + UNION ALL + + SELECT + nil_uuid AS id, + 1 AS should_force_update, + NULL AS file_url, + NULL AS file_hash, + 'ROLLBACK' AS status + FROM input + WHERE (SELECT COUNT(*) FROM final_result) = 0 + AND bundle_id <> nil_uuid; + `; + + // 바인딩 순서: app_platform, app_version, bundle_id + const result = await env.DB.prepare(sql) + .bind(appPlatform, appVersion, bundleId) + .first(); + + return new Response(JSON.stringify(result), { + headers: { "Content-Type": "application/json" }, + status: 200 + }); + // return new Response(JSON.stringify(updaterInfo)); }, } satisfies ExportedHandler; diff --git a/plugins/cloudflare/worker/tsconfig.json b/plugins/cloudflare/worker/tsconfig.json deleted file mode 100644 index bb3e0f7e..00000000 --- a/plugins/cloudflare/worker/tsconfig.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "target": "es2021", - /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "lib": ["es2021"], - /* Specify what JSX code is generated. */ - "jsx": "react-jsx", - - /* Specify what module code is generated. */ - "module": "es2022", - /* Specify how TypeScript looks up a file from a given module specifier. */ - "moduleResolution": "Bundler", - /* Specify type package names to be included without being referenced in a source file. */ - "types": ["@cloudflare/workers-types/2023-07-01"], - /* Enable importing .json files */ - "resolveJsonModule": true, - - /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - "allowJs": true, - /* Enable error reporting in type-checked JavaScript files. */ - "checkJs": false, - - /* Disable emitting files from a compilation. */ - "noEmit": true, - - /* Ensure that each file can be safely transpiled without relying on other imports. */ - "isolatedModules": true, - /* Allow 'import x from y' when a module doesn't have a default export. */ - "allowSyntheticDefaultImports": true, - /* Ensure that casing is correct in imports. */ - "forceConsistentCasingInFileNames": true, - - /* Enable all strict type-checking options. */ - "strict": true, - - /* Skip type checking all .d.ts files. */ - "skipLibCheck": true - }, - "exclude": ["test"], - "include": ["worker-configuration.d.ts", "src/**/*.ts"] -} diff --git a/plugins/js/src/checkForRollback.test.ts b/plugins/js/src/checkForRollback.spec.ts similarity index 100% rename from plugins/js/src/checkForRollback.test.ts rename to plugins/js/src/checkForRollback.spec.ts diff --git a/plugins/js/src/getUpdateInfo.test.ts b/plugins/js/src/getUpdateInfo.spec.ts similarity index 100% rename from plugins/js/src/getUpdateInfo.test.ts rename to plugins/js/src/getUpdateInfo.spec.ts diff --git a/plugins/postgres/sql/get_update_info.test.ts b/plugins/postgres/sql/get_update_info.spec.ts similarity index 100% rename from plugins/postgres/sql/get_update_info.test.ts rename to plugins/postgres/sql/get_update_info.spec.ts diff --git a/plugins/postgres/sql/semver_satisfies.test.ts b/plugins/postgres/sql/semver_satisfies.spec.ts similarity index 100% rename from plugins/postgres/sql/semver_satisfies.test.ts rename to plugins/postgres/sql/semver_satisfies.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f5fb867..6e5ec431 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -738,9 +738,6 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 - es-toolkit: - specifier: ^1.32.0 - version: 1.32.0 execa: specifier: ^9.5.2 version: 9.5.2 @@ -6527,9 +6524,6 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - es-toolkit@1.32.0: - resolution: {integrity: sha512-ZfSfHP1l6ubgW/B/FRtqb9bYdMvI6jizbOSfbwwJNcOQ1QE6TFsC3jpQkZ900uUPSR3t3SU5Ds7UWKnYz+uP8Q==} - esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -19311,8 +19305,6 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - es-toolkit@1.32.0: {} - esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 From a956820804981d016112d42e7c8099d3c4fe7cf3 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 10:26:27 +0900 Subject: [PATCH 30/61] feat: sep semver satisfies --- .../worker/src/getUpdateInfo.spec.ts | 2 +- .../cloudflare/worker/src/getUpdateInfo.ts | 267 ++++++------------ .../worker/src/semverSatisfies.spec.ts | 33 +++ .../cloudflare/worker/src/semverSatisfies.ts | 119 ++++++++ 4 files changed, 243 insertions(+), 178 deletions(-) create mode 100644 plugins/cloudflare/worker/src/semverSatisfies.spec.ts create mode 100644 plugins/cloudflare/worker/src/semverSatisfies.ts diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts index 1d707913..30fb4c83 100644 --- a/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts +++ b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts @@ -17,7 +17,7 @@ declare module 'cloudflare:test' { } const createInsertBundleQuery = (bundle: Bundle) => { - return ` + return /* sql */` INSERT INTO bundles ( id, file_url, file_hash, platform, target_app_version, should_force_update, enabled, git_commit_hash, message diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts index 6c3b608c..64e92cda 100644 --- a/plugins/cloudflare/worker/src/getUpdateInfo.ts +++ b/plugins/cloudflare/worker/src/getUpdateInfo.ts @@ -1,185 +1,98 @@ import { Platform, UpdateInfo, UpdateStatus } from "@hot-updater/core"; +import { SEMVER_SATISFIES_SQL } from "./semverSatisfies"; export const getUpdateInfo = async ( - DB: D1Database, - {platform, appVersion, bundleId}:{platform: Platform, appVersion: string, bundleId: string}, + DB: D1Database, + { platform, appVersion, bundleId }: { + platform: Platform; + appVersion: string; + bundleId: string; + }, ) => { - const sql = /* sql */` - WITH input AS ( - SELECT - ? AS app_platform, -- e.g. 'ios' or 'android' - ? AS app_version, -- e.g. '1.2.3' - ? AS bundle_id, -- current bundle id (string) - '00000000-0000-0000-0000-000000000000' AS nil_uuid - ), - update_candidates AS ( - SELECT - b.id, - b.should_force_update, - b.file_url, - b.file_hash, - 'UPDATE' AS status, - CASE - -- (1) Exact wildcard: "*" - WHEN b.target_app_version = '*' THEN 1 - - -- (2) Exact version: "N.M.P" (e.g. "1.2.3") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' - AND b.target_app_version NOT LIKE '%x%' AND b.target_app_version NOT LIKE '%X%' - THEN CASE WHEN b.target_app_version = input.app_version THEN 1 ELSE 0 END - - -- (3) major.x.x pattern (e.g. "1.x.x") - WHEN b.target_app_version GLOB '[0-9]*.[xX].[xX]' - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (4) major.minor.x pattern (e.g. "1.2.x") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1, - instr(substr(b.target_app_version, instr(b.target_app_version, '.')+1), '.') - 1) AS INTEGER) - = - CAST(substr(input.app_version, instr(input.app_version, '.')+1, - instr(substr(input.app_version, instr(input.app_version, '.')+1), '.') - 1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (5) major.minor pattern (e.g. "1.2") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*' - AND NOT(b.target_app_version GLOB '*.[0-9]*.[0-9]*') - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1) AS INTEGER) = - CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (6) dash range: "N.M.P - N.M.P" (e.g. "1.2.3 - 1.2.7") - WHEN b.target_app_version GLOB '* - *' - THEN CASE - WHEN input.app_version >= trim(substr(b.target_app_version, 1, instr(b.target_app_version, '-') - 1)) - AND input.app_version <= trim(substr(b.target_app_version, instr(b.target_app_version, '-')+1)) - THEN 1 ELSE 0 END - - -- (7) inequality range: ">=N.M.P =1.2.3 <2.0.0") - WHEN b.target_app_version GLOB '>=* <*' - THEN CASE - WHEN input.app_version >= trim(substr(b.target_app_version, 3, instr(b.target_app_version, ' ') - 3)) - AND input.app_version < trim(substr(b.target_app_version, instr(b.target_app_version, '<')+1)) - THEN 1 ELSE 0 END - - -- (8) tilde(~) pattern: "~N.M.P" (e.g. "~1.2.3" ⇒ >=1.2.3 AND <1.3.0) - WHEN b.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(b.target_app_version, 2) - AND input.app_version < ( - CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS TEXT) - || '.' || - CAST(CAST(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1, - instr(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1),'.') - 1) AS INTEGER) + 1 AS TEXT) - || '.0' - ) - THEN 1 ELSE 0 END - - -- (9) caret(^) pattern: "^N.M.P" (e.g. "^1.2.3" ⇒ >=1.2.3 AND <2.0.0) - WHEN b.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(b.target_app_version, 2) - AND input.app_version < ( - CAST(CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS INTEGER) + 1 AS TEXT) - || '.0.0' - ) - THEN 1 ELSE 0 END - - -- (10) single major version: "N" (e.g. "1" ⇒ >=1.0.0 AND <2.0.0) - WHEN b.target_app_version GLOB '[0-9]+' - THEN CASE - WHEN input.app_version >= (b.target_app_version || '.0.0') - AND input.app_version < (CAST(b.target_app_version AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - -- (11) major.x pattern: "N.x" (e.g. "1.x" ⇒ >=1.0.0 AND <2.0.0) - WHEN b.target_app_version GLOB '[0-9]+\.x' - THEN CASE - WHEN input.app_version >= (substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) || '.0.0') - AND input.app_version < (CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - ELSE 0 - END AS version_match - FROM bundles b, input - WHERE b.enabled = 1 - AND b.platform = input.app_platform - AND b.id >= input.bundle_id - ORDER BY b.id DESC - ), - update_candidate AS ( - SELECT id, should_force_update, file_url, file_hash, status - FROM update_candidates - WHERE version_match = 1 - LIMIT 1 - ), - rollback_candidate AS ( - SELECT - b.id, - 1 AS should_force_update, - b.file_url, - b.file_hash, - 'ROLLBACK' AS status - FROM bundles b, input - WHERE b.enabled = 1 - AND b.platform = input.app_platform - AND b.id < input.bundle_id - ORDER BY b.id DESC - LIMIT 1 - ), - final_result AS ( - SELECT * FROM update_candidate - UNION ALL - SELECT * FROM rollback_candidate - WHERE NOT EXISTS (SELECT 1 FROM update_candidate) - ) - SELECT id, should_force_update, file_url, file_hash, status - FROM final_result, input - WHERE id <> bundle_id - - UNION ALL - - SELECT - nil_uuid AS id, - 1 AS should_force_update, - NULL AS file_url, - NULL AS file_hash, - 'ROLLBACK' AS status - FROM input - WHERE (SELECT COUNT(*) FROM final_result) = 0 - AND bundle_id <> nil_uuid; - `; + const sql = /* sql */ ` + WITH input AS ( + SELECT + ? AS app_platform, -- 예: 'ios' 또는 'android' + ? AS app_version, -- 예: '1.2.3' + ? AS bundle_id, -- 현재 번들 ID (문자열) + '00000000-0000-0000-0000-000000000000' AS nil_uuid + ), + update_candidates AS ( + SELECT + b.id, + b.should_force_update, + b.file_url, + b.file_hash, + 'UPDATE' AS status, + ${SEMVER_SATISFIES_SQL("b")} AS version_match + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id >= input.bundle_id + ORDER BY b.id DESC + ), + update_candidate AS ( + SELECT id, should_force_update, file_url, file_hash, status + FROM update_candidates + WHERE version_match = 1 + LIMIT 1 + ), + rollback_candidate AS ( + SELECT + b.id, + 1 AS should_force_update, + b.file_url, + b.file_hash, + 'ROLLBACK' AS status + FROM bundles b, input + WHERE b.enabled = 1 + AND b.platform = input.app_platform + AND b.id < input.bundle_id + ORDER BY b.id DESC + LIMIT 1 + ), + final_result AS ( + SELECT * FROM update_candidate + UNION ALL + SELECT * FROM rollback_candidate + WHERE NOT EXISTS (SELECT 1 FROM update_candidate) + ) + SELECT id, should_force_update, file_url, file_hash, status + FROM final_result, input + WHERE id <> bundle_id + + UNION ALL + + SELECT + nil_uuid AS id, + 1 AS should_force_update, + NULL AS file_url, + NULL AS file_hash, + 'ROLLBACK' AS status + FROM input + WHERE (SELECT COUNT(*) FROM final_result) = 0 + AND bundle_id <> nil_uuid; + `; - const result = await DB.prepare(sql) - .bind(platform, appVersion, bundleId) - .first<{ - id: string; - should_force_update: number; - file_url: string | null; - file_hash: string | null; - status: UpdateStatus; - }>(); + const result = await DB.prepare(sql) + .bind(platform, appVersion, bundleId) + .first<{ + id: string; + should_force_update: number; + file_url: string | null; + file_hash: string | null; + status: UpdateStatus; + }>(); - if (!result) { - return null; - } + if (!result) { + return null; + } - return { - id: result.id, - shouldForceUpdate: Boolean(result.should_force_update), - fileUrl: result.file_url, - fileHash: result.file_hash, - status: result.status, - } as UpdateInfo; + return { + id: result.id, + shouldForceUpdate: Boolean(result.should_force_update), + fileUrl: result.file_url, + fileHash: result.file_hash, + status: result.status, + } as UpdateInfo; }; diff --git a/plugins/cloudflare/worker/src/semverSatisfies.spec.ts b/plugins/cloudflare/worker/src/semverSatisfies.spec.ts new file mode 100644 index 00000000..53009bbb --- /dev/null +++ b/plugins/cloudflare/worker/src/semverSatisfies.spec.ts @@ -0,0 +1,33 @@ +import { setupSemverSatisfiesTestSuite } from "@hot-updater/core/test-utils"; +import { describe } from "vitest"; +import { SEMVER_SATISFIES_SQL } from "./semverSatisfies"; +import { env } from "cloudflare:test"; + +async function semverSatisfiesFromWorker( + db: D1Database, + version: string, + range: string, +) { + const sql = /* sql */ ` + WITH input AS ( + SELECT + ? AS app_version, + ? AS target_app_version + ) + SELECT ${SEMVER_SATISFIES_SQL("input")} AS version_match + FROM input; +`; + + const result = await db.prepare(sql) + .bind(version, range) + .first<{ version_match: number }>(); + + return Boolean(result?.version_match); +} + +describe("semverSatisfies", () => { + setupSemverSatisfiesTestSuite({ + semverSatisfies: async (version, range) => + semverSatisfiesFromWorker(env.DB, version, range), + }); +}); diff --git a/plugins/cloudflare/worker/src/semverSatisfies.ts b/plugins/cloudflare/worker/src/semverSatisfies.ts new file mode 100644 index 00000000..28b1b6b1 --- /dev/null +++ b/plugins/cloudflare/worker/src/semverSatisfies.ts @@ -0,0 +1,119 @@ +export const SEMVER_SATISFIES_SQL = (alias: string): string => { + return /* sql */` + CASE + -- (1) Exact wildcard: "*" + WHEN ${alias}.target_app_version = '*' THEN 1 + + -- (2) Exact version: "N.M.P" (e.g. "1.2.3") + WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' + AND ${alias}.target_app_version NOT LIKE '%x%' AND ${alias}.target_app_version NOT LIKE '%X%' + THEN CASE WHEN ${alias}.target_app_version = input.app_version THEN 1 ELSE 0 END + + -- (3) major.x.x pattern (e.g. "1.x.x") + WHEN ${alias}.target_app_version GLOB '[0-9]*.[xX].[xX]' + THEN CASE + WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (4) major.minor.x pattern (e.g. "1.2.x") + WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' + THEN CASE + WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST( + substr( + ${alias}.target_app_version, + instr(${alias}.target_app_version, '.')+1, + instr(${alias}.target_app_version || '.', '.') - instr(${alias}.target_app_version, '.') - 1 + ) AS INTEGER + ) + = + CAST( + substr( + input.app_version, + instr(input.app_version, '.')+1, + instr(input.app_version || '.', '.') - instr(input.app_version, '.') - 1 + ) AS INTEGER + ) + THEN 1 ELSE 0 END + + -- (5) major.minor pattern (e.g. "1.2") + WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*' + AND NOT(${alias}.target_app_version GLOB '*.[0-9]*.[0-9]*') + THEN CASE + WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = + CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) + AND + CAST(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '.')+1) AS INTEGER) = + CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) + THEN 1 ELSE 0 END + + -- (6) dash range: "N.M.P - N.M.P" (e.g. "1.2.3 - 1.2.7") + WHEN ${alias}.target_app_version GLOB '* - *' + THEN CASE + WHEN input.app_version >= trim(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '-') - 1)) + AND input.app_version <= trim(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '-')+1)) + THEN 1 ELSE 0 END + + -- (7) inequality range: ">=N.M.P =1.2.3 <2.0.0") + WHEN ${alias}.target_app_version GLOB '>=* <*' + THEN CASE + WHEN input.app_version >= trim(substr(${alias}.target_app_version, 3, instr(${alias}.target_app_version, ' ') - 3)) + AND input.app_version < trim(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '<')+1)) + THEN 1 ELSE 0 END + + -- (8) tilde(~) pattern: "~N.M.P" (e.g. "~1.2.3" ⇒ >=1.2.3 AND <1.(minor+1).0) + WHEN ${alias}.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(${alias}.target_app_version, 2) + AND input.app_version < ( + substr(${alias}.target_app_version, 2, instr(${alias}.target_app_version, '.')-1) + || '.' || + CAST( + CAST( + substr( + ${alias}.target_app_version, + instr(${alias}.target_app_version, '.')+1, + instr(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '.')+1) || '.', '.') - 1 + ) AS INTEGER + ) + 1 AS TEXT + ) + || '.0' + ) + THEN 1 ELSE 0 END + + -- (9) caret(^) pattern: "^N.M.P" (e.g. "^1.2.3" ⇒ >=1.2.3 AND <(major+1).0.0) + WHEN ${alias}.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' + THEN CASE + WHEN input.app_version >= substr(${alias}.target_app_version, 2) + AND input.app_version < ( + CAST( + CAST( + substr(${alias}.target_app_version, 2, instr(${alias}.target_app_version, '.')-1) + AS INTEGER + ) + 1 AS TEXT + ) + || '.0.0' + ) + THEN 1 ELSE 0 END + + -- (10) single major version: "N" (e.g. "1" ⇒ >=1.0.0 AND <2.0.0) + WHEN ${alias}.target_app_version GLOB '[0-9]+' + THEN CASE + WHEN input.app_version >= (${alias}.target_app_version || '.0.0') + AND input.app_version < (CAST(${alias}.target_app_version AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + -- (11) major.x pattern: "N.x" (e.g. "1.x" ⇒ >=1.0.0 AND <2.0.0) + WHEN ${alias}.target_app_version GLOB '[0-9]+\.x' + THEN CASE + WHEN input.app_version >= (substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) || '.0.0') + AND input.app_version < (CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') + THEN 1 ELSE 0 END + + ELSE 0 + END + `; + } \ No newline at end of file From 917b917095d10ad4cba0a8ed690813c6edc960eb Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 12:09:43 +0900 Subject: [PATCH 31/61] fix: semver --- plugins/cloudflare/package.json | 3 + plugins/cloudflare/tsconfig.json | 1 + .../cloudflare/worker/src/getUpdateInfo.ts | 30 +- .../worker/src/semverSatisfies.spec.ts | 33 --- .../cloudflare/worker/src/semverSatisfies.ts | 119 -------- plugins/js/src/index.ts | 1 + plugins/js/src/semverSatisfies.ts | 21 +- pnpm-lock.yaml | 277 ++++++++++++------ 8 files changed, 225 insertions(+), 260 deletions(-) delete mode 100644 plugins/cloudflare/worker/src/semverSatisfies.spec.ts delete mode 100644 plugins/cloudflare/worker/src/semverSatisfies.ts diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 01003214..6b83cd26 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -49,6 +49,8 @@ "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.13.0", + "@types/semver": "^7.5.8", "better-sqlite3": "^11.8.1", "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", @@ -56,6 +58,7 @@ "mime": "^4.0.4", "pg-minify": "^1.6.5", "picocolors": "^1.0.0", + "semver": "^7.6.3", "toml": "^3.0.0", "typescript": "^5.5.2", "vitest": "2.1.8", diff --git a/plugins/cloudflare/tsconfig.json b/plugins/cloudflare/tsconfig.json index 0f50f74f..c66382e7 100644 --- a/plugins/cloudflare/tsconfig.json +++ b/plugins/cloudflare/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "types": [ + "node", "@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers" ] diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts index 64e92cda..39f5ec23 100644 --- a/plugins/cloudflare/worker/src/getUpdateInfo.ts +++ b/plugins/cloudflare/worker/src/getUpdateInfo.ts @@ -1,5 +1,5 @@ +import { filterCompatibleAppVersions } from "@hot-updater/js"; import { Platform, UpdateInfo, UpdateStatus } from "@hot-updater/core"; -import { SEMVER_SATISFIES_SQL } from "./semverSatisfies"; export const getUpdateInfo = async ( DB: D1Database, @@ -9,6 +9,20 @@ export const getUpdateInfo = async ( bundleId: string; }, ) => { + const appVersionList = await DB.prepare(/* sql */ ` + SELECT + target_app_version + FROM bundles + WHERE platform = ? + GROUP BY target_app_version + `).bind(platform).all<{ target_app_version: string; count: number }>(); + + const targetAppVersionList = filterCompatibleAppVersions( + appVersionList.results.map((group) => group.target_app_version), + appVersion, + ); + + const sql = /* sql */ ` WITH input AS ( SELECT @@ -17,24 +31,20 @@ export const getUpdateInfo = async ( ? AS bundle_id, -- 현재 번들 ID (문자열) '00000000-0000-0000-0000-000000000000' AS nil_uuid ), - update_candidates AS ( + update_candidate AS ( SELECT b.id, b.should_force_update, b.file_url, b.file_hash, - 'UPDATE' AS status, - ${SEMVER_SATISFIES_SQL("b")} AS version_match + 'UPDATE' AS status FROM bundles b, input WHERE b.enabled = 1 AND b.platform = input.app_platform AND b.id >= input.bundle_id - ORDER BY b.id DESC - ), - update_candidate AS ( - SELECT id, should_force_update, file_url, file_hash, status - FROM update_candidates - WHERE version_match = 1 + AND b.target_app_version IN (${targetAppVersionList.map(version => `'${version}'`).join(",")}) + ORDER BY b.id DESC + -- semver 규칙에 따라 최신 버전을 선택 로직 ㄱㄱ LIMIT 1 ), rollback_candidate AS ( diff --git a/plugins/cloudflare/worker/src/semverSatisfies.spec.ts b/plugins/cloudflare/worker/src/semverSatisfies.spec.ts deleted file mode 100644 index 53009bbb..00000000 --- a/plugins/cloudflare/worker/src/semverSatisfies.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { setupSemverSatisfiesTestSuite } from "@hot-updater/core/test-utils"; -import { describe } from "vitest"; -import { SEMVER_SATISFIES_SQL } from "./semverSatisfies"; -import { env } from "cloudflare:test"; - -async function semverSatisfiesFromWorker( - db: D1Database, - version: string, - range: string, -) { - const sql = /* sql */ ` - WITH input AS ( - SELECT - ? AS app_version, - ? AS target_app_version - ) - SELECT ${SEMVER_SATISFIES_SQL("input")} AS version_match - FROM input; -`; - - const result = await db.prepare(sql) - .bind(version, range) - .first<{ version_match: number }>(); - - return Boolean(result?.version_match); -} - -describe("semverSatisfies", () => { - setupSemverSatisfiesTestSuite({ - semverSatisfies: async (version, range) => - semverSatisfiesFromWorker(env.DB, version, range), - }); -}); diff --git a/plugins/cloudflare/worker/src/semverSatisfies.ts b/plugins/cloudflare/worker/src/semverSatisfies.ts deleted file mode 100644 index 28b1b6b1..00000000 --- a/plugins/cloudflare/worker/src/semverSatisfies.ts +++ /dev/null @@ -1,119 +0,0 @@ -export const SEMVER_SATISFIES_SQL = (alias: string): string => { - return /* sql */` - CASE - -- (1) Exact wildcard: "*" - WHEN ${alias}.target_app_version = '*' THEN 1 - - -- (2) Exact version: "N.M.P" (e.g. "1.2.3") - WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' - AND ${alias}.target_app_version NOT LIKE '%x%' AND ${alias}.target_app_version NOT LIKE '%X%' - THEN CASE WHEN ${alias}.target_app_version = input.app_version THEN 1 ELSE 0 END - - -- (3) major.x.x pattern (e.g. "1.x.x") - WHEN ${alias}.target_app_version GLOB '[0-9]*.[xX].[xX]' - THEN CASE - WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (4) major.minor.x pattern (e.g. "1.2.x") - WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' - THEN CASE - WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST( - substr( - ${alias}.target_app_version, - instr(${alias}.target_app_version, '.')+1, - instr(${alias}.target_app_version || '.', '.') - instr(${alias}.target_app_version, '.') - 1 - ) AS INTEGER - ) - = - CAST( - substr( - input.app_version, - instr(input.app_version, '.')+1, - instr(input.app_version || '.', '.') - instr(input.app_version, '.') - 1 - ) AS INTEGER - ) - THEN 1 ELSE 0 END - - -- (5) major.minor pattern (e.g. "1.2") - WHEN ${alias}.target_app_version GLOB '[0-9]*.[0-9]*' - AND NOT(${alias}.target_app_version GLOB '*.[0-9]*.[0-9]*') - THEN CASE - WHEN CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '.')+1) AS INTEGER) = - CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (6) dash range: "N.M.P - N.M.P" (e.g. "1.2.3 - 1.2.7") - WHEN ${alias}.target_app_version GLOB '* - *' - THEN CASE - WHEN input.app_version >= trim(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '-') - 1)) - AND input.app_version <= trim(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '-')+1)) - THEN 1 ELSE 0 END - - -- (7) inequality range: ">=N.M.P =1.2.3 <2.0.0") - WHEN ${alias}.target_app_version GLOB '>=* <*' - THEN CASE - WHEN input.app_version >= trim(substr(${alias}.target_app_version, 3, instr(${alias}.target_app_version, ' ') - 3)) - AND input.app_version < trim(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '<')+1)) - THEN 1 ELSE 0 END - - -- (8) tilde(~) pattern: "~N.M.P" (e.g. "~1.2.3" ⇒ >=1.2.3 AND <1.(minor+1).0) - WHEN ${alias}.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(${alias}.target_app_version, 2) - AND input.app_version < ( - substr(${alias}.target_app_version, 2, instr(${alias}.target_app_version, '.')-1) - || '.' || - CAST( - CAST( - substr( - ${alias}.target_app_version, - instr(${alias}.target_app_version, '.')+1, - instr(substr(${alias}.target_app_version, instr(${alias}.target_app_version, '.')+1) || '.', '.') - 1 - ) AS INTEGER - ) + 1 AS TEXT - ) - || '.0' - ) - THEN 1 ELSE 0 END - - -- (9) caret(^) pattern: "^N.M.P" (e.g. "^1.2.3" ⇒ >=1.2.3 AND <(major+1).0.0) - WHEN ${alias}.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(${alias}.target_app_version, 2) - AND input.app_version < ( - CAST( - CAST( - substr(${alias}.target_app_version, 2, instr(${alias}.target_app_version, '.')-1) - AS INTEGER - ) + 1 AS TEXT - ) - || '.0.0' - ) - THEN 1 ELSE 0 END - - -- (10) single major version: "N" (e.g. "1" ⇒ >=1.0.0 AND <2.0.0) - WHEN ${alias}.target_app_version GLOB '[0-9]+' - THEN CASE - WHEN input.app_version >= (${alias}.target_app_version || '.0.0') - AND input.app_version < (CAST(${alias}.target_app_version AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - -- (11) major.x pattern: "N.x" (e.g. "1.x" ⇒ >=1.0.0 AND <2.0.0) - WHEN ${alias}.target_app_version GLOB '[0-9]+\.x' - THEN CASE - WHEN input.app_version >= (substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) || '.0.0') - AND input.app_version < (CAST(substr(${alias}.target_app_version, 1, instr(${alias}.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - ELSE 0 - END - `; - } \ No newline at end of file diff --git a/plugins/js/src/index.ts b/plugins/js/src/index.ts index 904af29e..18ba9969 100644 --- a/plugins/js/src/index.ts +++ b/plugins/js/src/index.ts @@ -1 +1,2 @@ export * from "./getUpdateInfo"; +export * from "./semverSatisfies"; diff --git a/plugins/js/src/semverSatisfies.ts b/plugins/js/src/semverSatisfies.ts index 7b978567..317e1292 100644 --- a/plugins/js/src/semverSatisfies.ts +++ b/plugins/js/src/semverSatisfies.ts @@ -6,8 +6,27 @@ export const semverSatisfies = ( ) => { const currentCoerce = semver.coerce(currentVersion); if (!currentCoerce) { - throw new Error("Invalid current version"); + return false; } return semver.satisfies(currentCoerce.version, targetAppVersion); }; + +/** + * Filters target app versions that are compatible with the current app version. + * Returns only versions that are compatible with the current version according to semver rules. + * + * @param targetAppVersionList - List of target app versions to filter + * @param currentVersion - Current app version + * @returns Array of target app versions compatible with the current version + */ +export const filterCompatibleAppVersions = ( + targetAppVersionList: string[], + currentVersion: string, +) => { + const compatibleAppVersionList = targetAppVersionList.filter((version) => + semverSatisfies(version, currentVersion), + ); + + return compatibleAppVersionList; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e5ec431..0201baac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ importers: version: 1.9.4 '@nx/js': specifier: ^20.3.0 - version: 20.3.0(@babel/traverse@7.25.9)(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(nx@20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)))(typescript@5.7.2) + version: 20.3.0(@babel/traverse@7.25.9)(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(nx@20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)))(typescript@5.7.2) '@rslib/core': specifier: ^0.4.0 version: 0.4.0(typescript@5.7.2) @@ -44,7 +44,7 @@ importers: version: 5.7.2 vitest: specifier: ^2.1.8 - version: 2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) + version: 2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) docs: devDependencies: @@ -190,7 +190,7 @@ importers: version: 0.74.83(@babel/core@7.26.0)(@babel/preset-env@7.23.2(@babel/core@7.26.0)) '@react-native/eslint-config': specifier: 0.74.83 - version: 0.74.83(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)))(prettier@2.8.8)(typescript@5.7.2) + version: 0.74.83(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)))(prettier@2.8.8)(typescript@5.7.2) '@react-native/gradle-plugin': specifier: 0.74.83 version: 0.74.83 @@ -232,7 +232,7 @@ importers: version: 2.2.4 jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + version: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) prettier: specifier: 2.8.8 version: 2.8.8 @@ -293,7 +293,7 @@ importers: version: 0.76.2(@babel/preset-env@7.26.0(@babel/core@7.26.0)) '@react-native/eslint-config': specifier: 0.76.1 - version: 0.76.1(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) + version: 0.76.1(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) '@react-native/gradle-plugin': specifier: 0.76.1 version: 0.76.1 @@ -332,7 +332,7 @@ importers: version: 2.2.4 jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + version: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) prettier: specifier: 2.8.8 version: 2.8.8 @@ -393,7 +393,7 @@ importers: version: 0.77.0(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) '@react-native/eslint-config': specifier: 0.77.0 - version: 0.77.0(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) + version: 0.77.0(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) '@react-native/gradle-plugin': specifier: ^0.77.0 version: 0.77.0 @@ -429,7 +429,7 @@ importers: version: link:../../packages/hot-updater jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + version: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) prettier: specifier: 2.8.8 version: 2.8.8 @@ -722,13 +722,19 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.6.4 - version: 0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0)) + version: 0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0)) '@cloudflare/workers-types': specifier: ^4.20250124.3 version: 4.20250124.3 '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 + '@types/node': + specifier: ^22.13.0 + version: 22.13.0 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 better-sqlite3: specifier: ^11.8.1 version: 11.8.1 @@ -750,6 +756,9 @@ importers: picocolors: specifier: ^1.0.0 version: 1.1.1 + semver: + specifier: ^7.6.3 + version: 7.6.3 toml: specifier: ^3.0.0 version: 3.0.0 @@ -758,7 +767,7 @@ importers: version: 5.7.2 vitest: specifier: 2.1.8 - version: 2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) + version: 2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) wrangler: specifier: ^3.101.0 version: 3.105.1(@cloudflare/workers-types@4.20250124.3) @@ -4904,6 +4913,9 @@ packages: '@types/node@20.16.10': resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} + '@types/node@22.13.0': + resolution: {integrity: sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==} + '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} @@ -10807,6 +10819,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici@5.28.5: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} @@ -13542,7 +13557,7 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/vitest-pool-workers@0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0))': + '@cloudflare/vitest-pool-workers@0.6.7(@cloudflare/workers-types@4.20250124.3)(@vitest/runner@2.1.8)(@vitest/snapshot@2.1.8)(vitest@2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0))': dependencies: '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -13552,7 +13567,7 @@ snapshots: esbuild: 0.17.19 miniflare: 3.20250124.0 semver: 7.6.3 - vitest: 2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) + vitest: 2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0) wrangler: 3.105.1(@cloudflare/workers-types@4.20250124.3) zod: 3.24.1 transitivePeerDependencies: @@ -13958,7 +13973,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -13972,7 +13987,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -13993,7 +14008,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -14007,7 +14022,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -14028,7 +14043,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -14042,7 +14057,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -14390,7 +14405,7 @@ snapshots: tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/js@20.3.0(@babel/traverse@7.25.9)(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(nx@20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)))(typescript@5.7.2)': + '@nx/js@20.3.0(@babel/traverse@7.25.9)(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(nx@20.3.0(@swc-node/register@1.10.9(@swc/core@1.7.42(@swc/helpers@0.5.15))(@swc/types@0.1.13)(typescript@5.7.2))(@swc/core@1.7.42(@swc/helpers@0.5.15)))(typescript@5.7.2)': dependencies: '@babel/core': 7.26.0 '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) @@ -14419,7 +14434,7 @@ snapshots: semver: 7.6.3 source-map-support: 0.5.19 tinyglobby: 0.2.10 - ts-node: 10.9.1(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2) + ts-node: 10.9.1(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2) tsconfig-paths: 4.2.0 tslib: 2.8.1 transitivePeerDependencies: @@ -15731,7 +15746,7 @@ snapshots: - supports-color - utf-8-validate - '@react-native/eslint-config@0.74.83(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)))(prettier@2.8.8)(typescript@5.7.2)': + '@react-native/eslint-config@0.74.83(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)))(prettier@2.8.8)(typescript@5.7.2)': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.24.5(@babel/core@7.26.0)(eslint@8.57.0) @@ -15742,7 +15757,7 @@ snapshots: eslint-config-prettier: 8.10.0(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.24.5(@babel/core@7.26.0)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)))(typescript@5.7.2) + eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)))(typescript@5.7.2) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -15753,7 +15768,7 @@ snapshots: - supports-color - typescript - '@react-native/eslint-config@0.76.1(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': + '@react-native/eslint-config@0.76.1(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.0) @@ -15764,7 +15779,7 @@ snapshots: eslint-config-prettier: 8.10.0(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(typescript@5.0.4) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(typescript@5.0.4) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-native: 4.1.0(eslint@8.57.0) @@ -15775,7 +15790,7 @@ snapshots: - supports-color - typescript - '@react-native/eslint-config@0.77.0(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': + '@react-native/eslint-config@0.77.0(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.0) @@ -15786,7 +15801,7 @@ snapshots: eslint-config-prettier: 8.10.0(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(typescript@5.0.4) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(typescript@5.0.4) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-native: 4.1.0(eslint@8.57.0) @@ -17162,23 +17177,23 @@ snapshots: '@types/better-sqlite3@7.6.12': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.5 - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/connect@3.4.38': @@ -17213,7 +17228,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -17244,7 +17259,7 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/istanbul-lib-coverage@2.0.5': {} @@ -17277,7 +17292,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 form-data: 4.0.0 '@types/node-forge@1.3.11': @@ -17292,6 +17307,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.13.0': + dependencies: + undici-types: 6.20.0 + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -17346,7 +17365,7 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/serve-index@1.9.4': @@ -17357,13 +17376,13 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.9.0 + '@types/node': 22.13.0 '@types/send': 0.17.4 optional: true '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 optional: true '@types/stack-utils@2.0.2': {} @@ -17720,13 +17739,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0))': + '@vitest/mocker@2.1.8(vite@5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0) + vite: 5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0) '@vitest/pretty-format@2.1.8': dependencies: @@ -18835,13 +18854,13 @@ snapshots: optionalDependencies: typescript: 5.7.2 - create-jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): + create-jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + jest-config: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -18850,13 +18869,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)): + create-jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -18865,13 +18884,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)): + create-jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -19431,24 +19450,24 @@ snapshots: - supports-color - typescript - eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)))(typescript@5.7.2): + eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)))(typescript@5.7.2): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 optionalDependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2) - jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + jest: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)))(typescript@5.0.4): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)))(typescript@5.0.4): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.0.4) eslint: 8.57.0 optionalDependencies: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.0.4))(eslint@8.57.0)(typescript@5.0.4) - jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + jest: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) transitivePeerDependencies: - supports-color - typescript @@ -20823,16 +20842,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): + jest-cli@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + create-jest: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + jest-config: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -20842,16 +20861,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)): + jest-cli@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + create-jest: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -20861,16 +20880,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)): + jest-cli@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + create-jest: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -20880,7 +20899,69 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): + jest-config@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.13.0 + ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.13.0 + ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -20906,12 +20987,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.9.0 - ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4) + ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)): + jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -20937,12 +21018,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.9.0 - ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4) + ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)): + jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -20968,7 +21049,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.9.0 - ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2) + ts-node: 10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -21221,36 +21302,36 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): + jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) + jest-cli: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)): + jest@29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4)) + jest-cli: 29.7.0(@types/node@22.13.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)): + jest@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2)) + jest-cli: 29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -25176,14 +25257,14 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.1(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2): + ts-node@10.9.1(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.9.0 + '@types/node': 22.13.0 acorn: 8.10.0 acorn-walk: 8.3.2 arg: 4.1.3 @@ -25196,70 +25277,70 @@ snapshots: optionalDependencies: '@swc/core': 1.7.42(@swc/helpers@0.5.15) - ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.8.7)(typescript@5.6.3): + ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.0.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.8.7 + '@types/node': 22.13.0 acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.3 + typescript: 5.0.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: '@swc/core': 1.7.42(@swc/helpers@0.5.15) optional: true - ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4): + ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.9.0 + '@types/node': 22.13.0 acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.8.4 + typescript: 5.7.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: '@swc/core': 1.7.42(@swc/helpers@0.5.15) optional: true - ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.0.4): + ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.8.7)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.9.0 + '@types/node': 22.8.7 acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.0.4 + typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: '@swc/core': 1.7.42(@swc/helpers@0.5.15) optional: true - ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@5.7.2): + ts-node@10.9.2(@swc/core@1.7.42(@swc/helpers@0.5.15))(@types/node@22.9.0)(typescript@4.8.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -25273,7 +25354,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.7.2 + typescript: 4.8.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: @@ -25418,6 +25499,8 @@ snapshots: undici-types@6.19.8: {} + undici-types@6.20.0: {} + undici@5.28.5: dependencies: '@fastify/busboy': 2.1.1 @@ -25616,13 +25699,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0): + vite-node@2.1.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0): dependencies: cac: 6.7.14 debug: 4.3.7 es-module-lexer: 1.6.0 pathe: 1.1.2 - vite: 5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0) + vite: 5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0) transitivePeerDependencies: - '@types/node' - less @@ -25634,21 +25717,21 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0): + vite@5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.22.5 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 fsevents: 2.3.3 sass-embedded: 1.83.0 terser: 5.31.0 - vitest@2.1.8(@types/node@22.9.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0): + vitest@2.1.8(@types/node@22.13.0)(jsdom@25.0.1)(sass-embedded@1.83.0)(terser@5.31.0): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0)) + '@vitest/mocker': 2.1.8(vite@5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -25664,11 +25747,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0) - vite-node: 2.1.8(@types/node@22.9.0)(sass-embedded@1.83.0)(terser@5.31.0) + vite: 5.4.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0) + vite-node: 2.1.8(@types/node@22.13.0)(sass-embedded@1.83.0)(terser@5.31.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 22.13.0 jsdom: 25.0.1 transitivePeerDependencies: - less From 0639e25e0c0727e30900067613c02036de2a4de4 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 16:26:39 +0900 Subject: [PATCH 32/61] feat: filter compatible app versions --- biome.json | 2 - plugins/cloudflare/package.json | 5 - plugins/cloudflare/tsup.config.ts | 3 - .../worker/src/getUpdateInfo.spec.ts | 38 ++-- .../cloudflare/worker/src/getUpdateInfo.ts | 23 +- plugins/cloudflare/worker/src/index.ts | 196 ++---------------- plugins/cloudflare/worker/wrangler.json | 44 +--- .../src/filterCompatibleAppVersions.spec.ts | 49 +++++ plugins/js/src/filterCompatibleAppVersions.ts | 20 ++ plugins/js/src/index.ts | 1 + plugins/js/src/semverSatisfies.ts | 19 -- pnpm-lock.yaml | 175 ---------------- vitest.config.mts | 19 -- vitest.workspace.mts | 1 + 14 files changed, 133 insertions(+), 462 deletions(-) create mode 100644 plugins/js/src/filterCompatibleAppVersions.spec.ts create mode 100644 plugins/js/src/filterCompatibleAppVersions.ts delete mode 100644 vitest.config.mts create mode 100644 vitest.workspace.mts diff --git a/biome.json b/biome.json index 2fe4c6ed..0d6cf6a5 100644 --- a/biome.json +++ b/biome.json @@ -9,8 +9,6 @@ "**/dist/**", "**/build/**", "node_modules/**", - "**/worker/**", - "**/*.js" ] }, diff --git a/plugins/cloudflare/package.json b/plugins/cloudflare/package.json index 6b83cd26..ac093f2b 100644 --- a/plugins/cloudflare/package.json +++ b/plugins/cloudflare/package.json @@ -38,8 +38,6 @@ "dev": "wrangler dev worker/src/index.ts" }, "dependencies": { - "@aws-sdk/client-s3": "^3.685.0", - "@aws-sdk/lib-storage": "^3.685.0", "@hot-updater/core": "0.5.10", "@hot-updater/js": "0.6.0", "@hot-updater/plugin-core": "0.5.10", @@ -48,11 +46,8 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250124.3", - "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.13.0", "@types/semver": "^7.5.8", - "better-sqlite3": "^11.8.1", - "camelcase-keys": "^9.1.3", "dayjs": "^1.11.13", "execa": "^9.5.2", "mime": "^4.0.4", diff --git a/plugins/cloudflare/tsup.config.ts b/plugins/cloudflare/tsup.config.ts index 74da0a25..6953a8bd 100644 --- a/plugins/cloudflare/tsup.config.ts +++ b/plugins/cloudflare/tsup.config.ts @@ -4,7 +4,4 @@ export default defineConfig({ entry: ["src/index.ts", "src/utils/index.ts"], format: ["esm", "cjs"], dts: true, - banner: { - js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`, - }, }); diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts index 30fb4c83..f1575059 100644 --- a/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts +++ b/plugins/cloudflare/worker/src/getUpdateInfo.spec.ts @@ -2,22 +2,23 @@ import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core"; import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils"; import { beforeAll, beforeEach, describe, inject } from "vitest"; import { getUpdateInfo as getUpdateInfoFromWorker } from "./getUpdateInfo"; + import { env } from "cloudflare:test"; declare module "vitest" { + // biome-ignore lint/suspicious/noExportsInTest: export interface ProvidedContext { prepareSql: string; } } - -declare module 'cloudflare:test' { +declare module "cloudflare:test" { interface ProvidedEnv { - DB: D1Database + DB: D1Database; } } const createInsertBundleQuery = (bundle: Bundle) => { - return /* sql */` + return ` INSERT INTO bundles ( id, file_url, file_hash, platform, target_app_version, should_force_update, enabled, git_commit_hash, message @@ -35,20 +36,21 @@ const createInsertBundleQuery = (bundle: Bundle) => { `; }; -const createGetUpdateInfo = (db: D1Database) => -async ( - bundles: Bundle[], - { appVersion, bundleId, platform }: GetBundlesArgs, -): Promise => { - if(bundles.length > 0) { - await db.prepare(createInsertBundleQuerys(bundles)).run(); - } - return await getUpdateInfoFromWorker(db, { - appVersion, - bundleId, - platform, - }) as UpdateInfo | null; -}; +const createGetUpdateInfo = + (db: D1Database) => + async ( + bundles: Bundle[], + { appVersion, bundleId, platform }: GetBundlesArgs, + ): Promise => { + if (bundles.length > 0) { + await db.prepare(createInsertBundleQuerys(bundles)).run(); + } + return (await getUpdateInfoFromWorker(db, { + appVersion, + bundleId, + platform, + })) as UpdateInfo | null; + }; const createInsertBundleQuerys = (bundles: Bundle[]) => { return bundles.map(createInsertBundleQuery).join("\n"); diff --git a/plugins/cloudflare/worker/src/getUpdateInfo.ts b/plugins/cloudflare/worker/src/getUpdateInfo.ts index 39f5ec23..102edadb 100644 --- a/plugins/cloudflare/worker/src/getUpdateInfo.ts +++ b/plugins/cloudflare/worker/src/getUpdateInfo.ts @@ -1,28 +1,36 @@ import { filterCompatibleAppVersions } from "@hot-updater/js"; -import { Platform, UpdateInfo, UpdateStatus } from "@hot-updater/core"; + +import type { Platform, UpdateInfo, UpdateStatus } from "@hot-updater/core"; export const getUpdateInfo = async ( DB: D1Database, - { platform, appVersion, bundleId }: { + { + platform, + appVersion, + bundleId, + }: { platform: Platform; appVersion: string; bundleId: string; }, ) => { - const appVersionList = await DB.prepare(/* sql */ ` + const appVersionList = await DB.prepare( + /* sql */ ` SELECT target_app_version FROM bundles WHERE platform = ? GROUP BY target_app_version - `).bind(platform).all<{ target_app_version: string; count: number }>(); + `, + ) + .bind(platform) + .all<{ target_app_version: string; count: number }>(); const targetAppVersionList = filterCompatibleAppVersions( appVersionList.results.map((group) => group.target_app_version), appVersion, ); - const sql = /* sql */ ` WITH input AS ( SELECT @@ -42,9 +50,8 @@ export const getUpdateInfo = async ( WHERE b.enabled = 1 AND b.platform = input.app_platform AND b.id >= input.bundle_id - AND b.target_app_version IN (${targetAppVersionList.map(version => `'${version}'`).join(",")}) + AND b.target_app_version IN (${targetAppVersionList.map((version) => `'${version}'`).join(",")}) ORDER BY b.id DESC - -- semver 규칙에 따라 최신 버전을 선택 로직 ㄱㄱ LIMIT 1 ), rollback_candidate AS ( @@ -82,7 +89,7 @@ export const getUpdateInfo = async ( FROM input WHERE (SELECT COUNT(*) FROM final_result) = 0 AND bundle_id <> nil_uuid; - `; + `; const result = await DB.prepare(sql) .bind(platform, appVersion, bundleId) diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts index 4cdb3888..17d79661 100644 --- a/plugins/cloudflare/worker/src/index.ts +++ b/plugins/cloudflare/worker/src/index.ts @@ -11,188 +11,34 @@ * Learn more at https://developers.cloudflare.com/workers/ */ -import { Bundle, SnakeCaseBundle } from "@hot-updater/core"; -import { getUpdateInfo } from "@hot-updater/js"; -import camelcaseKeys from "camelcase-keys"; +import { getUpdateInfo } from "./getUpdateInfo"; export default { async fetch(request, env, ctx): Promise { const bundleId = request.headers.get("x-bundle-id") as string; - const appPlatform = request.headers.get("x-app-platform") as "ios" | "android"; + const appPlatform = request.headers.get("x-app-platform") as + | "ios" + | "android"; const appVersion = request.headers.get("x-app-version") as string; if (!bundleId || !appPlatform || !appVersion) { - return new Response(JSON.stringify({ error: "Missing bundleId, appPlatform, or appVersion" }), { status: 400 }); + return new Response( + JSON.stringify({ + error: "Missing bundleId, appPlatform, or appVersion", + }), + { status: 400 }, + ); } - - // SQLite 전용 SQL 쿼리 – PostgreSQL의 semver_satisfies 모든 로직을 CASE 식으로 구현 - const sql = ` - WITH input AS ( - SELECT - ? AS app_platform, -- 예: 'ios' 또는 'android' - ? AS app_version, -- 예: '1.2.3' - ? AS bundle_id, -- 현재 번들의 id (문자열) - '00000000-0000-0000-0000-000000000000' AS nil_uuid - ), - update_candidates AS ( - SELECT - b.id, - b.should_force_update, - b.file_url, - b.file_hash, - 'UPDATE' AS status, - -- semver 조건 평가: PostgreSQL의 semver_satisfies의 모든 패턴을 구현 - CASE - -- (1) 정확한 와일드카드: "*" - WHEN b.target_app_version = '*' THEN 1 - - -- (2) 정확한 버전: "N.M.P" (예: "1.2.3") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[0-9]*' - AND b.target_app_version NOT LIKE '%x%' AND b.target_app_version NOT LIKE '%X%' - THEN CASE WHEN b.target_app_version = input.app_version THEN 1 ELSE 0 END - - -- (3) major.x.x 패턴 (예: "1.x.x") - WHEN b.target_app_version GLOB '[0-9]*.[xX].[xX]' - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (4) major.minor.x 패턴 (예: "1.2.x") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*.[xX]' - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1, - instr(substr(b.target_app_version, instr(b.target_app_version, '.')+1), '.') - 1) AS INTEGER) - = - CAST(substr(input.app_version, instr(input.app_version, '.')+1, - instr(substr(input.app_version, instr(input.app_version, '.')+1), '.') - 1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (5) major.minor 패턴 (예: "1.2") - WHEN b.target_app_version GLOB '[0-9]*.[0-9]*' - AND NOT(b.target_app_version GLOB '*.[0-9]*.[0-9]*') - THEN CASE - WHEN CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) = - CAST(substr(input.app_version, 1, instr(input.app_version, '.') - 1) AS INTEGER) - AND - CAST(substr(b.target_app_version, instr(b.target_app_version, '.')+1) AS INTEGER) = - CAST(substr(input.app_version, instr(input.app_version, '.')+1) AS INTEGER) - THEN 1 ELSE 0 END - - -- (6) dash 범위: "N.M.P - N.M.P" (예: "1.2.3 - 1.2.7") - WHEN b.target_app_version GLOB '* - *' - THEN CASE - WHEN input.app_version >= trim(substr(b.target_app_version, 1, instr(b.target_app_version, '-') - 1)) - AND input.app_version <= trim(substr(b.target_app_version, instr(b.target_app_version, '-')+1)) - THEN 1 ELSE 0 END - - -- (7) 부등호 범위: ">=N.M.P =1.2.3 <2.0.0") - WHEN b.target_app_version GLOB '>=* <*' - THEN CASE - WHEN input.app_version >= trim(substr(b.target_app_version, 3, instr(b.target_app_version, ' ') - 3)) - AND input.app_version < trim(substr(b.target_app_version, instr(b.target_app_version, '<')+1)) - THEN 1 ELSE 0 END - - -- (8) tilde(~) 패턴: "~N.M.P" (예: "~1.2.3" ⇒ >=1.2.3 AND <1.3.0) - WHEN b.target_app_version GLOB '~[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(b.target_app_version, 2) - AND input.app_version < ( - CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS TEXT) - || '.' || - CAST(CAST(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1, - instr(substr(substr(b.target_app_version,2), instr(substr(b.target_app_version,2),'.')+1),'.') - 1) AS INTEGER) + 1 AS TEXT) - || '.0' - ) - THEN 1 ELSE 0 END - - -- (9) caret(^) 패턴: "^N.M.P" (예: "^1.2.3" ⇒ >=1.2.3 AND <2.0.0) - WHEN b.target_app_version GLOB '\\^[0-9]*.[0-9]*.[0-9]*' - THEN CASE - WHEN input.app_version >= substr(b.target_app_version, 2) - AND input.app_version < ( - CAST(CAST(substr(substr(b.target_app_version,2), 1, instr(substr(b.target_app_version,2),'.')-1) AS INTEGER) + 1 AS TEXT) - || '.0.0' - ) - THEN 1 ELSE 0 END - - -- (10) 단일 major 버전: "N" (예: "1" ⇒ >=1.0.0 AND <2.0.0) - WHEN b.target_app_version GLOB '[0-9]+' - THEN CASE - WHEN input.app_version >= (b.target_app_version || '.0.0') - AND input.app_version < (CAST(b.target_app_version AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - -- (11) major.x 패턴: "N.x" (예: "1.x" ⇒ >=1.0.0 AND <2.0.0) - WHEN b.target_app_version GLOB '[0-9]+\.x' - THEN CASE - WHEN input.app_version >= (substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) || '.0.0') - AND input.app_version < (CAST(substr(b.target_app_version, 1, instr(b.target_app_version, '.') - 1) AS INTEGER) + 1 || '.0.0') - THEN 1 ELSE 0 END - - ELSE 0 - END AS version_match - FROM bundles b, input - WHERE b.enabled = 1 - AND b.platform = input.app_platform - AND b.id >= input.bundle_id - ORDER BY b.id DESC - ), - update_candidate AS ( - SELECT id, should_force_update, file_url, file_hash, status - FROM update_candidates - WHERE version_match = 1 - LIMIT 1 - ), - rollback_candidate AS ( - SELECT - b.id, - 1 AS should_force_update, - b.file_url, - b.file_hash, - 'ROLLBACK' AS status - FROM bundles b, input - WHERE b.enabled = 1 - AND b.platform = input.app_platform - AND b.id < input.bundle_id - ORDER BY b.id DESC - LIMIT 1 - ), - final_result AS ( - SELECT * FROM update_candidate - UNION ALL - SELECT * FROM rollback_candidate - WHERE NOT EXISTS (SELECT 1 FROM update_candidate) - ) - SELECT id, should_force_update, file_url, file_hash, status - FROM final_result, input - WHERE id <> bundle_id - - UNION ALL - - SELECT - nil_uuid AS id, - 1 AS should_force_update, - NULL AS file_url, - NULL AS file_hash, - 'ROLLBACK' AS status - FROM input - WHERE (SELECT COUNT(*) FROM final_result) = 0 - AND bundle_id <> nil_uuid; - `; - - // 바인딩 순서: app_platform, app_version, bundle_id - const result = await env.DB.prepare(sql) - .bind(appPlatform, appVersion, bundleId) - .first(); - - return new Response(JSON.stringify(result), { - headers: { "Content-Type": "application/json" }, - status: 200 - }); - // return new Response(JSON.stringify(updaterInfo)); + + const updateInfo = await getUpdateInfo(env.DB, { + appVersion, + bundleId, + platform: appPlatform, + }); + + return new Response(JSON.stringify(updateInfo), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); }, } satisfies ExportedHandler; diff --git a/plugins/cloudflare/worker/wrangler.json b/plugins/cloudflare/worker/wrangler.json index 8d616cec..215e4ac7 100644 --- a/plugins/cloudflare/worker/wrangler.json +++ b/plugins/cloudflare/worker/wrangler.json @@ -1,47 +1,15 @@ -/** - * For more details on how to configure Wrangler, refer to: - * https://developers.cloudflare.com/workers/wrangler/configuration/ - */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "worker", "main": "src/index.ts", "compatibility_date": "2025-01-24", + "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true }, - /** - * Smart Placement - * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement - */ - // "placement": { "mode": "smart" }, - - /** - * Bindings - * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including - * databases, object storage, AI inference, real-time communication and more. - * https://developers.cloudflare.com/workers/runtime-apis/bindings/ - */ - - /** - * Environment Variables - * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables - */ - // "vars": { "MY_VARIABLE": "production_value" }, - /** - * Note: Use secrets to store sensitive data. - * https://developers.cloudflare.com/workers/configuration/secrets/ - */ - - /** - * Static Assets - * https://developers.cloudflare.com/workers/static-assets/binding/ - */ - // "assets": { "directory": "./public/", "binding": "ASSETS" }, - - /** - * Service Bindings (communicate between multiple Workers) - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings - */ - // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] + "d1_databases": [ + { + "binding": "DB" + } + ] } diff --git a/plugins/js/src/filterCompatibleAppVersions.spec.ts b/plugins/js/src/filterCompatibleAppVersions.spec.ts new file mode 100644 index 00000000..95b4f09a --- /dev/null +++ b/plugins/js/src/filterCompatibleAppVersions.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { filterCompatibleAppVersions } from "./filterCompatibleAppVersions"; + +describe("filterCompatibleAppVersions", () => { + it("should filter and sort compatible versions", () => { + const targetVersions = ["1.0.0", ">=1.2.0", "2.0.0"]; + const currentVersion = "1.5.0"; + + const result = filterCompatibleAppVersions(targetVersions, currentVersion); + + expect(result).toEqual([">=1.2.0"]); + }); + + it("should handle semver ranges and wildcards", () => { + const targetVersions = ["*", "^1.0.0", "~2.0.0", ">=1.5.0"]; + const currentVersion = "2.0.1"; + + const result = filterCompatibleAppVersions(targetVersions, currentVersion); + + expect(result).toEqual(["~2.0.0", ">=1.5.0", "*"]); + }); + + it("should return empty array for incompatible versions", () => { + const targetVersions = ["1.0.0", "1.1.0"]; + const currentVersion = "2.0.0"; + + const result = filterCompatibleAppVersions(targetVersions, currentVersion); + + expect(result).toEqual([]); + }); + + it("should handle invalid version strings", () => { + const targetVersions = ["invalid", "1.0.0", ">=1.0.0"]; + const currentVersion = "1.0.0"; + + const result = filterCompatibleAppVersions(targetVersions, currentVersion); + + expect(result).toEqual(["1.0.0", ">=1.0.0"]); + }); + + it("should return empty array for invalid current version", () => { + const targetVersions = ["1.0.0", "2.0.0"]; + const currentVersion = "invalid"; + + const result = filterCompatibleAppVersions(targetVersions, currentVersion); + + expect(result).toEqual([]); + }); +}); diff --git a/plugins/js/src/filterCompatibleAppVersions.ts b/plugins/js/src/filterCompatibleAppVersions.ts new file mode 100644 index 00000000..e80d56ca --- /dev/null +++ b/plugins/js/src/filterCompatibleAppVersions.ts @@ -0,0 +1,20 @@ +import { semverSatisfies } from "./semverSatisfies"; + +/** + * Filters target app versions that are compatible with the current app version. + * Returns only versions that are compatible with the current version according to semver rules. + * + * @param targetAppVersionList - List of target app versions to filter + * @param currentVersion - Current app version + * @returns Array of target app versions compatible with the current version + */ +export const filterCompatibleAppVersions = ( + targetAppVersionList: string[], + currentVersion: string, +) => { + const compatibleAppVersionList = targetAppVersionList.filter((version) => + semverSatisfies(version, currentVersion), + ); + + return compatibleAppVersionList.sort((a, b) => b.localeCompare(a)); +}; diff --git a/plugins/js/src/index.ts b/plugins/js/src/index.ts index 18ba9969..fb27acc6 100644 --- a/plugins/js/src/index.ts +++ b/plugins/js/src/index.ts @@ -1,2 +1,3 @@ export * from "./getUpdateInfo"; export * from "./semverSatisfies"; +export * from "./filterCompatibleAppVersions"; diff --git a/plugins/js/src/semverSatisfies.ts b/plugins/js/src/semverSatisfies.ts index 317e1292..8bb44d9f 100644 --- a/plugins/js/src/semverSatisfies.ts +++ b/plugins/js/src/semverSatisfies.ts @@ -11,22 +11,3 @@ export const semverSatisfies = ( return semver.satisfies(currentCoerce.version, targetAppVersion); }; - -/** - * Filters target app versions that are compatible with the current app version. - * Returns only versions that are compatible with the current version according to semver rules. - * - * @param targetAppVersionList - List of target app versions to filter - * @param currentVersion - Current app version - * @returns Array of target app versions compatible with the current version - */ -export const filterCompatibleAppVersions = ( - targetAppVersionList: string[], - currentVersion: string, -) => { - const compatibleAppVersionList = targetAppVersionList.filter((version) => - semverSatisfies(version, currentVersion), - ); - - return compatibleAppVersionList; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0201baac..d5b355d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -701,12 +701,6 @@ importers: plugins/cloudflare: dependencies: - '@aws-sdk/client-s3': - specifier: ^3.685.0 - version: 3.685.0 - '@aws-sdk/lib-storage': - specifier: ^3.685.0 - version: 3.685.0(@aws-sdk/client-s3@3.685.0) '@hot-updater/core': specifier: workspace:* version: link:../../packages/core @@ -726,21 +720,12 @@ importers: '@cloudflare/workers-types': specifier: ^4.20250124.3 version: 4.20250124.3 - '@types/better-sqlite3': - specifier: ^7.6.12 - version: 7.6.12 '@types/node': specifier: ^22.13.0 version: 22.13.0 '@types/semver': specifier: ^7.5.8 version: 7.5.8 - better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 - camelcase-keys: - specifier: ^9.1.3 - version: 9.1.3 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -4819,9 +4804,6 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/better-sqlite3@7.6.12': - resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} - '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -5672,9 +5654,6 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - better-sqlite3@11.8.1: - resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} - big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -5682,9 +5661,6 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} @@ -5891,9 +5867,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -6213,10 +6186,6 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -6232,10 +6201,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -6320,10 +6285,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -6761,10 +6722,6 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -6872,9 +6829,6 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -7084,9 +7038,6 @@ packages: git-url-parse@13.1.1: resolution: {integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -7431,9 +7382,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -8653,10 +8601,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - miniflare@3.20250124.0: resolution: {integrity: sha512-ewsetUwhj4FqeLoE3UMqYHHyCYIOPzdhlpF9CHuHpMZbfLvI9SPd+VrKrLfOgyAF97EHqVWb6WamIrLdgtj6Kg==} engines: {node: '>=16.13'} @@ -8695,9 +8639,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -8736,9 +8677,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -8759,10 +8697,6 @@ packages: resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} engines: {node: '>=12.0.0'} - node-abi@3.73.0: - resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} - engines: {node: '>=10'} - node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -9335,11 +9269,6 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9463,10 +9392,6 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - react-devtools-core@4.28.4: resolution: {integrity: sha512-IUZKLv3CimeM07G3vX4H4loxVpByrzq3HvfTX7v9migalwvLs9ZY5D3S3pKR33U+GguYfBBdMMZyToFhsSE/iQ==} @@ -10199,12 +10124,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -10432,10 +10351,6 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -10495,9 +10410,6 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - tar-fs@2.1.2: - resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} - tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -10726,9 +10638,6 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -17175,10 +17084,6 @@ snapshots: dependencies: '@babel/types': 7.26.0 - '@types/better-sqlite3@7.6.12': - dependencies: - '@types/node': 22.13.0 - '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 @@ -18347,19 +18252,10 @@ snapshots: batch@0.6.1: optional: true - better-sqlite3@11.8.1: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - big.js@5.2.2: {} binary-extensions@2.3.0: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - birpc@0.2.14: {} bl@4.1.0: @@ -18589,8 +18485,6 @@ snapshots: dependencies: readdirp: 4.1.1 - chownr@1.1.4: {} - chrome-launcher@0.15.2: dependencies: '@types/node': 22.9.0 @@ -18969,18 +18863,12 @@ snapshots: dependencies: character-entities: 2.0.2 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - dedent@0.7.0: {} dedent@1.5.3: {} deep-eql@5.0.2: {} - deep-extend@0.6.0: {} - deep-is@0.1.4: {} deepmerge@3.3.0: {} @@ -19065,8 +18953,6 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.0.3: {} - detect-newline@3.1.0: {} detect-node@2.1.0: @@ -19683,8 +19569,6 @@ snapshots: exit@0.1.2: {} - expand-template@2.0.3: {} - expect-type@1.1.0: {} expect@29.7.0: @@ -19846,8 +19730,6 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-uri-to-path@1.0.0: {} - filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -20070,8 +19952,6 @@ snapshots: dependencies: git-up: 7.0.0 - github-from-package@0.0.0: {} - github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -20499,8 +20379,6 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} - inline-style-parser@0.1.1: {} internal-slot@1.0.7: @@ -22810,8 +22688,6 @@ snapshots: mimic-fn@2.1.0: {} - mimic-response@3.1.0: {} - miniflare@3.20250124.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -22859,8 +22735,6 @@ snapshots: minipass@7.1.2: {} - mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -22896,8 +22770,6 @@ snapshots: nanoid@3.3.7: {} - napi-build-utils@2.0.0: {} - natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -22910,10 +22782,6 @@ snapshots: nocache@3.0.4: {} - node-abi@3.73.0: - dependencies: - semver: 7.6.3 - node-abort-controller@3.1.1: {} node-dir@0.1.17: @@ -23523,21 +23391,6 @@ snapshots: postgres-range@1.1.4: {} - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.0.3 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.73.0 - pump: 3.0.2 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.2 - tunnel-agent: 0.6.0 - prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -23654,13 +23507,6 @@ snapshots: unpipe: 1.0.0 optional: true - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - react-devtools-core@4.28.4: dependencies: shell-quote: 1.8.1 @@ -24763,14 +24609,6 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -25025,8 +24863,6 @@ snapshots: strip-final-newline@4.0.0: {} - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} strnum@1.0.5: {} @@ -25101,13 +24937,6 @@ snapshots: tapable@2.2.1: {} - tar-fs@2.1.2: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.2 - tar-stream: 2.2.0 - tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -25414,10 +25243,6 @@ snapshots: tslib: 1.14.1 typescript: 5.7.2 - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/vitest.config.mts b/vitest.config.mts deleted file mode 100644 index 13e5fe73..00000000 --- a/vitest.config.mts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: [{ find: "@", replacement: resolve(__dirname, "./src") }], - }, - test: { - exclude: [ - "lib/**", - "**/lib/**", - "dist/**", - "**/dist/**", - "**/node_modules/**", - "node_modules/**", - "examples/**", - ], - }, -}); diff --git a/vitest.workspace.mts b/vitest.workspace.mts new file mode 100644 index 00000000..d941cc05 --- /dev/null +++ b/vitest.workspace.mts @@ -0,0 +1 @@ +export default ["packages/*", "plugins/*"]; From c625c8396ed15622d42eb575cba0c5db307c1bac Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 16:31:34 +0900 Subject: [PATCH 33/61] fix: binding --- plugins/cloudflare/vitest.config.mts | 2 +- plugins/cloudflare/worker/wrangler.test.json | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 plugins/cloudflare/worker/wrangler.test.json diff --git a/plugins/cloudflare/vitest.config.mts b/plugins/cloudflare/vitest.config.mts index 6abd7e3a..e7243a5e 100644 --- a/plugins/cloudflare/vitest.config.mts +++ b/plugins/cloudflare/vitest.config.mts @@ -5,7 +5,7 @@ export default defineWorkersConfig({ globalSetup: "./vitest.global-setup.mts", poolOptions: { workers: { - wrangler: { configPath: "./worker/wrangler.json" }, + wrangler: { configPath: "./worker/wrangler.test.json" }, }, }, }, diff --git a/plugins/cloudflare/worker/wrangler.test.json b/plugins/cloudflare/worker/wrangler.test.json new file mode 100644 index 00000000..e9716b49 --- /dev/null +++ b/plugins/cloudflare/worker/wrangler.test.json @@ -0,0 +1,16 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker", + "main": "src/index.ts", + "compatibility_date": "2025-01-24", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true + }, + "d1_databases": [ + { + "binding": "DB", + "database_id": "00000000-0000-0000-0000-000000000000" + } + ] +} From 792aaa4c8a1ff3023285117ee7dd11b572d20bb2 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 16:37:46 +0900 Subject: [PATCH 34/61] feat: /api/check-update --- plugins/cloudflare/worker/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/cloudflare/worker/src/index.ts b/plugins/cloudflare/worker/src/index.ts index 17d79661..e8b4f00f 100644 --- a/plugins/cloudflare/worker/src/index.ts +++ b/plugins/cloudflare/worker/src/index.ts @@ -15,6 +15,12 @@ import { getUpdateInfo } from "./getUpdateInfo"; export default { async fetch(request, env, ctx): Promise { + const url = new URL(request.url); + + if (url.pathname !== "/api/check-update") { + return new Response("Not found", { status: 404 }); + } + const bundleId = request.headers.get("x-bundle-id") as string; const appPlatform = request.headers.get("x-app-platform") as | "ios" From 5ecfdec3975b3f189a18a8ecc686dda8181ac723 Mon Sep 17 00:00:00 2001 From: gronxb Date: Sun, 2 Feb 2025 16:41:01 +0900 Subject: [PATCH 35/61] fix: docs --- docs/docs/guide/console.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docs/guide/console.mdx b/docs/docs/guide/console.mdx index cc1e80d4..49e444d6 100644 --- a/docs/docs/guide/console.mdx +++ b/docs/docs/guide/console.mdx @@ -15,7 +15,10 @@ For security reasons, only localhost server is supported at the moment.

! zc^gd{h&;xoE~g>L+}Ss2qb<@D6PUy&OH|hQVLhAO2Ejd`Q9!*t2->1&#>`E?UV&dw zmdZg!UUB)nVpU@N{DVRR_&?5DVQ26+|*(+Za2vWQ?v zgB3?hk;(1#J*{nnc@GSx!&RH5zSZz*Z4QrH!ZX5T9`q*$li|S+c z)4KY=z5NvGQ1iN~gEhzQ_}b5p+}-;GA1S<}^EGIPnBZkm-#(NvZ5(rvLp1SjB&!Np z!5TO>P;<&8&8rHeY}`D%3!gbARW8D zvGwoYypDVDqmdRKrHl({=iu0aO_juv_@SLm1kSBP-c$1#$mz5S-y_wW$Y%yiuyOVs z@rhamyXygZv*B(DHhYyhE<<|^$DTdQ{4Tq*V<0e4Y}-+EhZ7TVVBysqtr;mL-*lJ3 z!s$yGCS?26x{}>-?OVtE08CKzNio*C`(BHv0YTdi_ALs^QkczUvy@Oz(dd*09Wu|F z7?exD13@Pl7Ul2tjpVyOMLur6l+%3`f0jc%e4d_0_no4d>vVcr^f9MW0l#b&Z+T>X zlfXTAjhtmP?@Fv)v+UOgVcf;xS=YfyjxRKAMHb3dWeIbKYp zH+5QV>e$QY0V%Yk%_>G*e3z##*Gie{CxnxMTf8U2&M3b(FIDmL93{g4nFqbW;BzoF zE_spA<0c^21TV#3P7LWLUHaF{y+TB;ds5XJnIP`+8a!rxrT(3*$9aGyy9=uZ#A6#4cr;0wlbya(n z$vnE(Sy>&XJXbq30V+B@P&dgz{#@2v`Np7t{qJX$UI68@FS;~ssby7;{lFyFOT$9J zwdKNST}2}PMDybUuJ(HI#YY;4;--7pKf|>(; zeo<)a3zfaRxQwoT(fD0CJdrdoyi}q!y8u~;p7%A4(#D|+-1nLk3yqqZ`a7b-c!ho_ z-a|{+PG8~*#O&?u1r*x0d3lxbWZC5F$CAYe7QDkcHT%nt&{L^HsP3-&E}x!8UU}61 z0{t=Ni#+hU3fHc}jdn%@dEEb*SZR?^ysuZS) zg7+wQm0ORMjm+g_{aQK%2AvJ`DA2YOUf(IAx^S6NN>t~B>`;bLjPMj8v zoW&?{iQPqCtksZsqU#VS9EIPhL6#EXkqaW#*hLlfBkUO}j=+HCb|m>{fO?^f^Fv;X zytwV3S{t}QSho(cA1SuJ(uift8O5-#w=h>`m6PA{z88_H-6UY~nZEY6xAI+d8kJ_(`Qu)>yt_R>g>!pC`;SS$2Int0ZB6y3!H)HN7R~%>8%oE$was( z009tXy!qGK9L!vmVBVK|e}1}wZ2clRlB|awa_mEzpbJyYs5<#5%yvETq=XJ07da^; zc_zS)9jO>Tvk)DJPWXsHLH`S1uQdMP1fE$H ztO)$l`yx;y0HN^)5xB6Y)&$2W&VSG^jBF7DrP#F@0*}6(XEncr=TvHhyY)TKFYzBx ztmiWCS1g<$_^`?!h9d=Z`M*Q#+k@h!K`SB1_V`}aPkZhSqzc-Azy-j7K9 z*gtcPe~w^7Z}79bq+~sqv0+;C!p|+YlFY~z^u#WRJxhH?#gI{Q3_bo7>-oI22Qe2g z&Mj4@rwze{N!VGh$MtGoTi>Dl-z(Ba^|S43klcciq%&u-6@lEKzAk+<^xG43R=66J zhGt)MXkbrcvB%qxePpKz)j|N2NW#2QXYs>Eu42@ya39?zD%Ja}KufMhWQplM=URp7 zqRINIypO~1cPvu)zWdZ81ax6p-NbyU(D*`o7=Zt5I4|B= z{S)ia626tQ-|0@434R|Rh>Gu|$wv>;Sw)>7bR5(&SQXSUx!o2n03Z+43p3HL8Xb`0 zyosPabQL)Ntb&hey;6x^YX{Hkfzdh^HI=}Hz<`m_z7=7aGUgeC%!3iE1hHIanln4S zz4xTU21`MqqwbC7XlY zi0CSZ1kv+1s3FOeef&MiDzCUNRyz*55PQ0`tQZB-ZU;~>7^ACFIg}GMV|}!ug;xO{ zt0=p2zn71~$g~6;A$tJu4+T&WbxInZiv&^=&q#CYSoN<*0#_#h66zRn4_Ev7R-l7^ zsrHsSiqB(wbfTw~BuYrTRpu1n?KJ9Wkg`{#xsQL6u^9dF*3-Pz`GunLCyv=vGUw=r z{3>@qc^fZlj|9csb!Gc~U{cRqBui(nN?})|^=ZVg7oavIC36!zx9ioimA&A-AOe_C zI@h_LTp=e2-MGbMgT%hK>EoEE(=qEyetT&^2ncT!FqqkT94Vu(Ev z^nyldIH*St>39(MjF6^aGAk`g_*Q?_&(Ml}`FSWf{^wKg46>}ZPpb2r#I;5_6aD1O zb@~V`$~>>zI`m-5A0|6PTF#@hCmhc1GyB)2ra1>!#4zU{1)2Yr7;pmtuB@KQjdtuQL1< z1hT5YAr{aM+KLcB1IGe@Q1^R_dd)c{z`a2p_je_AFi{hOBIg@s@>F z?n^%qmA5E%YJxUAKL3eYI#1{l=>IrjKrqVwBT@}7#4m<8y$W96m>{TQR8&^0p|-*P z>azQ4#@ax;9ecVHpG4j0W(Cpd-1_0HfeZ~O)yeGXBU+?Bf3dclfSgd&>zPg+ig?$^ zQ)Gt3Jj?$ShEYmZ(dMChTNMp@8-=W&g-#t-b^9OKvIiWA#^shkp%xze3Pq`dT|K3> zw{v}m*peEaZ|m11fmsnkzjlh?GBM(?Ff|hF?r1<3l!Og+z~85Vo(vX{;fk<`^GcLA*hZm>+{Vk0O$ipOSe*d9dJTnACkUk>iLJr!&N_(5d&cMcf zfv&ydjBELTISS|EOEDNQZNz_^3q#QP=Q7|BJ~YPoZ!S3y=R%8(nNtvAJQ_kQo>@WC*0H0|g|L8u zgGAQb=>j3W5CbHsLM91!%R_Vj|CJ-b9fThUf5=xaJ^+@3*4TI~)loi~0l1Rk@L=CS z2ov8$36^xG&Tf+X@SfMdGyMtC94&b*{^XappbPeH9+7nR8M;e3b@Z;yC$LggrO>Pg zT;uzXbZa{)V4bNxySx3zPS8N-DSkR4>r4q+QWXhtZisYyD-J47679y&N15s^ZZP1Q z|IyAa0|7Tiz6&~v%49&av#$EO1{ef~=mA6E!X;3u9f@@-3%sPXI0~{NcBg zhs6kg)*o6&PG}uF$0m)zh ztwU=F|EWb4cyf&Y&qw}C(1h5IWXmu>V~je-hF^jgd9MMD>D^w6EjctM{ohP&0nbx}N<(~+)ll>}m;Re8VvX9GwBJykV4 z8u9iE#6aTg3=R&21S*#XEK>Vcf3f*tEAqdozJs{IHvchiAJl?v#fwV1aTL!s12JzM zmUC^D!GIq)Lc4dK_`UORe9pg>>1XYbnrQyVJyQY;mHIK~NH<}wfmTSb>soP%2AJGc z5on3)?1nl1+bjQ@)c@y?>Jas|D_oQaS!g^=Cn+WOTZV$?u0)0QkVd1^PCIF*)Q30l>h zi#Ec;pxIQkG*+jDbDW!f$C-7j^+n0aqKO!Ib`qO^K#$!``zdBxxd}_@4w`@&UL-6*LXf3&&T8O+=2}> z!HsVB`2QuF?`{laJ$gtf^~Ps(0s5bSw=kUYmvQona2_1281 zjoDD-=|adWR9BSVd|d@Gol(oDd&_W%p6`|3TX{Bp*2;o|&-S&*>aUlRvUdXGop7a! z+qk<`w;@a)A1{{!5J(-&A1z?oWdkW8PKZZ9zeVzX)h2+{(>}cYY48$SK<}g-YMdQB zlU8xGz0lq=338W~t~z0$|GCyf9PL97nS1{lC{8j#6wsR8`IxjsX60D-n^T-dkPt!9 zazG_qz#7ypx(Md-5acT2K^}1!59h6=4up*L89@oAx}eZ71C@|W78Lfv;*Wj*_+I7Z z;S~^5#fl)G6n%&6Sixo~WPbW)#y>}@+krB!mSupd$ufbcp2um`EJ>rtp}e@z&G5u! z+00_+jrFu{U~L$^6`ISA$~5hHmSg(~@M=}xG6*HB7ueWSzGL)pl zs;4fL40eNNRT@uK8%04Jy$_4+={VeQEdOLT{YfBaa{qGzP+{4gHY9G@HK$j zIm=6LP%+>aKdohz`zA((&6@km@LcQDPEpM|E*ocH!RR#ioEaUDHH!8Gv$V?tk>c(^ z9>^yDq3;T1*_i+Z*=DRMNxZs6^~LRes2R3RivymzTdkg_ng`77EaL;xjqU9XQ=|^y zbdd*D$XOh=^s=qbkK z2RhuuMdGAqGe+w}d)&!|Px44ep;+;hVFvkcL0@`j!$OVu^)+jp8M*aj>*vdnPS*vT z58bNxdqe72FGmg}^Xl*pqrujC_+5|VKn;AuJXW}MU zQG40=FVKkVV#P5APbRvN?LWlJ3J9Rv4)~?lrdM8H<1WT%%s^?yIDZ1e15ZB=NHfCn z3c-H5$Q%4k5jDw`^3+11GX=2j;+T+fa|49?{ai@KAItFwa2|j1g-Gs;Ni1FZy(27k z`s9!PInwe&dT$^0c)NSPT?8$A%7~BD;5;6E`t9 zn~SkVk*fj!U?#iA#0KWRfr8{|9;1&F4y}qlSJf5xW;X~WD+LU$o90QHCB>TLXEPv{ z=z@?UVpd-8`>XSt0T|m}^OrL~cGKVZ!b@#k2P^!W@b;?q2B2~?RgA8gI<+6xn3aRB zs(hF8hpj7c=*5v!$4e+n7~Bk@=#T+8-KS~gtO7&~zrVwZj9$QM&e@W)tpPyt4Ph+K zw&mXCHS6u)3Z{2sIFfVlyk}GwcOCtisNw9XEYDRs052Oesf)E}kC$UKgc2#P!wzDJ zZg?(&y8=?^BG8cID#`0u>DoX9sBlsy1rFNzo5$NhlR3UJU{*nfku)*5O%{AHY|xdF zL~s-~;TcS*iR8xvid5fux}KcCb0xxIzx?ze=(af8l4L)=)<@#2)_-Wm>w_l|JN$Y^n)7HU#F6E7h<)$aL_V`pZJ1QRgI8wQ3D zd92jJNzJdt=h;rFfuE<1Ka-MnHlwiNe&;pV`)xFj$&_)e%@l9Ux~Mjpu@P@lE~>%p zB2kQa75&pd7F0TKzgC!PD&=R}%|8}g9dzHqK|T7;*%T=2bHx-nETsb%8euI0(ygylUe0s$&h|TXIg)DKD8tI&PWGz^6Y$U31_A z96ZBceKHU|yXMize+<%}KZg5Ny87mb8xl3kQy@`Fa}BN-23&Y`aG&5elbhh(&PJUdjgjr2oQ6^$WMuxUdnBg7PE zPu_6eOo{^#HiR+?8ogzws!@gWw-06;5@dd{jZC3m@5g0ahfEPSkSAW`8mK>;Weu4F&~UTTz6$TKNk8(psBY+Ut=8L0jj zYiuvF#`bvQc2{F1k`36=b$n%lS8gRkf({3(hGlq3>r$;;kx0ah5Ce-PgLuHE0MB(> zx9|rwT7gl9xx2{GR>V00`#F(CO)sb4Aqi2CZ4bXVZK4O&*4QmbuMmdrM>i`dcjZ9U zwGo74)JH#;zo?7Z+mcLaQwOn7W4_hIWB9^&@#Wdex$$;^|CS(rMV%bQ|9u&(T?zqYP^QN*5ulgr%%r5RMdJW0>b(^^P z(7+N>Z-5O!H)p`50EvJwl7`XDr3kIM8(s|!&+S?Wz$YmiQoIb{w{K_CYuUCRdM0ss zj^~;Wg8;?l^;d+BIQ`{>+#*#)CWEObS#V|5T@Ch{kI3qs*k0;u+Dc$9#Y|e0cmaio z`xF|#rU|AV1v}dt#_h=}>*JxAX9%})YpE69tC8+*?)uwYjuJV>pI7tkro&MQ$RuWY z=->;E9dWD=(@Md1L>DO|qZ)w@u5gYws*KPNjGd_X%W=5IA9ymx0A;dR}1>Ur$V zY6N79E7b}*H4_CRPIR5ei_=h_`lKSH4#& zh)Gux!$BGZ`%7j-#_R1^o%+A2F!+VS2H*Rn(|31+e~i4|Hy@t=tG7#qe7I8k`#EHY ztO48W&R@EbclzH9F9}Op!~PE0;`M#5gxHOl2lD&=K*N)@VAu^$hT;DWvgfBm6epa! zy!@t;THLTKTj~$i^4z=K@I&+ig21F7EdTM_b@At_Nqa#oX>Dda_c7HNs{av2jTn^i z+e@LhBM+yh4Dr6+y&YmiX0+#O{=XU3w4)>e`H+z(Co-o|w-2AqveM$grlz=?vgH6? z7KgZZCAv7F|5X#E+tf@{Wmy#R-Y?s%#<1y_rjQTwnC*#m?77hOODk^~YXy)MBwCG3 zb$jiG-|Yf_KJ7D69?~a3)7KDV6&7v=a@d9}`@-s)X%_E)B4E2WPenzmS-5Keg~a`;tqqZqg*r_w0t(r`HhlkWv(e zV{h8{zcG?86=0G#a5D^k(PrUzhvak7Lf3JTB0VRA=HB(x#F>gaa#^)F*Dnn>v_uMu z2&b6iotMJdlYKx!*JpQIuwd}bO@;N|jc=3%g*$pvO|yGWTh;3`Z%Eu&##&D-qz$_TN69WjAmN@XOe!h_9;E8-l=5185I&tFIOl0?d_zeRul*Dmpp zmsnE9EQPOJH`+nX{0$17TG27E`9zS>JTCRq@^%LArz4ItrB%jrb7Nb}H`QjJydA(_ zjB7mAH=7-==`?VMEU&biE?uf-yF#v@8sqviE45q6wI^hG$Z|_z-Gl6G?s9oTMD|z% zS%vNgWF~w@YLiN`2YzpO@yAU@wIa*JO(GkwB{UOCaLrD|^ub|h6k>LJgJGbf_~Vv4 z2vy&)s%MvvmRJ{{un&c!}ta85@+bgFAfeRgXZ==M)=n z+<3gEOU9iIP2G*z>>}SXUk2;Oln-dvaHDXMSFcJw~Lv-VD-c{#1c zbsWz#IQyDs^MfoKWzK7Q>*W-i_SYL-e!_|P_md|<%;F1MMF3J-c;BjH^l$d9bj>Km z{T=;lMz4Klg9M0m#q2Z8vGRQt9@T`N33WoYrL1t1Nde^Q*DW?9Rn@>4p>vGnQ%w4oNFig(`_#=XXLAynpm&QK3>X6 z(Kmfl!ZV5gT;;sH`pFhAyR-InF?ISIr)l;H-`+lDG+s>k=t)Cc;T!yM`D!Qq237qY zK^S-u2z4nJtfh3PPjTvYBDqdkG(hL8{!DzC!@h#ukPsLHsZg%wHQ0n10?wO;0PKKT z>Hes_ENdNZvFEhc1&%)x&~U4kd+m^y->4FpO3rZVOa!OB!&{5ewp}_jNCs^Lu&!)p zJUN2KAqg3S-Qe>8XT!%gBZZxf$~_Jvm~aoMk~_XIXKd4@H!b+d0D6$I?O<6@>V8z< z+lQ+vj}fA#^>pX89FyDq!j8*EJM_(>7C;8O{jTaCd?25f9}Wl^3@JB=o>V;)HPe-8 zP|oArE@#3@*Ab^KG}T?~V$#=Mv+OQ6Yf5kx=qPq^_Cxg;o!4ZQ!F!nGYTe!#@eJMM z4!4+>59M0Y-uM}))|VL>x^67U&|2|Oxnypf5yHb)t@7Xf*_%xhKe(zvyz4@4q z@yDL%1NeX2HPq@pc2Y4zS7rlp;%Uh77sM{1?g>Cy>jaiY-!)aEXnJRxuru8v#a zp+dtxf@JAn*3nb$NxC&tH2?MVY{^-kx+NuBD1P9tA(fH!WR0Tk#aPS?WPv~TW+%9- zHJg**fFkjJ^g9bPHE4Wy!|VJ;_-Vt{j+rRi%-|=FE`o zXgaMo(=)4MS)%@gwO27Jh1@{vZNK^b!MR#0*KOmTyur|NqYMptJ*t`@?`49;6VuS0 zzH_1cOG-&9Lf68m!CrQZ>k7M&L)=XJjRH4UeKvnc`1~qi&q!Y#x}e^TxViE6nz@r- zQMY(aDxPDY1L=>KgMNrOYGoaCrPF{ae+F%-#KuDvOCo@be94c#;M8#4TCxE`+l#l% zwN`t%WUk=`d+>51Ggt0|qL1S~$OdDF@$@t^(it%vvAC367PVt&soNULZib1rN~^4h zNnE6b6;xx=;I$Xb?K4Np*8lHlMf5dEfY+evp)lgtb{wn>kMTAr1#bd*mENhZ)nB~~ zO`SPvRl|i#+4zR#3+BCSc0U-+U{2LKuwbc7@FEH!jv!7vKUR0m1xmy z*(mfh)MVWh)nw(|rF5N0S=~BNCWNaRDpov<482e!SU560LL-XcK79aH*nm05Y7J@# zyCBH6NGb&4e$R7PP(Cir?K$eLwbH(}s(9cT06Q(2UXoz@aG zO|FnU)~7AcXb`A93fNnK2}rKy6-Z2gHf~eT-EnIJ^?Vx2r7H^B4Ii&nOtz;O3ap47 zq<;cvi*~Er73|eZ@lY}9m4#xThx0lGPV4}yYUbR=W$BNPV4{9RN@D%P0POIfJ}Zb$ ze^4{Icj|oK3z6;Bilah{@*>Vjn~O&%d++%~ z#o#n2=D7AF$hg1IUU)UZ9IRQ4Ky{sxI!Lpw&u=}c=m@0xqR-;*LsXTX_f37~&NBmv<>C-5ly***p1Ma-BJ0t^BZe5(LFBpBDHIss}-26#&e zu;cNRQMsB ziN;@E9V?Cr$2MnyKbF9qE9<+J9HcagKyoSO!(W?MF7>T}f#9Q+KHEqH}U zf`>{}n&xiX>d;0svlc?rbUiAxPllP!bhlcS!$OhBq8HMjG$2^C0L8?j9d%CdXxsJi z(EfoL!xF-9T8rb&JYepDx1JQtgc4TweF=IUaCn=KSB2`Nx>fUS4B-n{Q*PNfKWzhw zz~lgHO06d04a;7vCjnC|2ysuz0QHUp!H-35vP++&rHN$nw68Zi=|;nZ1|e^Z0jVf# z6<`VZnBhQY@R%EpPg~_j%SC z<%;zZghXbvH=Jr%+c)PTY)UnJjC<9CE}$jXDZBtR}yW7-_NE?H%5&?(=bOy zjt5P5XP`vO;Tggemw=&`F(GkshU$QDT^oovO4Ocz1k!ESfa}gog?gxIaUS^)a zrtGXvWzMx6@}yahs)Yh^M9UVgVD{(U^PV+GS+qnGAIUO~+4Gi2kz|YMNXC=u7Z4C_PT`WeK(2~ zgm;*}#iq$6c*W1%+u7c7rn;f-n*Tv_k4!ts0Tv@nnxftsJ|#YD!EXi z{cq}>rAur?kXPfv`LDvC5-94SqBdUP2wz6lhd(^qSqoTefo^kwNXTF4p$t|GL=A z3)P_{rIe69E4&PZDChCEa%b-lcZttUg7k%lxSx`;&2bJwrL~n?!%WZOCXA_YEd;8W zjx0^5`O0ku#4yxV*ps-;%9P|bmN{&>L(_HbX-ebJiaSmjMe5%Jg1aqiebo_vS%GQ- zTY~A&k3*JP{rtq+@G@?Q+|KBi6Ot3^8F1OC7x9J}!M{!)yWn9t?ijP88ZGX5R_7BT zgy^e)##xjQsb|2_)iw!2K>x7amGKo8m#b61J>1=%ZMe&|XN$#-jkF4l#*IrvS{lx~ z%%nPq#HpRUl6vjIQPqmmvyO{D!enS|>+&)Om`dn)91Lox3fH!k4?DBvEk_3k-(;}s zUzu=T=nQbyx@qFvZI#=`IE7&&Q{UBmRk95d9jp|-w2!*vS(0wRFp(lZTcJ`y*;1Hw zA?cgjRRw4K2S&@8nzPfs6LGjil4g3!@OO`jU*UJ0{w(2}lud1{2@tw3sm|Iq+XX7) zM&nA)UW=oURngHzRGSa^2AG&gw$jnm)R^OA*PWsE`prDhdvle|Dri`o&86V0dEkHr zde5aQPfZIek?5k;@7~2FNdeS67q4zCO^vM-JVRynp@x6AAjn z5|sk=o&^yk`-Nji{-gJ?czXAYev9ltwMp3xpxZu&LEP9} z;+;PlAL$}Mdxwxztlf>Xtt0l^6t4*N1Zs!W5!{pO=MeR2hLx9 zw!w*6|9zs{wmKR#GJ)9skqR`$i@Itvw*a3zedhF35I)_VaC+X-Hx(!p;RwM=tmCM*K zZ%U#XHISt#<7{YLV;z|`+Tlq9iAk9g-*kCy)R#SU~T_hNH;Cb+lPs}J7P z4&~vmVlP?S9uSGBGIeOfw(Js@yQ^PsEIHWfl|GKB-zm%ANo+*uQpVX7QI&QgwIgY`&D_Bv(kz% z%-y&3)Ph-O;N`0nb+d<3N%ClC0vZShZz`l{kq^*+t^ROMP#7mpY2RY`h>; zAzJ3xt?GK2gCWB#{y(Hcd04xt+zcUpFv(;sJ|3Y1FzoEbKJ)xbTSw(fSzPFe!C+CK zoN!Q?ncYQ2I5%mbz{{+8xqS7`!OJO>qu~mE*BO?N+?*6NNAxuWpYoz;Ps^cP`~B$xxfny#L)QJBEyOLYmy0#G{P&q?m53X6 z^@q?|ksWJmc!5)Th0 z#FQc{jYHR^R{nFnOzOv`C7>-Xt#CamedaMU~=RFqvb0XTXf zE?r55Rw%vSmZEt3YR)Kzd$Hpfc{XsDi0*f%MzsM?tmWwakG7ZhJM}ImFl)0kUFJj` zxnh9lBRTn_1B9trp9|Mt)ELYUc0*uENaZeObz$RG#|<#Lj@=9t$Jv#lCjVw=dO(>3@>oK&qogdREGFO8knn8LLygHT^s_kul=T#zXU!SAb7H zE_WbrPWX$v@ethv3*(DPV}uaV68K{zAwMc2BQ%D)TlpgaWyNvjx2M;8(NoT{9|gHc zMGA$qKZiRl9o(+T&rEs>;;6T{>#5Gs3Xq0<65^R|#QeC}A#6l0v<(1rQw^?Q+9NZF z$L4!ad*T=eVuclYeZ9s*xah!}tOAAzqarpVLm_s(&@}e73NX%5=_o!wvwGe(2Y~ev z+nhfE=^_qn{7jx@#mLQ7q&pa;cd4?bvknNUSU!9{`DtYTQ7{Rx#Lstu)ou#Fc!w{8 z#5W+1SjlK{yDGgU=6c+9(It z_zFxiS)wBAWTMA&CPeO|mxSGJcXI^<{ULavt@diQ# zG4YZw*>%=YgHPAXOz7iC-hmV44V{O|UEd;80dX58Vs$AALyuRXmpH*F!m;KW#3e;O zj3WK$%5w;fazF&woR)2go90L#0{%-SWL0#QZ=At)z@-if*mwHW1Fu__aV7x|y69)k z6(kje!vlS?27NKl!+7XsoMfO8BqyJdyor%9+c0q$3cLPu)59D79qNz1qR)I48KY)Z zy;V@M`kj<8;`VOT&AoYf)r*uo#04!{-_-CW~!A~2r9t59IlMb}Cum~>cyAE5xNXbHX z8nCL?36Z2UA)${ufLb%PF%bzgr;kbbAEs8$Po7k3J~>a2G78ePrmEkxt@h+d0gTcm zH1t6$nxZyx!1Kp@^7%+pF!bCh1o`~|%V?<*nG)~C?Xl5pD5i&!60bj{XH+ag_t3@x@)knk)=A}4koVaPYcz9)!^X8(q$>l1y1^I_Ooc=ey{|hJm`I0OGryqE` z*8Sp5_-Uaqu=?<6r$Upm^0#7UND33>@|}nHkebFGB10vpFNQ3!O?RP*FCqF%$#JGW zI}%f7J8*isGBr?ALO_VEf>hHj*N`R%#z64Eq^j%!hb1F^jNE@)k*M>O_~`xPEO5P5s!=iNJH+`K{w2$}gqkbgv~+o$xyW!%I~lvmpd zPM1sN`!+2W-QUxQ5?dg+bk1UHx`3rKJ)lE83-~KE=>V3A?X~_G?Une zYxDl`O{DmJvSEDJW)gwUCg>WL^pI+XT){JcPy9k&} z_iBjM-{i`Q|9WrxUE=a(Ip>6EI6a^8Ig7ou+$-uzR$W{h#Ja}wGi4)S+Z*d{&t(TK2AsA9%0n@50NBq-TTR+*$^qdkX5XF?R(b(0f zq&*VroDK?MQRoP(=EDhuMjqv6k2kBI*B3iTMmy3t#PafDSiu=aG!sh|DzcUz2sT>} z*+gmRUBFgm)i}g}2ah3y7Z2xg7sXp3T_$4L*q)8%(A$r^Yt9~_qS^fEj3!Rk3+m_f ziCbY;w-gfUd^n#dPP$M;GW_|sVyXAUip-xBxjgaWq+(ek z#%D7zI^(g=)H)e_pYC#a(rgf}g!+zj<1AL?2NPX%+JhVW-#HbQGk^iw^4^zrt7lW8 z6WcG@9KBG*pW^IiZ zHj%vLnp*8lLMuwj`=Du9+>Dh$m{gk{+!2HWeqd6LgvP|-`cUKx8uNza&|(Jok1N&+ zg`>GO4~qvNS2{+Y8dzQ^NKAI@Vi?DA87h6DV-Y~Sui4uLlPqg%&Y-2Y^5~Z%9;N&s zZQ}59iDA6R`(Ow3#dQfSkDmxBc3toP;vTbplsWlB??@`aJ4u6jM){2FnBq9II$m%V z6IA#5Ahq{#G(N94AeTA5MJSiVhaY$Rk#FcaXE!6b^*qwg#HWlP)Uz!YGlkAkCN%Q~ z+}Aai1WoogrZs1FdgPmI5n;^2a*^YT?5P$R{mDrG!tW&+AjTJSH}qW7h(j~|Rx|p1 z2dP7;ZodA3y2o1lwn-K3V~wb#9Dy&V)0pv%ASr(Px~CCT8%rqc`DBN9i2W!7a^cXf z&@HJ08;+wS&di76o_YJ)DrjD!OC?8&4TRW*%Li>0h3@X9++r$yuTL5sO5v727UB5d zC`yMXT%jY5LamUQ;ydQ>l2&l;Yd6k(Mq(!rv78XOAQ8>t6BqgY5YE$a9J;WS+rJm_ z*b^8k%%P<$woh+p(2aoxTZbU+TCembFe{4;@nIH{;^TMe%dL|_lnOncLh+}5c<@d7 zZ1tQ5AVDT9O<|D9be6oWj+XQ66K_kVxT;_$->3w32d@-R?BO@h;+rUSE%7VsGu1;uD!Lja@bN$X z@@kcN%R$Up;lL|nrg6de8jJj#ilc&8hn(f-=JKa-3mci52cmyi`$A(eNNXQd3ZP~| zy1-j;@%O8=Q1sMRD2J^c^EQWFT?fyTX|(G>N1hY|_3k!K);|nDyDrWFl%@hvQTkL& zZb&8(?wo&~q~54F^cO($8+KtbXS1m{^u%?D%;1`sYAXExs%#b&IH74+Hk1UqH2as|tMb(60aSCQzNKY;5mU?9s<2|`0bU;pDNSWq50}SP(*G@5VsyB z(sD7P!Tel-#$!uPRn1WjRo3db2cfnWC<sN=nvUCIx#y z?X}ZTF!(8NFgVY^eH5eZgHxNLjRyw%s9W&1;EcFlzuLelAqnM+*Cta5ZloE(Nz%Fq zexLU!B(D^+L<*Q^Ft+C^Hm&d?9 z{AP*cqVQA!W*~K*=TYe%%V=fGp~sfdU-gu=N|Zql5+?*KfM&kmTNh3NLwqDjBn^D% zd?PSKhDdHFoUu#!_ESensZ;h(EVxY^1PUO{s6**Wj^oaFh59x&+*pui)b%FRfo`A} z=k=a>&oGO`n)&ytst$G3WlQ|R+8J+gZ3B{=TuE`v89A{~aXymES$fUo2!Q}iz5{Tw z5mE_(@(5$t{YQ7JkleZ*OdJD-DOv7+tV7-Mb~7hgVvdR{4HP#9gHC3vPlM{7BvhOB zRx)|q>Thw@Kp;3Q_QCByQ~4^fknHmiXjioEs9bvQc^i<>J%*(B#v;6gmu-}ktxGI# zVkrW|<+u&o->e&4%oz+>uHJ%1MmeBM>_Red=ggur1X+!}j6Q*2BjUWYUBIHSz18kK zVb2_vV5=^cEbVScD|J?in2t(GVUMg#V+?e@5I*$DmJi%(gdI$mBCfPjx`n72efHeJ zlURBOYr?820XuJJcfP7sGW-F`w($?TMCmY?Al=&Ly02 zqnx-+QTbvs&Z|XCO#a=`b47PkTf7%tGO|~`NM8yL=I{^pA90Kbx@XY<-dS5+!$r0c zoBdhbnV6k+lM>NDZbKb=gfnFXmJ#(|y-!p((e)hhI`D?;#s@)Llj`>uy%T$>s*SJN zsNAHbjlj>zq(7;iEzl(U3@$NU`w?3edVLYvfaWm+ZWgjLw{7$sQ%K?iqq(BKoW)!Z z>efG5gCFXgZBt~jBu}z8a10TBEmNM zjq^7Y)KXU>v18~3MslHDI`b;%?L$NqCpfW0v<}T_AttPRz|}6Nyzd!HPxSffAagEt z?ae{+-yu4Ch+KdmJt~3D-=$^GA>*Av`gH3`RL?1#MvQTJO8F%>oXB}<7)~*#k*7U2H>v? z-^%a(o97h21CRFYD)$LwF;9bfq+Zyt?C!tej=my=8#Iq@K$gRE3V21--%IvL?{+Ev zf6a(L2WSLt)^js;&mQD`P6IjE+u1VWUp;0x3~iS__eoM1Ac}J^TH;TlgD?HACh`}H zM+5%S{`n9W^4VuQ@WWbTyQUDMu+u{xlU}Bc6~bSwUALB))_)~~KIfx&#VAtzNh(Oz zVyuC+(u>NUj^E--8I#@Lk%UBWx!@ZSkV(_U3!Q zi0=8F5u+qvPW^iC``t>z1#L%w&yeo8Vgj0VpAnGV0||oa(9NfB{+QAKla{oX1(vNS zjjt9my`CfPJ!VP-U`2l5(r>v7xa*-L;)e8V4}O^lF&CrGW`hpA#*>uh6O^EvC%r@e zc0Zc~5l4OKk+I!hZX!RJW?t+5tfX3CONeNNi1yA#9Oo|=rZ{xuIo$HSLc95dLh6&X zl}!Klvd^D^qX@N`)YoUooGC??^$zz9r`0s5jmQ9RLNbnfgPrF5FR6MdJCUlrF@(J> z5NlVzJO5HMLM;C8Mj{$9n6m1K2u4Q29B!AjVLW#8QA#$n5)`lp4N!~|IoYpwbY^uw zsO#8(78N_GI>*Y&DgJ+F?RPXRjxhGU`;dt%E(d%0R)-T1Q9zw|q8j>QZXK*W^y@WC zO^GhcMFc=o4q&^eZ!RAZ_&X1#Lo%>%qVs0!XklM`rGTuW8pgPnE0b+f>&r8>Jkj@C zVvhYXFUnx6_gtR9ORTRD7z_QM=>FaFo5N1WD%)w#1b;};4J%{1WE(0?xFd9YP22l) zJbwL~LEF9C!gGSx$p2W6|KqvEAHXBuWfSyA?)*El3HGi_{FrdMMn!e8`E*yz}@V!M*eSDF3hb_Q!e^ z)kBt1%93t2{O%bmvWy;|I!Cu(Q~LI|irc@;I&sNoCFJ4;dBInI8L&UEM_~xV@YpBh z;O+_Y5PrCNc%%4dWq@L4v``6>T^RKqR38zj^EsBtS_s| z<|+dusAYeVTTr}5uJOLM;nT14;m>Q8z!tag*KnJ`tRH22T+--tO{aug>((z1|0Qsb zE8~~I8;!s2KgbZmb==h8RhXr+XCTyhBVZHPf2cjKD-ZAGg**8#*YL=MYj`aFXe#VS4WG*t{cEwfp&gWJmP>+-P6?F}+chz7`rU!pPWW#|H4HA|W~m-b z1;2Y_2*Z=D!NAD}7ulh`_Nz17V=1`EW2um_UuMl;yKyuUZe+HHb|FKU4eAwLZ=F|W z6wPao4Pb*!f7-}k58%%Qpt>Fte(Ak)_$svEhM4;zq8$ez+_vp=+h3mf%V$xmZQ)nL z(F=$H+W&lk@Z)J1{*3wW99;0rGY{eH>2lSHU8rmY)&Cjj=N$4hB;WFbA%@6#Mw(FW>a)))-;m=HGnBeu}@{FWb$nia&a#40hwx3+ia}p!4oA zXWb~qYgA~Yx0rPB_1l03Tm{VEA}WK!<-a$7`VjI(r;kLQ{hPr%)Co>;Au=>Cw2(<$ zh6IG$NvFfvF^{k0X6xfG>9=761DNiMgF~*VYwO)RCl$)F;42<{^*MbRB+~;#g50p#% z_wIXk@4iRc?fKtbu2UB7e&&P6-rc(gC4c?4AsyU3+TD=(!6%L$CE$HUzUR_^V81zE z6$eMkdtTFivqQWly?$WU@(qZCbtMq=S1ZT~U4&Mw0Z_@{>(MN)@<;J$-L zUkUG0BRil)!PQD{8ry82>3Zmts59O9`2iTS=tD6jcT}}#8jy!Tcr}ucmewx;CUX}G zBz2we$;(if4Fm~!_3IOtr9)vx)JU-xJ^A{he+bUbC)=BA>ELG4^;!Gnlg0>jAa7nl zdU&=CLtFL7IOscku`UJr%lX5pUkg>lA8^6DU6mk8S{Yx?HknHnu*o?x?)sg(UdHgzI|1Lbm@#!OPAbC&~(2`s3 zn(zSrFgdHC7wHJx|9c)|Rm*;o(24zo{vxC){5v2NOKq;s{{R=16J!bWLJqBUJ7Dzw zURlGwvan;pv~d2#Vcg6>$-sUXry3pH1gG<195`X#a#$Qc3*H-}$s#+O8Hc0vpP7K0h^{YGPT#*3+3rN=tm7X=^v6eE z3)g;5M11R}E*!aBL)>Iv>`B0;|Hea%T%^e?)D7+^h%=1w_YE=L=h-pZD4ut3ReG{ zzC=FSQ zN)-SqPe*ZL9AvR7maX>S;~~b?iPO8zl?6ch{RVZl+xu)B(8W$&Z`5wu3+U1pSdZI*R>4@@ z?B>g(fuM)*#t%7LJi)wtk$`}>ET8Mc`0=O4(@mgW7~6F`7?3}ho8$8CGjlg0PO_8_Ajqn_fczf(xDOVI1aXs(Ew;w zO29WoC?!hyU-Fq^#W0dBOtdHE-^NN{+nHSAn1vNFvsw zEm#Gxs4DaP-Z=26#*xhAJW}o zSSakjSEt1xgV02A8?lJ%U}srCQI^(a4Gd@KNp&-q(9Zm zVf&;$cUJ>Vfym7tAKMUSq=LoaM22&Q`oz!b%aL~>+xP?o3kHczFu?u>0Lu@ki@$?e z9m;czxC5~`64!_js14?fheneXXfIhaaO$(T4*ekI-g2DfCQDkH?Nb9xj`hSPX^yiw zuSPp_&9%F-43$3)cEem@H7#2DFz(7p`}4}fw5S{4aPXc5#bRj!U9X3*x%Nr9HXF;v z%@iRFs(lT3^W|xkjPOJgYiK%mAMY_3a+$f|Khbvd#oehP<+U66{ddaz@vqf$6!Oq> zJ9m`!VFn9UpRZnks}=Jy+ft}EDJxH7*twS!y*h?Ha!N!l;_7>~$-=n+OHoak9Tb~J z{w34QWR}AxzSu^Lfz#>!Y>3o1QZ*s^`pFF)l)1<-`X8kUOICVq>6}j+KF+ZCU%2-; z1lIII>&tjcC9@PTX!{A&Vh_WZF~rN{ggibfgu%}uFyk*TfKXo34`9b23DrEEGB@I( z+2#{SFa`D8Py-8M5=uXWi@d+7uznuoNf?1}lT(wb<}z2ZCv0MAUGRIZ%4i4&Ns{S zLvv5B2@7aSuhF6fUAGGracC8!D@sO@E{F*$qUhlG_vp6rmhWR}6-XZJ27V?R7?WjL zcgHLDxnw{D_dUfS|uUAUCQXj4ce|fuO`Y3ErmG6@5f6kq=mv`slQSZaQgu?&X zrf#W}E|+ny%yn%UZB2B)>?=29FA-M#fa|5!Et~B-kvFz_49@SW5{{rJosKe}tSd;g z*Mg_AG^W9K!K4+qsL%Lo|@F zV{r#!0Zr&}IS;(wYJgR5=Olm)dHjjH?usDz;plO(neMy~TrSw*=LW^VX~fR7 zbkAQP_4bmF5+8(QnR*XRV1~UHTygF3-T`m77I1vR2bvt7)G(J}*ZU}&!*1J znFb+BiTI{SM>Cl#nfh@&eUl4d25HeBYYJQDQ*36OOsEos5lryDtu+P;Y*RUJD0=S=+1cDU#HU1B1(T;2M+NhyI$AMXh8ZW2g! zr$XYDQ@Sx!J&VF*satA)mDYadmxuF>OHG8zN=t0(m#Di$Xy}=8& zgP8iH%vdF*xx~*5A^?JGbqpkZTl+gQ1%j0h;pYOBIIf_u0noBcB&J78BiF1LkadZC=%`?mOIVW$at6i{}}IcFy4=6mPs3C#FxTS&vIQU zv=Kq|BwzdT^-5XRPQYQ@*3&Y<{ciUV1WcI|wt_x6Gnw165jU6-XPm@Bn~`tFGEhiy zT$e~g^&P92n`_QHn>PH6_{$@w9+#tO^IJ%ss!r4e-6Y^yx!|;7QQeY$E5TrY3+c6- zJ>DuG8V_QmB7F$;b)dc6s7A^MY+zlcTMyzlTCP7k-1I2K#1LD?Nj5cM69y&00Ob}lMpf)9i0hTWAMuQ z&D0%;?$hlTbepj@4wNj4cOgA{T3G*jXGaVuW$r6~ol2s5B)efNrFQ#Y|Ajb_{Ix^e zKbGcfR^M|W(;?Mbv|2_Jvf-^xN!~s}we%`U0WilngJU zP10qZ$Qt&1R6>@|_@FHraDqX9|bFU<2h8Hy!2JBuvbZca;>fPoMCE5mO$u> zy6aCH=;EG^3w3Km###Qf8dpjLNg$G<-h;B@4elYjU)~FbByHPudGP<@K(N=GxkOCF zlS_Td?M3??IS~pRODM`(CWq9AX^~oGXS`69vBO9zoDz0T*Fpu$g&uQqOzfzOYlI~)!QlKAsqvMI6 zV$wrJ6T#v^jbfW1N>)Dwn^_??XtK_|RoIn|)oZE~;JOgMWzuK;#iNx(B&08MAbaE0 zglmj;5}t;v3>t24}ovwRT`w~`Hvq?Ap9@>6{*6g6ruT`4BS%G7`UpZhw^ zRHpsX#ax^5@l%Pdde6}LPBIuD$D1tt*yM@%#yCUYXT3}(%BOmM=uGsk+gnCJ?)^vv z((q-q6D!B&D+3C?xkmoEq5Xx6Ja3`^D7xw4Q{7#*-(M^Ik7xgqjXd{?0)0IRhWS%G z*5LnR?>(cUO1A#tQBgre5Cjw@Nm4RMkQ^jPmMlSnWSS;}AUTPO0)pfuS!lY+O^_st zAfb`i3{)OI+djzNrOYH|Ei}!C0 zEyt1SD@5E*y{6Br8^NSNoTZc=YGO9dk)X)SLx7%Jk)K`Re)O4|d#y`ZEmMD-bCFOG z&E4}wZzTAZ0Q%P6Bae>QbEu6gE7dP{9bcVa?vWbc$-O*Np?ezj-ARN2K7XmmWGCWgo`dpTj~JH^Ms`)}AC} zk?OM;+>T*Q*fc0#@RDhD4s7s#&E=IKw-%Gag*_XV!gQ(;TZk@#K1Cl%O z1Z1=kwBH*v=*9Ja=%CO9;%x8H1d=7>{N!9&c!Q|meQ8G*;oa;r6&X$+hLllnwJg`j zo;tZ+YP16jR8Qnx{-ve_-l6lzG2NHU4bwj913~X>gSusEM!RhYatTld?`Ss$UcMq- zc$Bbty^ z8xL#I@25a*9y+pWwTwG(dU8#_3X1$;_5(AZ+Wb_}Evvt#J$1_`(H8)JMye|ph8pKZ zAU$H+9&mQEpc+i=mEfT6#?*pcdVOUL+q~-XZmMae_=&J)v*{|5?l*0a?#vQ@1h9|Z zMWKkp6|7klqu!yhYDp z4AxZ~?=~r8szZ^SrOj~*g|l^d?yH#wY9&U>J8&7h*-YL1&~QVo#qih*C;{s4M|RHo zDT=z+M_c!YHS~oF9A6i&>MDY202k@}R*YB~^`%9j2l20$?u>zTf#VAu={SfD75jZ& zUj9*gtdOB@^lTwRo72CB@xy?=5Er=Ex8;VCsW!Eq%a>*-$x?uw#-s zmU6B775Bo)s)a)M*2SCZ+4=ng>Sfy{q1QD%;*nx1w%0i1BI&uaN9VVYp1cRzaf0Y{ zupM+;&wEz89%KQcxHgCs*H+u}#OGkUZy&izeHV1@S!xXZr=m;%Z z-aLrCGkdtt5^&^I>-;oB@zwfK{&#>+a|I#{=Gi^pg~>nw`&JK`7yPSRBdH2C@*H~N zz@z?miuK1&V?HPJfE)F$mUQXUT$FUwO`=@noIZGbAQjY%KOf<9?R~*|C{hxp?WJZr zQppzL-OoBLFU&m3p+CC?3V~}0aV^trqLK8{tMqE6o)_J7@7fAD9Ts_(86mHUy2^}L zt|YE68yF_UPl8uS?Q4J8^vAi7)h_{rcQd(9XCAP$H&32++y#{+Oikh@!g=Sr-r6@1 z59Qu_>X;pkcU=fja-3aya7FA*6i=S&{OH*=5)cP~X-u1|0$ky!Yg(3Ul?OxZ#a`7+MF0a=HXkO^i99mxuh@$H z8l9x;f`m`XcVaH$uxMn;<^kOHwU8LQsF4pfk+m20;h$3VoMtd9;m+{AG#-?WZbG1d z^Sdu)XO(#+c!x1gtFaeW)ux#|FQBZl!CUyC#;`=W7e2sLDe{)80e?2az^*q!J`+E_ zeIGn14^XEqo~|{>&N4egx$v>k++?6S{9m30sdV>@ZN>7jtH8Q~Xc0 z{6Ek8p)_>7*0d)`au=v<<(yBZ^?C4rU(UMn8wPn|*r&w*O4(#8_Mr)%w2ziY$g z%V2>L;Xuh$?NcDa6zx3M`5AIeJ$ldmWBZfndqN#>0haW; zsAK2Ffv82vgAMKZvI55A-v=aU7t53?&yaDtY#5k{r{vE5C zz!+)@>*ABP2J!X~FhQIq8nj9qxVODTl@X}ncaKy+eb6gK#2!*l`geweQ&b1c73dAB z+%)v?r=(54{qVbgB^xh~&+|GmWJYua=5i~LmR{d6mZJCL;L{o)1Rw`$?Vv6h&W3)x z$NHrSKkd(0PSzAs*9Run?;eC|abxiYPo*EUA-;{)8iLa)6HqoLsFLG+_~Z58nTf=_ zJpuGBX$q61_nef4QNVHP`#?1QU*`>Kwg!TuRqVYC!sCxf0h`3mM`@EEKkwi1^FMFa z@lXDs$*TK>?IsD-mHPI3ll9|&VO9PWc>i=E(RT=_=QH2Q4^57*EJ&QGbZ-A7E&k6M z_~R$>CXiR)$7PzQK^J;KB^L{}%XmM`@lVGHUF`rt5!t)lAtV>k1bDS1_sM*n5e}`kHfPWnIcm8D8`u98X?|b&60mCHHdd3B%uM!YOsfq0M+`oVQx6^}5 zGnNj%hWfqb?`;5RoE`ibXMbfOoPh4?p|MKmR|PP-vVNg~OnRlO!}A2~ zf9VbVgJ1Wj7_+|&k_HU&^ha*4yI|4KK&F4Ev5&d`T_OD2MF4)`3G_r~5SJH^->Njo zV{IVN`LB!nulFl14uCiKU#CsJL*q1p)@UO%E2pKWOMr2S!OWf}U1QF#X z{V(|5-`z2;JXi-36?uvf_Mrf{67qWF6Q=))-e}z@2t502ibuycI1Z*Lh;AI`SDq&= zgl5W!PWTMH*P9TUDa^&_(l3n@Aj(S1BW5e08N!98YrMf?{#UMq>~&UP$B%(%0o?e* zq{Op7ITioqql_T;I<)SEGtfR7_ym0Y-;_Q65|(@+SIiO9-#6$n=pWwcnZL9dorK)r z#92uKpz!JsIjz|vnTr3XJjEZce4X}*S{y7-cpYS)MMHG$&q^^Tt$RR0`~e)D?+NDXAKTbeVV zC+z_t^tN_61)G_Br@>KI^K-`;YwdX0HLTob9`6-w@3W-;r!no0fnXi zxrhKN`2Y5Z{}B?v9`XN?z{hz3X9)l#0f{#(fGOnTbn&EVz3`=fif57#$i9hPgak@C zC!lyX)W5en1?tS70aCKH7N8gJ0#Zd{8LWUsHBAcB{(jXQm~n9f2C>y>O*LS8&}(Iv zf=)Zs5Pr@6K=%wG)ip%>jPXCLE0A&6K#3LHA^Eu4OM`RF2oVM!^e7%eU_#-Q zk0fd}p6)k+)}@gmv%ji!X)(ZaegHUi7#UqO5v&7s>%D(^8sm0hkj8^H_eV+r3|WKH;ZpJ7IwZd&$* z*~0Hxj z34x@wtXlw26N2*q4E3d~q>XV1H3kq3cR>M!0kA2H%-bk*YQ3uyMZI(ZG=&L5w*lx4 zgu$lw*!*U$pn^O^TlX;c15BW4^Xt!vEcf2{_GH=&qLqMqD!IVp^|ye{r2hQU&4=hI z0GUWGep#b~wu#H3*Ur1obrS%@Vqv=y5VG__f3-(hhH{cF0PV3rP+vfT3Wgz=P=>Ya zpo2_SD(w6<+_WuT6WD9gkuq!Gs=rqrw7v>QOy&dLdGfhO05c;CaBi&D-T;bbq5~Ff zm`P8<12&f`YqZ0p%q_A~@bUx-1M{cxWRs*XrNAc^d1Vm5T^T@=dKCbB<-yWz7{*iv zYyU(30kSLRlP9j|U*(0@;vscNN^h) zh?)^6ixxTlAy@${w4`BnfQ5F(?*PzRfp#4`Ab!gP$xCc>?Bp6V!}C)Sujpi(0kJv} zn`blxX!;B$jiFKC&65iuW1A|)8r+g#M*z8=l}r#NI)Q@06<}x5vwDW156+=+<|5663~z^52fI75@A(9 z(qu~r669Dh2)O7L82QCTqOnB7_#9PZRho@EfTtVFiv&i7711Dy?Ep}dlAAK3aqco; zeauz`^Z(;!;LCCH1gUwR3jwlq`t8LG@#Q+dq6GJ!rzMG zo|DLWGNch^)D197v?{_YfonyiXn9cl1(<@$>Fl1SbB=Sf7bpeq0%tX(0wYf^K!}hK zcSY)MMTF$5h$@{eziej2xn_f~t}Y5swId zvQY((oPSLC11I3`l9fk(zIZ4d(r!->HX=*wVXnh`C40U%iyQ_eu5^{fU22VKqd;p| zao>enFe+r97lpI!9L*It%%0Ci%A1{&M}50eufI0^q0f%2#p90o-2@p@Gq-%knjwlT zBrgUTA{KE?Cmn6pSEM?HT1cX!ye@9)ePS64d;F zF!bon?tUv$&SBA8(3TEN3pocJbc>J10tg|fC8Kke#Epf5dM%1ij>D*~N>sj3_5%aFfG0&o{fPmlzd?pk?ULBy{WC6H-ylUoVIU0s#C-Ln19;XHC zBD!zd2%JDzAX)|hX~tPd_ZsAT;G+ltO;N+e1K^pbfpW=uUk})Nt5hYLVY!Ehob+K%$*sf(I@@U|o+BlJ3C7M3BEZ9ocp6&cupgT`< z4myYXng%eU-j@RWYltnW6(wYp-v4P0K<{~@%A8v;cqEK>0ozA05=dagTI++PChFKr zkF6D9J1$`Pg{C+tJG-CxYuf^Skyz2t%x-R`ZR^D2dmK`)|dg0>+Wq_xIKHG!OYu(McObtH9 zeok6Dvj4{{S;-`IUxtfVorU->w= z+vZk%zjM7TJVRn50@GeoMO!`-LI%GmE)L4ir#~(G0ZK#KMc8c{&}hPZiAVP!Ik5n? zY71fF8wUv~1x+z98hMsDonTUQJWvJ~21#WBREYI4Uh0MF7Ws5sOiYa9>jA3&$N7P_a3SU5^VUq>ct@eG@P?guus-o3H6+=6N6N zntLb_xK&UA3^&hUsx@f|5V8~EurlKr_Adr-XpB>`KMb5zBEf6XTM`H8nmI(XRqv-? zY6Yp60g8J3IVTFnZaQuSI{>xUzT;Hs_11Q_()-}Cg{4Zq)y8dPbha_yKhNTrHx3|Y zn8VlRDwW=nN_EM32rW}NuXVQ{ylGe$snO7P0%D#dvl-&kWeh$`twpvY!Kj$)o<0CL z@p(;={Bs1?%l$$lUk#Y3l$PBP-JlQE#O+!4s|8)N?4P>@5T+}u$Revo+RsrU0VTPH z2Jt;?Ozr$vSr_;gBY%(QUBT{~ol%2kOA%tGz{yDp(GbO)tUa=DU;P555M8Mu<;|C{ z=;)PGdIHPtsLwQVGkrzt!R>-Abq-Fgz;#4~y(O!fDTunwcHqG~Z*|b$H}EznRPr_~ zQ*aba)+uy8k+Zh1uEuG|XJOcT6)v`3e7eZ-z_x!AtNMBW9>vYqV(+RBb`{iU?6eSZ zYa0MaVTPFyGhM@ch`6mP*<}&(xV=dsD#ae{;I%esr#HpaSewaGCRdMx*LvUJ?e}tF zz0J`tzA!zn+Ip@~JoV@+jUh++ye4mT4;|do8=N9&B>L(C_NqN)XdsjG)1Txk&tSFk z?q3Am)&BF3H^1Eku=Hqfyp%_19UZjqG%a;4yb3~=DCq-~P(`l;FeuUii9ATP)Lj<` z%#`_Iy{&=RJU&!D#SI8GZJEhGd|mU7M`h1f&f=HUWUV8OTOfftySmQZpZ%MhJz!`(7z7GfG=H z_=O|e5QH~1*VJ>$&&wiN*Gg?0bbs`^@6faMedz+ft=y-Fix4Qq(et4E2BF7uI^W)< z;rtAXJ&IN3V2ahAb;D_-_ozI;0cScbkk@adzt7LjKOfx|S{HAtv@7*K7{aN9vMIF7 zDag9P-_e~1yX|HGoqli8phZ^g`8l=EbON`Usr3kN*C?v6R*&W}*bydDu$M;zlo-9) z^Nh~@X-Dq)7HWpJQ=b_=1Q0fT9M6v{=pTN!H5G+PY1+^9kxvW6n|dZm*pixVF*Owz z62WZ7y))+{QXh!L+&=lJ)PRy!?-3=}2`+-#MQ5CG^rGlPNh=cBDs~F);!tf5Pug2E z`CQ|C1pP^(9&){QBTUxArHdh}-q{0M>T80woy6{szB{iO+;IZbglGU*(F>A2+mhLE2gP_F?P^~?t!0jk0=UXbxNpOvYQ`~1q#j4140b+8wcNzLKsz_yxe1HFNp>jQbzSOq{{ zQEVt5HD8Zj4}FtJHF>mEyuNQ4->>WGccm3bRL(mz; zv9V|CE<4x5Y)MhoY5hR=v?9V!diwV6mcUJo%wS;ynqbSi7Qy*4Y7_LkeHR%HlA$6D zjG;i^OfaFKcfQ{%$&YiPQj4jJ+3az-E$P6ps-|Mu2hU{!Su@_o=tNYo76+o+-Z!Uv zhOM0MXu(!(X-@WmT_gf)d$%`RNFLov*gPQM#ZY0Qpo&dze;Su1KiEg8*Tmw|9Iue? zOl5~=PTH|z%27iVz?=l!)=P%U-St*Q6F+MRbgj&n3zd0~sCq3;bHYp022VGlll!?d zoz4s}^HbcoivAYE>$LmX#Jwocs&;A%xw^WLo@xA2ZMFYR@614`NMnWuQdF(Vp1pV@ zXMup?tB~tzRfbYh{goF^)fgx+)Wv*{7rO%!BnJ%a!nDI>I>Gn6!xZXwlxp`^L-6!p zUgnYn_^jD$>!3CQk$)XmCZyGG=0RL17_`S2+nm8VD|`X_O5FFgaN0Xkh$fvzK)`L? z#^&kJfr=oPQzs0VvqyphNoQbd$*%t9wah)JF7vg2I@OOM#Kl1zgs)g7VdWO0Q(LV#JPm*b8w7|!}_N|tyC-gTINdLtTTmqljwAcxHBkoc6}xa$u4UHfNvJi zf7)Wbd?*M&q;vd_qanB?1e*jWcySM;oY|GtC7&pDZ@pr5XHOS|2%Dh*b7BQG#eVau zsYk`4mcycZV{aOQf9Vy$+MdFQe%z2<%Ie^3vD|(58p2)eY@~-AZ2rb z%C&gFlC)G1E-RlP6FSdZ9Ut2j(AXa7XEGZLmkHjmW@Sien#`*-AGWBr#x2lYqh%E4 zYjyD1un3Wh_=tyWF*b0i(Mg#%LURqHP3@r~V?dD3HSF4SGpiP<5K=Uz`s3BCbHhTE z*76BYNE3S&>RsyaaQ5kmqP`C_9>gmYS5EKq`1`zLD4)?Dxx63@mJa1dP>omY;YGG) zHbT5GP;7_*vU0JDj2lD1OpOU-5ImaA>%~;Z`3YD%>+X;Yf@PmFA;C*hHeC&N!y231 zP^$t~z**upZ@pZ|+>#{fH4s<}(i`)cEIqdwP&_(3+_M8GgphvoV!&4723l-#fW0_} z1^VF})?yR}5G!>z5wrwiiM|41T*U&#Q|3!O!`F+~K^>#O02DQ*K`}Anw!_1l5VvgW z@L70GvD~<9VX0U) z-MuK_QLCd@Sv^ycJJ{9Z(ElFw0DRnx7D+%TW*yCDYx`??XfDi(d*qb!;#XCEuA^b@e8_{NP~A=MGp^b z*8~Dg@H>8h?uQ6gQdK{}goiP)^JSN$4R%RWziI~6l!F*~`K~#OZ;-K6 znw16JXvDQ!2eSO-S?OQy#hj@G0{*7{HoZ+eUm=cD4j@Fveu*~-W3|bIKNKp@I(i46 z@6MSS1$`o=QZCBIJA@+iAg=Jy=3lz0_Vwm$YHB`+|DVC3dkTO@XFNfjSDGblt}1~S zM#iW}scNreHv-Pz{SP%xIqP;{1R)gR;$RuMmy6i%>%g0bl^>K1I@lfng6+e~UZO#| zFyV*HpQ70I>|E5hELQ)gY~S&w^Yl3sIP%~UAPzZ4JbrLA=OU7)y*7U2GX;?4RYIH# zwTspI`HjuOQ@$;3T} zwO-1K3EZ*imLwg~`Tj5NI*492!*}ZDt%fWu*u@CC#v`Ow`iTlV+0w{!ndfc~&R-qw zbed1pOd}lv#4GdM5v!)W7?Z&+_I#~daOAwr?)$F$>-(Q?9o_x3PT2Hiiy}BDFKaMV z860}a4vORs`yST?E|yelx$SkT_F(j)_~9S?0NTL~JaaAZ%mLmfON-ln7ZMLiyzNl0 z@x2-706<+S`!maYoiHpa4ld*p(h8b$L7anJ94_ z1PP`hDUXkywWUhRqk~7^>X1eUDx9gn(#Kg z?`&xvCX?}HW4e{s;S9+pzgd(iK7C~jf!22%2l6A>oCK|O@;%E%$q#+PHidsfRS#jt1btn5a``;^vOdUcyNhIdBh?tPf`ESd@6sc=x|pnm<u-Br8M5tw!5Pb>t5+e@U90JbQk(PVURWy|l4+L3*=Kq) z&fDliTS%eX)eovQqw&9rP6cIB-i8wN-g@SIye3>}xonazRS?yOi)!0*Pv==&e^zyG z+#!x{iNGtbaT4$;qykt!c<{dBd&wNn_b)D2F9A!_(-Eblt)zGM{TfEce$E;y@xHG3` ztuue21tVIzs)s#{oL;z68^^JQi+rnE*$OwP#9O}JAU7qdrn z7xL!1eARjb_wc4YE_c;_vsP9~c&BUNF{_|n2oIgqmz1r;2#O|`-+xSyL~AGX=^)ue zLG1#LKZJ}e$md-9=hibQ2tV}mJ%`evm(<(3BGFkbBv(OVx}+YUZFm9B0#UaX z>>8FATt9;YpxjD*g=v4umg2iwl?UPHHpE_&WOKy-raR4jx^2%;YdM@LWGcdm9Mv>K z8C8-Uiw;!4dkDDsv6*4O`L10|-g;^+iffk1AcnU(T;bO+#LLJ`c z7?#M_PJvA&@dJ*swU1Gj?-Ps07M3EorPXe^!dRq`kG1}uEi+m&e27)e8M1uSzgeO- zcaqTvu=B|{Mz6#87jH4@CMmI0&ZYXHd+mJK0S|}iN>?_YA6oH>T1(0xVVr%ER%sVf z8h@q*XbyoRK{9G&5N8hXU>`8Y+@vSW^r;IxX>n8{!JVPN5JGt{+noHqWcKW_))`x@ zc1*?3=&65h1OQPpajB#c;Ktzm+EK4-BQL4VyH7Uw9SuoOT4u$M1> z*JkTZfbMDLN8|(uc`xOJ^y=PY3CI81HpT-ojSl*P4;!SkCF zAjmBRrM}AQ&9n^VD18-yu!fd0`DNo#0e`qp!lIN5%a$0%iVy|W9tM5xb7RoJPaJ^hfRare>SLh^l$J08)o5IE;XO*OLuWdkCY$4g_ z8tKT$6=9{BgZ*rW?lr;;dB$l4Va5+0{9Gq)BiQnBaf$)3$jIQq%#;5WL;sjo@u`z1 z$o2J#7pr*Yt5ws~T-wf6^j8e%PAQn?la*2GIa1yBlm`cT&eq=8S1;8NrB>lecZ|5) zuL(I|z{2z{UDcIp+_+jc6`tO#yV*L|+WY2WX%AACkV=Hjis~*cVV2SrRrOh3EOq0|TuZ*lF3PQu06$pXf=C0Yeo7}q&{o?6WhA&g*Cx7|hPwe+U@B`wXsLWJp{p06(1OG%I%7eWV3%~fpZ=3}eOuxBHb^LYV&%r=6%AcP9#j7oWYYnQj zk}*>fx_ngyOyc3-+t{F^{aG=f-DArKyoE_aBIfoVJQ_BGgxwaa)-b&Q4yWke{WuLs z_>6+0=0nJU*ky5`M|&3Z*Gq(!tGb>LQriNc8)}1jruh(@QTqw_hl~IdvJi4E&_-)^ zUFO;bI39Sr$W{qxU29i;wT{tVIak_ol7}C1so-<|dQT7dClB5@ol9^6SHpz;ZG%NB zXePfma2r4Eadfby)PB`x`vbKjRwvnGNnj5k3T!?Q@R!th?M0i8f*L6tRL+VP0luae z@F*iXOEl<9-%~ysG6q&KDR5w+!De=5`uH9B%My7l4rT!_{OQT#Ih=-`PL(iA(jdSq zSrJDA!y1oGfD}3Q4F`~dGUE?V`VBl5&43`FN3l{L@S0f!KNf=n_}+d(k}-jK0WiuK z0L@3}W{O3MSMurwQRn~Bd>zCDi9RTU*GA#>G^DwohN_u|`6`uAY%lDEuT3xL@k zb_5_~B!v`XIaBQuV*((PV}R4Kh%tDagG3KlGsKlHRW1yy&vgMOzO6Fg%9ozO!$vv> z{L)I4`1(HrD}#r?)5%03PNL%HOPB$?sahxL(~jQ-ad+^pv2e7@Pcsr;BvyFf)W+Kj z1iY7D8o!bzD+Ji40aZMug?0kEy8}QIX_WPru3C9kkPzM{%}IBqkntynsXli=RH=$4;rd7U%z4J#R5M><**`Q_YK@ z9=MGM^#UH}C7do@(bV0y745Oc z#ab`w+)Z|D2c9JyXJWqEet9FIxnddL9ltt(ph=jZ4Y6MYm?xgnMxcLZ1B!1iEc)}7 zr<1oXqIeMkcYpeF<;sIqqv@c2`8d)(1+U@2*RunXcaxpz zEkQ)pl|<+B^b?wRhQhU$91n<4bJ=P76i78ASiZn8WkPKPQNE0KJc1G zG~Oak9|mj7b-hDY?|UCn>Ds&Hnl*#Eskp@sS?W@NyprO`7L+LmwyD+ITB!{*GcJYb z*lKs-FnhKzq}o+Ju0tYJkBmw~a~3XVbi(!Sb(cUG!IjUEIaLu>9`bPY{)iLf6na5u z)1!2oKW&T0i|`&Z2y>Yc*Qt)D_w`FKzeTBemNM_Y6@^sq%p|yO^k@&BW62A@-^-NZ z`A}^CdjP>@&bpaclhDTw$zGc}UA2&Sm7w!iqYqCAic5QSTeS4xrH@zEGr7if<2_$v zfqSA2=%2i)-d}BDBqS+}scJ=|gqEc-aLPftBK`lDb9 z>=mZfyWZd5pJi;Pt*n_071}Lo(15eMzB~=L%SAFxUs2Y>Vu_dlq*U0bmH_&_xW0VsHjxEJXRV4>l(AKhyG4!sWrk=kALsevz8H4v$ zNfCio6X7Kr@!EPYO3h0(g)FdWPu}R1YI(Kw3bNmcp<51y9JDMGo~~rZ7|ibl4MzFH`Yygu?RZ(cu=sK;eD&=I`JoUU$LsQryr1I|{r@ ziN6%cefO~3YMxv6IeJIlpNo3V5B3|OuWtf8eR5-(yyVpL-3E9jRVMXJq+DKk70J20 z+DZZzOAE5J0wKp`NsbN03wf-r(q=InX&XtuBnr z%4c4jP4Tgnb6V#g^_&z*cwRa)KqQ&K!{_s)d_{#zL#NUQvHTg9JM($5Y_!IJGHtFz zdKAdQEiETZpjHr~EJ72L59WBXgZ7>-=~>$iTWHF34k0TH7W3T=j%MOv>^_Da!sQsj z1t}%2XA{#(C4JTR*WS(;wxuwu&K>OKsgI}a&pDV7_lJZu8s?P0)YS1fGdEFM6a+IRDaX?uh1=uk(c-TIu>Qd>ub z_?FhQ6TG6`WgFj!X2Sc;6OaZKyJ>ut2b+TjUaEYom0!~i1st-ITyx7x&7G2iBuB}v zZs_%2TB>a;%#kn~!rKtqP#N~oO!;mzlrBs7?rU1eIl0^p>iVj5kMepo1-5B17sBn~ zW$)RrbSmunBnez1fA4MbaI}v3m9A+J@IiS%Pa@B35L07j%Co- z!GcYWkE7(vLkl2ws({&#*0U`H;M&nfY<+Jnu*u~h>cV3ql6y-=&=3f-T6?usMi+X! zo+G~50Ix7hIcdJ(60_ab@T09XTuPvcxnXcWO?sE62EV>k4K1n!OCbADav3SD(Vr#l zjrXMlmuV%QtZt&7m+iXzZQ71b)Vrm5^1)J0icY!TNCIKHCAQ5ANoY4sQR3@zV{}(F zjsUz`543lx`)HYKB8=|vDa2+<-^a_*$RgWbDxbVy%u1nU;BICYhI5Lz{_eH3hQVTt zV9S{c*DO2GofCyNuO1YsZiE+bR^HeJsm1nYzB^v>A)OOBkF8c7y+2LP z^YXzne$z;l7udv-5zX4}swm*`iEfvb3?-s>d-Yvm$122VgDXjW@9hDDB5pnXGZND^ zkmD||5lrWV79AuyVOAsq!yl=+S7wP4lWj75)u8UWuPXLxOvB|lM#8U)`e<8ruPfzn z3jVrIre6FsS5#vYr>HZ#QuQSxrNL(7TTZN-*=(!Ta!EWU-&Z))gO)m)iqTiM2cqydGlvd!RtG+VR$V=Oi z8;?+6UzybMlC7V0`?8M^kXnlm)R2?#TB0 zbbXmzl^~;XUnvf}B{3C&&bRi)3hx2YLh_FnQ_r_e6GyyFa#q2kEs-nSx%Dx`$68cP z$eOCh?aQ_F!nXqQYgwPtXVxt5CER$qt7qx)%-LmB%crOOJp7IGyh>%5r8553E6#lR z8>l@C4SEz8E1?ERvR+Vs$VCk3%#mIYm?9EW+2dFeo31_FUD&HokZg1dC(@ZM=J$l6Xbt0RD8?aRqdo^OE+8ntybMhH(PYlml#HiKMHZUOdOr|(xU2Itn z(((N@Unw;TEQKKjcD#me+&OlwSQLLQPQLxj{NT8|+-6SLU2%_ycPX-WC=$}<>I;)k z_}eR)C`$^z#2Y?ae}L3Ikf`Tv+oNbXCsBghVO&4aW9VIH0!Js#q!7TYAO z2rJG8Egnhw$W*83*}$rLi<~v@0gJweATMop8SV7U_Rg}n0G>RE9whTEOg}xMi=RBP zb@t}T-+%We7bmeUHFsL|KHo{@l6$jN={*AN8ZNdMOE7HDdn-;)Gv0C_&+xI0HytDY&MDdS`NhLme`I{vG@m}p{ zXvfRg=iRU?yQkkIbI^}>w9i`_)vRTYzw4s>bh=&0PnWjlf}}uhq~1N0w)TTC^k?AY zuARL~Y8tj1Js{5~c;e1@{KRd)m|DiAjD(V)TYjKb7Ry zbc}*=HJ|GUd_M6j665|1U-iI3%^d>sg&}H)7kwz0l%2;~y8A+()XAX{c|?i#(v-cV zTt9VX%oP`|%zo5z$xYv#1ZPYgqqG`(kW)N&C6 z@p~uYW@Fq%S6u~8sNSge$<5C}uIH?f@4hCn?Gf<4_FI9x>PS*EC^oSHYgywllN zl`?%rtKDSYLwQQ~M#Hn-`NXqoM#o%p@h zbx#u{47tzW=>%246Za?5$h_;tW;4?;?k5mVRDr~6y(eEyRI*WA)qga|y9*;8lDZRN z&juIOy@9RIA!AO8DYe`@ynj6cR9es&cg5UCY$5h+NVskuISE@Q)3c+J0;+8-`SKYw z?}nM>xFGv=5~5jngW)5C$a9|cX^Dec2<4C`R+9JK9wpmR&_~k9z^Bo^d0h^|IDYMY zg(mnhCE?7ecOob;Ey-mf%uVjVGJRj5)K1N780~rZSa&>L>Y7BKIwywmHiwO<*lbdU z{Qx{{zFkg{n6UYS`){k*(mE_AiXY!BRm|&o4FW0snRU>lNXB?#U}= za$hbH$VqbJ>6C<1+Fk9><9j-=fDT^Yv9@Upn7HZvT3gMXSTN@ZVY~8XWunSp=a3k_ zS10$d(k(nzbw`>ULCU05J>f1>%g^YF=fy9FOLsv+|1znX)LJN2_Tt)m)I+B?A0;M; z$@2pXsh-k(cK@Pqk#AwJBz! z-YTCgqS-#Tb#b<%aHRagbU9M#z^m6iW-XiES3Qndh*=kZW#dI^4TFOxZreqaToXk~ z+3WnZl9`r{YgY)2Tj^h79b3tD<~jE2V-*Jvi9_c`h?1NbgdGjg^NIY@qu7A2y3;rp zqt=?U^OGM8csRx4uP44aBquaxZ9x+y28)hTtqAC18x*%*es$6(lF7V?va(ehjkqU5 z2lY4JSLvEL6@JUc>hL3AXi^n0_DJIjj8$c zc+nq*B4@7fvFmqvrn_nKN8cE;)pG#+_ zEJbpcm|r)Y@ihXX$8=W^;y}$d0{yx1NOUxePg{nZAB`G(!qF5cdp5(Gy#r z7uOxvB^xw&JjY-+s47btW%<0S-qJx;Kixl!HKg>}>GDX4*iCHhAureSz$eUx{%?rt z)AYM1mCJj(v&lw!$R{S4Gx0+rw@wL>#>%^y}_eO#F^J4oj$lK-Q=BMzKtpo-nPep#4wsXRa}#*!r7{l|uO;=1 znbZpXwHJZD9Yj*x^I~$euSpm8B=U&KQ9-*6r>$4}-d2d=zKLjlmW=Ntr6$I~Kl!CL z?IiEB6#9>^zOn}9dGR9bk$wADzRB%fsS=Y3f)l^V5hH``xm{VE%G&OzAqpcopbE4C zl{@Ek=V}x85n|Y|9%($Cz5=o2&32kEA)`n2v2qk)sVPz1gt;#=!ZQz>vkUhu)C9&8 ztHyG?>Iw0As1=%+Q(epRWu%LVze~-2=3MF=O8B5p!(raw-~45pFioIMdVW-J7HzF~ ziC1Q=d6o5&RDFjJG<{afp`8SY}QRa<*AEc)KsE4eFl=Ucq_>Y-3T`#LFWIe50PWKH=-q{`0^W=9vr z%QwZyuKnIKhytw=1o78%m(r#0xo3X4o==ugDxMqEK8803Y7lF)&IS)lGJGntd{pyq zIE?E?d)k(dpGmRnJn3w5qw^e=*-Vd!Uy59(n6;M-fk01P+IqXqM4_E_o-G=d${kp6 zgUOD<>W*-}Yqjzm8otr0gx`C}*I_<^=Ib4WrCg?32xcBd&S&zx_C0LynkY!|PJPmSG zlT*_-DLT7Z;@Q3R6~CqJsE&ko`=sL^bgWAlAsUX=ocCuErZcGSB+m-U%T-( zjIYPg>tmCTu>qSa^)soBVE$p}NRG0?Jx8MU)A@$RwwNMK%R&!LqH0T6A4nJ&KiB*b zEGgdk$Sj6YpLK|D)(rPmT13J58N#t-&x{ck&mK7|4N5-=p9aSSf8c4)X4*BTZenOA zT^7?#cwIv!DaKdH>B%Rf&T5W6&D=)bHj-xTqln}?A8uu%WUIetf(ioz9aDX^n6BXi zl&jCZxJ+DZ+WFUvIPMuF>0hg9<;tTs1!$(Ik68JZSq!2r`e^jkDeJFY-u0>a_NDRp zMqI{h=cnYx*FJ^rOa^?TQ0M2biNXB*0G|KFKZ>-P^kZa9@Uc2SP1aXw%o25Q6ARnH zdrtVeGi0CJo{_e?KjUV4xYAH@I{)Lt;k1$sU?r5e-30j8Gm|zoJ2NvldVJpdZh%K@ z0$_7wFK4Q3;s%LZ6PaFqECL;Z6E6go^=5F+LCpq6>6}OG3Y1B*7u%cIw&U~?kuiqb zMW6s5OVSWWvVT8C)N9|0shg{e4IxnID`3Y+n;TTQ1X#&vP}#YJ;ddRwp_!@jk6YCT zb$tq{9=cQ#uCKBLGgbF>K$)WrPGE^q2ovCnsA#A;V?`m9diW(%f!qUF&0Lkj7j~x2 zNx6M7sUqm!0^lAB(E6(!LG0r-dNW&?SjsYhO6LJSIUiQvDUKv>=eDf^kJHHhe&HJW zp}cxtjV(+^4>Ta)ey7Pe%QO)xU41^bz8)`Ao<*#|r{zfxHr{gT-pqxanu$t_#U0Qg*mU)U(U zYdv=GERUHGkIu;oUzYRgAH>{)#n}C40t(xY@bGy4vb^WEa|bNscdppUvf23#ih{Q! zlF)uC;=RqJyn?IieG(ce13CfD-UX~!HQEO#k=sb6sVy=vs8!i z)^AadYoP@}2V3;`fc%~j`IwOk-mStB-qF`pjmixy*0Q3&YEX4dm@+?k2|z$o4%&yb7pZ5Dh+nBK5XY6V_3e4 zH>F<{tRt`KBTqh{^?gy>T zQo+(tIPl~u7D%L5VnUV~dHB*OVqOjzre&Unq7XB0RHoulyS^;hDA6wGkC1xy68uLV z8_C?&!G0UmB~!6gn#-qee%hF`cwlTOT5;*85X%tPwT_`NF5VZ+;_U`nu^2w&+@u@n zMw`dHtmeXYUGI@=x)lOmXCA$l#A5%k!eX7LRK=$%FQ$(b(tcZ6F)X^qlfHP58~cM| zwNi>{zF0%G>Wv<@U!8e4?P~9nR_olhJHp0ODR@R*thmfoVR5%fQp{Ea?PhCHd*_C> zpwW=;bO+yM%yFF03%w<~qV$wHDhP8j#9o?r<0ZQBX4?Xk$?-K>_vnk3c`12L&FQ}> zm!QUpgeDfgkHhL?^=&@y^{NGC_qkCwuYOcW@oqujsvBh%Vi#l#WaYShTz^hxXpOb|?U?c#6^a@D-m7JHPsM?HI$h z#S|^)9&A~*Y25)t!s}mhg|-5bjQy1mZbJLO?G1b2NThqb;BKO06u9e z6KWm3R@`|S77=byn_jIAkHGn%nOj`rnE-4U}yu$m!)LfCrphm))vxEWuk; zx%I>u4L!#ro@jOTtF5^AgE=^{49b|6=hP?0qb|SPr+q=t%BwOr$?<)1CH{Ja;G2&S zC#eWizbZ{dDl!^nr+Q@|kFL$b;MJ86RLNgu?o!Mc`eq@4k(kHa#$4`x^v9kAvXh3WhYEQo?;>53Ws)H=I+ z=lMo|$=IY_Z+KO~&@#etxA6zZE*z^6P#jbAE3JH-dZgyPXkD`w7bnd!=NavjDaXr2M!oC8f`l)RNv7NKi@)_au9Kt<9ZGr=zvDl%C9 z?>(o&mqnH5zR%S2U*%BVyxCYa9qTTa+4!sb7c`>6%G{v}klGp+4Cdq$&i z`l9AD^-74(cKDU2InwwicF%$(y~i}`K;)6AWfALlBhgf{(LUrIukejn$A#6O&-N(F zG%^%!?-Osv2M3#;*SebDPDAv|epXy2t*;%YtncB=>AXc9@P731C*G~D&SKV)8l&Y` zU@EZb1$Byf+Qha-y%Q>cGdHrMJg+VpSSORrrzOPZ-jH*txV2* zj>JQBaq?nQMRw5XD-=d^dQ`UqL&FZDJPW3_8BYZlpx$ic=9~J1#S;3cJM^yP_DXpP z{bbH*fuaGAm+$;9)WilH@E@@Eb89q;qq6f>Wl!#p{@$L+nA$NVO5 zemo^(DXbf3U~i?XCTMkE_t52c9p;(WeY1=h=+aeQpRJ@2w3pO3Xybow59Uj7?3%6c zSLGl=Xx-=-PMlxw)ibWV`VV=6rH<3M-cydAwB;x!ioq*ru@D{Ile`i}by59WhUIgmZ8hVJWlsd~?)?tvK`=502ez zT7A7%nE)L;sZ`e+n+);@Rz>C5u2Z`+=b*pq)&AncTU*Oz&Q=!LY!6zn*p2It$Q$0u zl)r(rnpHH-)}<$;zHlu~CX80Ied2!xX7(+C(MK=iw^Vwh-lYyy8%~lodIFHiu;k>N zaUwe6#5?_Z+4sWBCh_EU?s(R&l7IFtON?<5HG6dZrDfn4d6b&-$H`aX@Ood;W*`KLsMplu7Z) zS(A|meoJ!h-8>SR?tAuYW?!kPk^TXG>&z#IFMN%eZum=^)*Xv8|_h?wrXJ zd22t}`910~zuiErEV)z8d4OfqVI#9*E;>ihE_4EwPOLY|0m#8~mqMAN;zYQy>JZ(oJf`)#@Zzjd3 z^S?lZ`jlG(Dl~%D`ZaAkT{;i5ab`XC+S$CvXe*gMy9ctn-BK?=NsiViwog$VtaiSM zy$&H0@+U^<^Xf2C=2q-wcYJgoh>TX2cawx85RycxxvwYEK=t)HueiUa4^{V?)}s?r zyHytLeeoj_rLT4Gi1M60!z0hfY0nZ-=RH{G9bSk1&Ko5-EIrlx+q9v5mvZ|C&tkR$ zy>>;nYzZjj4S69AsDhzaBlOFUSZY3D)y|A^RV}mTrmVeRiyMD0YNxMLM^0almFEi{ zrMEz!TFZTME`0#2u=DjBp`WhDT4(O1zx64KDlIUf)15YZWoxYX%BsUl7pwlhWf7jO z(>zyZH(kNPPY?5|@(CAjG{m)DSMEAndup&ckPupvS3pZ0K3{(G3T=&g*$e-dRBtjx zZGB5iw~WT~HP7_=U>25lTwkDREkyyKDAcfkA{v=}ewq zWJY3DEg(}S-iZs|9`SrC3b*T8J~Xt>aRH5gL|$-2V#=|>idxDTqd06X!R)p%TUrOj zu&TR7{o0`ksGUAlJ^5ivfNY%Ov78;1y`*B>{HP5A_RbKcm7{4aXmz5a;W{oXvK19q z9rdFD3IGm`s`#ldQZJT>jf8*ch!1wYgO?P%6w%k3D7@`3)7ztI;9}p$V$lah!+J&W zW+&`O-a6$r-wL^Hp^+Qe`OSn!RH!FIq!FEZDzk#d>p?`K@G|-!+SO-&+s*lCkE-wi z^rJAwsPM`@En$Ok$+wbkHardu8y(Lb1@z9@ypjy5VR_e)3jOc3Tj!Mx>^-hGej_)& zx=HXJ+&JHSJgz_)6Te#FZFgW;M8u7r>=QZ};ZTO>Y02>UgyS_Exc7k@^v5oCY= zI@9)0#6)EDY22#Xp!HIh>GG0UG1|YiJKj!}&7{!4E6({TZAQd&9yHLMv&$pUH}zeP zI$9sapkX%4TqhMj+@`9!QRGc(GB<;Jl}kE+}FHB<9#v(@pk2sf~-r?f9+BnPBq z@aIptZO+RrIPG|ok0+T*t>ZyPyIt&B;F@Pu!Sq9F#Tx&zNyfL8CPggZ@`d5O=XD<2>EhaG z)5ap`mvT>MREoE0dA$1m?()R-ckCGEkk|A*IY*h~IgQRjM-4lD);cWTtS=5+Nx@;B zOdk^I>F9E(#KG0EI#{(8#MX}^Sk1pZLjHo&K^JU?O_F##J7YPCE|({o@3JiEC|6Bh z2>nMoPKOo`9R1q6MRnG~l>uLulH6Y@aSCSMZ50}_Y56uPwwyMBKTywWgUc%XXtop& z25IJVe-FdY0+!&6H+9=nn)k_WVly&)Su%mUu8eWAy?lIC#)HJ$mGglH3%fAaSBq4Y z8ZJFrG&vI|{WWGfPW|T2CJx6(byCaSb|b}O;iS(DQ=WFy^_V3evlB}PP$kj zx7rAZyb+1luOaB-B{WQxo_xGsu)+6LEG<`sdQ-R^%Z(Bn4LfyE+RxMYD1PD=Z!-|- z<)V4OqDWyRQ%TVeUfhNGC)3%0fH1kaV_<5oz-)i+J zSMo8o?lVefrOrs zt{ZIZx?}(wD%4Nil-0Q%cK(K4=7`ANo-a`y4g2PG7$ar36l49!JC2k^Y<|YZE;i0O zsWwN&iB)5^r|Sj4Zh0M#J^agyVKpNeD^)LK3EG8E=urc^=P~Q{*4n1g_QeY3Yp!=2 z@7A1BJe@MfS>B#FQn9igc+O+{A7Q0ToN%fRiC(yg_$->55!m?yiuZyB6!|)qp`+ml z_L1W0z`!%3f|@7p#UH-ziJ+gzetK%v%=X%V?}~@{8K#^-PD&x2qiVFxc<#(7M;}Fe z)_{XKyXNY*cUL0VGEUO?nj31{TaCrxt_MmdpJ}|BrOTpmqwilPP*E8)f??|l~RN!GQxZ{&>?2VR*m=bTKJ_hTwxOZ;&L<%Al$bY(lKNsYwq7|-dr^>Zvuvc8R$+ed z=uT|eGsYEk>kIfYtZj4VDN!LMW$|r!q4q0h{)^r4L1i zl48q^C zAA(cuOUj35iY8lOWkAJlK{_zVQsTVKm`MSBcWcQ9@c81_&;BN5&Utre-mVROgHxdsHp)twcVBB;TNfaXQ=!}t z;#UrP<$3;}1GayC8vIgP0diV$0(KDNadb4&`RxaoLiqm2RHnb4{5|vQNNx`ay*e?@ z5eLPQ(?gz}DEJ?ig(NQhZ96PNz}(ish)p7*VKc(Kgm2~wleSlA6Zx`api2U zVIjr>QC@#r-v96@9JL%ecp9&ytU(N5ig7%)L0$H9=lwVG{7=uj)3brd-5aODiD)%A zB$Z5eW_K&=e>QbIa@a5>`>(kn%!NX7Vko2&uMj!yAE^9qnoy#wk$HX zx%v1%YG3~_|0|$^p zRY{;qMIOsVr_RT__gF%a#)S`~>`i&ef(V_o+%?ZewHR`=Gch)0BQeO~YG39yff;+f% zLRnsi?|iNc#Jx`tK8nmQZV?23=r-;m;J+sUQEL*}v~iA(UY3n^_vZci3R<-sB$RIN zH1h0Ul-Z%Ndj!X)by*b=hALnM;F1P1;ZaE+8`SN)9D2&tzMaUo=8yy`ecgRUh&;$K zf??VwWui>|%H0N-)fyb1$uF`b+6wckQN^@9Iq3$%|iRbi+Zru{G8 zfcbuva!+mC)NUD$C8Hifm`{vIUQRzvQ>zTZMNONTCZiXg~j6mGs!o+(Q~}oq-h$DiBkm zu)t)f4Ahg`2(@(1z*wu+-Km1@s~N|TX}L?c+s268J{k6RvGAdwFj2F@Emy+y}+aA;VIdEd!+NaPGa26bPaC5dpmI7r9t@VO!)2OpA@`>xG8w z1d8FgzsvBUSn2lBt<d%mgd_IKS;l;Rv4u~Kz zt3<{sv&}BOHCkqZOpBC6yBgd6;EkT-@0a}Mn+@8pYu<x3|3S{1)MH4@k6Uo{@DZ znWl#S>n3+EMi{dr^s@lM7HQ!y*2X!jc7}!pfn?ADwCRzi_^`LF&AL5Hc+i z_tZfNbm29D*qxW`Y$9^67`sfvsv9PRlY%SMkp$H50T<&VQ^{;l>u*cKF9`@ z;;8agWD~8*qU}slo|`&!1JSE$Qgl5(4qS&H?)!;} zz`f2hxsa$V8sa$t-}BmGpURXoR^8{fFQ=`n7K}vHA}|%E3lEYoIo~g}Lj^C#;C+3p zV9^wTrHpA$)pq#C6?O{>n=)4PlkP}7M6&*70z_JIX@kG02+?JN1n3$NPqylXvG?W6 z35Oke?6tPfd9P0wHA1t2!FR0xo{p@cZNt3tvL;U8s4o^29XF`4w`=+I(Td+AEHCGC zA0V0>xk$eFR35tsaa=p%q5cNcKX)@5(nATV;#0z`m{=%cT(X2uN#?}N73!~8mn>=$ z44*Nd-EK}(@p#vq{Co9&>qrJ4l23>@$G0guxFjjwfY;ptpgQ!-`;kTl(cn%Lg~IQF z6)Y|b>Jw1@x?g<%-_L~8&Vg3WvZUp5F(0KAa~xaF{5K*|?DPhJydcLc0QuLg@tckaftrBZ4h%2{AY~Mc?m3 zhP3b}${1~sF$oqpASD6Xn~b@gDFH8U*Tv=TmK(1`v6~U-^jSAy$eZ89fN#_+=wQ-= zVRE9ds6?)%S=TLy9(F%mvlYR zizjFI?rh7BpzR{t5dhzm$U7F~!XWX5YwUi49TGu4HfyE-;@~Jzn8xuU5);Lxr{U84 zykm|{C#e39HRUk&VrF0<`^C2wbl?JSd zsJ2d&ieRv&p@58jTe3ePERMVH`af zNKf2*8x=ie>WpE99ByaobZ-&_HZMIzn}qGhujG|x`dV#wE*vYzBnnu6d0IzX`uoBX zgIX3S(?hTBsKSH<~!dRy>N(>UN*o(o6VdJGj#xFKZWFtb(u+km}_u=z9e|~d#n)y z$O^I!3G%Hczc&H)BgvkmSa2h8Z}x!>=on)H$%e5PVZdIYY>869yzFE**K4~8Hra31 zOZUIL%#s;tRrn1gkNEbJqrLUpUt+frCd{EH36NOw95V?0*uOS`zkp6NWEOY08X_^# z*QleDpP(eltzR2trh? zTf1sJmqNFn5?^SNn|f^;lL>_@oWWmz?1qz<++nc_XWIkh!AKE@8H1>hiTl+~qQDL&(qXf>!oDT{KkRV&%$ch+*eO9-+ zJ$`CnPw4s5j{FhKl}6E;P0UbO6rCy;DdG9K-*}BZs&{cQ&;6Hw+VX+TXf^@8BDivq z3Ln{08~v19GY5(zJLbNAc0iP>Z=_kzJ2-V(%1*Jvp3MQC)g_43LUAuo*lv*94-F`NL1oEoG?p$@VX zHZQ{QkLmRAmM<3;aKlNfhJ)rj`zZ7BFJ|n4s?(H&c zevWBkZ$-Y>tSx63lXcv9mUIpEs1E?xD%*23plHN%ecXe{zIn1%PaBk@y00}kpCFnh zaj!Lcx5ABL`UZ9*3eFWov2B`z!;<@mN%)=&NI#?1nvxX&0+)p4fOKlP>Uk^~}bg?(Rphr_i7e?>74RZap{bjj^~ z=&nm7`3NKyb2I6i9_S1x#1=Sn9ze`Z3E128-QE#J3hb*UU zd2eoP?=06@blv(2}DYE z)OT_&+}p34?@CCnJx5jKg+|S;7CjNj6HK@&nEX?Nvad>0*KqdL0M7Wm-$O})>#DuU zH*P)WX?@%;kNME!vCm9w%pb33Ve+OG{GQX0`W*DxAtP@=+voUG%(Nqn61+5RT@E$c?+d=cYe&-`6534F+X1 zgHcuW81cN?nSaMi+-GGkFL!zDwpdrFfq7l1<*CfrM(y zzUcP6o;=+|e5D2pDgGeQb{+@YSy3eDq{=EPxAnT0f#@^GIazL*4p2@_07>#r3iUFq z_{}B==cRHALE&F9^v#!~C@=hEa|XdZg_Hxi7pl$e#5=o!gcu;}^po?QAfCkcR26p6 zg{TMxicLLD`dCN5n&hhS6C;nuR#2d5fe`YPjI4vi8@I^YA3?A7%7YY5lO7_pqZDG) zPUr=#+|;KO5%s|YfrCUfeCFSF!P(#%A-;!i0qVfyK;?vw-yd#5Ri7Hkt(Ci;=rnVi zYYd3AK1Rmr-BH7(6C}37>N>x0^tgR(%f;B+a`~Ze=7UHP!EufGh5qH_meW`oWfBy#8B|ZWo1QgtJH4);6FQf42}S z2WS>Y88+_1p_lUP$)8ziX@SzZP6pTJO${ps_)5i{n0Frm_@#DeL zKcKdFE+~;b)xi%lp|ZqV&l_X!b{G+L>Sn$J0h9MS{NS;(6+<#_Zh)KB-QK4Q4~PPq@&}EDfs{Ir~Kow-(An7-~$!=c5nv z7b^UeQ&Y=%)>Bp;>sD)G*K6*YjlV;1jJ{{NDe|;+Ld<%*FVW%CYzILn<&^pQRB>sv z!|W~RwfKiB!;coay}a`0d>lqM1qs=LWu`O@hmL=IEa>dJ7?yW0Oedd8a7sp_JokiZ zSzFk{DXcSLg`BlBZMpbP#>UhM@ztg@%Ngd5Y>Eu**J0fc0&dx=*+7^- zw2@!G15#Xb{E~~+sqysgFj5y{*&B>O@u3C^9)6h}=bHYr>z-)FqPb8uwHJv|_kGN3 zTa9-2>zp|o^+y#xGZj*N&AJEvR2nEOXsG4pwYk1Jncb+LF}Apey~LFNJ#{*mu9PPX zb}T;d3vRo%H$(i`(>%U{celFsTZnE_TsrHHXf*?*I({~&=gm(Ky{@(rw_lb2$gY}Z zNmsc1y`%G|tswrhv*l!^u3|{xWYy)V@a*mET>SMt3^`CMC2zB@+F&h7D=M>S>hz;r z*{rbMaAT%v$AnFXsrV_vj9U&v^!ckKD6DPJxpQ6_39e|S{PS+x4?Zxsp+-F|X51FM zHs>RcwHLHZIt0)*9n!Tr!+LJYO@+MU(#94^(BsL9CwU75dxt0&QQiERV%&`C1i^}s zj(Ha2lzm%hjkjocU=x$Q7>d5OF&B9p9kjI-;<8n9ul-(w>QGAp+p z&V1K&&r%bNr^(}e1feidi`J7G`aDrK9?;IBJhsCvh(`?v82Mfb1IYGr6@1XL3U)IA zBnqA2iy&zMQWVM8K?2lEvBr-)085cFtoYirwcEPxo<^(j7O07pz1`*jP?yink@606 zoR`gwD=qpA#d*IH5Cwh?Rg}p zehODVK6<&#*m-N#Lf0x;Ve-3f0Ith)qA6~iQeyS#)XKA3%Quo2rzA(|u9BScTIP1K zik!Uf?;-t z(w;nMDT>yN?%A%Gz|FI3b#~t#{a9^vv5J1CuP89R^~rTF^oNPc#7QGv>#QurVtQHn z{)mzne7E*kUTP0#>eiHOQeadpOhE zo}wk_rhRt;K+0$QDyfFwb#RF3&hJ!=rx9re#9lv8Y7{*^dSbQZeTzM_fbwa zxZ@k0b%IQVHqoa%4bE7Dmm2XT(Z5wX$-O%gHy^l?3i0q>yGV)mTv(fyj1R*-QxLjd zBDh1ZqTU>Z=eWf{tvtuTXE<<^;hpu@tEIO3OG*$#^cE!{tKbR2L!8B`Y}27=EFp43 zGrq9B#Q7mop{*E0(-Hh-Fk+4w;>n2a*6SNp3KmWak`p=>xXV3jCsO6h#0(DB`P)Sf zi?TR&xu>0=Hf=7I&Yo8Ui;zpb|M-rhaYvWQqTUHJy$S(^egADgulP`%nzDD72d{*K zv%CYF8R43fP3x2^ps%OU`9GHeT$)#KZ$N;r=h5E46wh}pWf9uvX-4fWN7LG` zR>V2FXyN-tCdBee7Jqgf8u;q0!}`$W&i3BPc-zF437LFcJD+lANrRlO1gRljfoICH z$SPZNBdcV+e^k_Rg?uTpZ6BdCg%V>^l1v+apo67&_R0*kK#T!>Ki;G$+O)BBBL-72 ztK`t_e!|CRmz*}(Bjl3o*R@*bT6K^iSe`cu;Wrd>b%d@UMToh5<36F(5e*e&X6B7p^{;g`0dfq1w=SP}O0H^~%E0+&Z{B$y-| zZ7xzKnCqMx$^=H#B#V)I?f1W@6Cgl zuConmMz=-xd?f>C>-n8XpO4ma5~BLE!<;~ezxcE3AkS~Ki1cB^7p|$B?@+_LZR>g; zDc0c|QGo{EysF8pZhJX(`aQ187Z6);l_OBMUq>?lEm(4~r>bnxFY!2C+L(Ojr3IWu z*g5RoS^%u(Qd+j$Zp`eRw?F5dSJ-&FJG*8+V=ESuRV!9{04|yPMmI|%)}j|99$JYn zcQU75UQTZhw!!#bY{*NjE<-nVV~bHjj{Wy=m^q7L*VEt{ zaoOVlva`9j|AKz9D(p}14O!~XQ|z99xJa6%w0B6-{B(}zj5OoXV??_Ih~ z45}9pi9oMJld2e%K*f+YruS=LX*PUuK}4&cFtdFx#ZTg!3m4^p0`Y{QJa_NcBdK68Tqgk?~Vl#^{Qjc;2Q-7^~OEe&yW!kJ}iY(5G zKU7bb&g_6kErk(cawZ!42$!}T;`dcj>aCw%w-^VFpFd1HOiWxPKb@AQM=|Q*bjUKn zE~9v+IO9;(dWO_nDu(|{IzWJ2gze7|zlCf*w+y(Fly%OinX7oWmy5np5)43NDTF+o*WE1z3Ey23t@IWvtpf8yz)PvnIuXWa+& zavKFnHW0$RfF5<&gGt=xFvKq$6E%zevZ?vTpwt1|??jc(jO$2nD3>{Lg7-Z5FKdt} zub8=j=QkRu|3T4cXLrg~s-Q+FjkyC{2{0Ym2e1;NUnS9aJ z>||V>bpS5LDzepd`oib6g|)t=kJiPT4|Sah8W)T9TT5hJ^O~4;#2=Pi<<_-dt(g8s#FD zuUDpx365cQSC>xZwQ}fpE;j6s?V`wVxg)i;;JaMG9bl;~3od0ow$Y9ccy~_DJ{t{Xcqb$%~Ex9qoUxclj zUz*6+#!YnN-bH3?OHn>TJO{WcO`b@JTvVnRtFHO$-44^4;+@B%#Z8jWnu7%90BX|h z4tm!Del5Ejd#{36ir9e*b$E*DAs`VXY`G(_@5}F8Y+@fSB>es;LS+Y5a$(_b#5~ro zV^?7+v!GeWq^m6IFK?LQe?HTg)?e&SK4J%Wt(2XgZ95j$dsnS=#h)Qs(FPES&V)Me zLjUbAS9FWrW@NiT@ATOi@T8nX+2lY{k{F;Fhe%x9FvLoL4&kF1N@Vb106SF zJYAiS+lG{ERB^74jT)4Ym)U#dP^jU%M0*zXA{Ul3PVwo{2KZ(;ceah5SvRVAz*?qJ z-DUZcW^*#?E-sJm>C*D(+wC5kj%k$X$esTBfA~KmS1c75HffjU~ z1nvG4c3St-4Ihv{XK9Gwr&=@aZ+~nva0NZv4s?q?i9_KxsEhB?&Q2F{%lrX;k_OKJ zr%sTg%9|rI<}x`6D_8=@4N+okmmH+r+g#rq4EKKq_-PHcQK3?%%TQ22=S3G5FE@vF zT>3=BMq!Z3*&BI|;DY==!$yhl=+S;nazvR^G<}Ru8sO0R9m0JCzTnV|r7Kw@`Gfx1 zV10E#`R%|5ukC=!ezZt@DcW(s8|t7Z#;23d%UKw6{&lev1&|80S;M+)yVqI-iRTVT zahE&y4v(NoEGN&F=N_dhMW~*nP9nmqM>~<^sM42fpH!}p=Qb(DW!56*LPOlHzhF79 zQVT$pwX9}-m;=R}xo(eu4RCGn??}npwEqNNY>Ham>-zJ$hKk?LM6T^7si%Gn<*+)~ z{>H1?{W`lCduW8Cl;_In(|>y!PzXwlDLgW&t>MF8LpSt;=-gAcTX} z?$m{!fo1_kS9{xK&Fd)LN&kj$GrLV1v0nO#^0tnaTilNL2=te>+xr6?39mQ7zd3dQ z^DeXK!I+T?v{!zYn}|n5j1B{=IW6U4@*%{4h(I9l+!5cHuzz5nKh5h*2{VPtLTcNV zjVOSwpcr={;ym@XZH_W_y&~tE|EI9}dd~fRso*cUut#|!zj4sRWWrjzB=1%oTnDfR zzv&(RngXz1Af2^C^Tvq}NaKjODI`b2fe;gcM83_C1QMNtE(sos`%V9b5$7jn6;4aI8Jdv(c?dErG_`#K?}XT7<$$X~=>M|tnE z($hZ)lytydsSF{|2jLT5JIVtEb<+D?$7gkBFDc|`Mjg^Z&S5K3!ao;(zY~F*#yv&Q zRQ*+79{hD{{wZ0naqNcU#&PoPZ@4{mQqC?t!c(XUO$6w_mcNe?d+@XA^M!}gmOX{` zcb%JX-PB1yr6vpH63SIku0&op6ov>p+@Vn7YyNge|2f)!-P|8!5IEsUjUBLcD-y6l zjJ2I>{huHC<5T=`SRf|-Tk>gy^+I{z>0!byia);b|NAQn(Q=iW6NzA7OfXDc6#wh* zCi906}JG)w`cT9>?!Pmhmn+nTvSd z|M%6$9fb!fzuK@6O)x$r4Xm>1)s9o-KX{rODZJ-xQclb*N%Mtux3|T#A`r^|V8_@3 zL*ap(iswi$ZWl>ClDpeQ_U%7d^A4q2K(A|iFmEV0O8nJfegd%n452%!Oj^+o;LJAYNU%|@WRj+Rx@I#0>q5Y zb7?5wmWCva-TAaj7qC=_6ZBr5=jxluE8l)2kaw1SlO53hH&1Jj5$TX~og}0*paGCn zr2u0@YPVaC)V)p%HZpG;hx`Wn>FzE7)|r5(4uzu5B$5jfGJq#uCzM}9D3gXXgTh`CFDgo7)oQLF}Yp-^S zw*2PA52{G=&xu57fq*)r8WB`S z`8%rcFRobS2J(~=_D>Mk%GO6(gJo67X@8)5LLVJLeFpiol?IWOtva2E1sfNFq#H3E z;#E_E#u(60S zH}<~%)OBmqZ6oqF?z;LVqtq_Bd#__X&D5c5xH>O8$J=_H7fSmS;yr^;aw(t468uI_ z6XB};LCe$f7}{t1k!qRUc4K$J!)cd>I=#`}b+L?&I=h+qsCkJ|Y^LigY*E4e8QOZyAbE00O_sN?cS4loD>?II7{MQ*%rw-5TY%o+6P zEcE=Sfu+2M1l<=op}9$74{6B6c5je8XiZXY#$j}M)!%wf49~CpU_3?vPyW}BbSKr1 zP1#OiI({V*PH7U(JM8W+VlG3j6F-kyOpZVm zd$AWL1V3k$9uMeTA~$it%4_gJTkF;g1E~pyDlQnqGwKtxZy)~p^28l#cl>l*dSJ=wQjE5u&(H#Qr8&yZbrI#kV;vbAPy9=G&Pn`1OMbeY$ z+I{k=Y{;MTEU^5gJabuD54g%?eCSaYZuhk1XxMglkRCpMo(-C_2SJmNe1Y1*CurkS z#Hs&lbVG&5%M^xfda0*&-h7Yr?0uHXBHDx>>(2PGM|hs8ISzidX+OLe%3i`!e%_zzjBqE5xZwlW1T0wD9PPZT;T}W1$MaC35!=d_j13%Vu{@l)k{Tf6;2^mnGWMCDZ3n zrc%`Hfykrq8dldV#rEiQZvkI)D63!rOYR zbsAhbQrU@{vWuKljko}mW7Sth+j?ff5$@fvy}H1E*}3;_3BvC*&+ou`Cas~nDtf)+ z1wx}H(yPmME%%)Yj{h5pI^k`FeK}E^@~~&{DEyGcJRVyO9MJjo;$_f4gt&g43PL&@ zO_%35hadPKhdVf- zfb0nMRpj9Lp`Gqxwj(kT;zm3kbG6b@6%6}pEC1Svb@Fii<;c+NSTbacv4{V6CODMG&Qi?9!=nt%YPg)2Ci^uvuV&0ZcF1rB z^m8WJd%(D+n37fUl-B{eSI5Qf8`o@{r~QNP-Rszi2Uoz(g4t2P+uQs8uOBkzJ%zbL zjj;xFodtG@bx~p(5$CMa^?Ub2J?-C|Aiak06@#4K6Ptau^*8jtWD9NFGiDiUS&#}D zh;S2)l0|l}Umk9k^SSgB#OsM#7GZ-!=1zB+&Fb0Q7ACtia8^Bh^CR^^;o;%Z3g@HY zIQfzhk7ra$re)XJ*O2ktnxdt~;lvZLfd4BXf~g;~9X;@+NzVw7|ONFStKk(~5m&Ve*44rgsdW{x9LaS8DrkGu~ke z?;7xJv@tf!o0g|$ye)-`msb%WbFAKeej)k!=dl1J(o;4ybCYPQDE4Q$_#r5K)5e-JpKYH6IU(BmdvER6$%amo*dcN<9;zeZ%`~45Z9@TbPRI*Cw-v98 zFSvh@{QZvLuoG|r6~Tfa0BS{D%teBjqKl;6$O~b!CTWGG^3&|>>?$ns3T7<{el!_2 zmft=cAs@iQ%AtW9%S+3ytoT=TY;paRyauI@&YIL6O@>jSNLNENZH}1!&~4~3cD=;O z{-z0R>m0P?;_E`vpg%nN;2~N1|FQR$QBk&g->{@04FUqvB`w_zN{S#MZO|j#9U`fK z)X+#OHApvzqIAd5-Q5TXyvN{m?PuTjd++W3@T~QGxz}>da+o>iJdfk}=kH%qe&52s zfB}+fG~LohK|-#peYDL2{t7l)`I-b>#R~7~@|OKV?kI!=Hp~6R`v^{mI{GbiPW7_% zDy%SJwDy!6Tp6Baz~ntkPBE+JvbR3nU*ZAno&E2*f?RHoj@{|W*8$YOg`xo=kRC?Z z_BaJ*bj60REOCx)uEa4^id= zqPfhE-+xNM%|pTKCTRZz%Cgkk{n%2$b0JjKG{gxw<^`M{h??V?{SLM#s$jTJv6j<= zw}Fq}kSb13Ir)J0&ila1R-UM|&hfpv%**Dwl)#+PRCc{`1$=wt_dS5F znLxSQF-;2aLS~D>q&6OpLkqPbVFgG?e6ZsgfDu{TknY^0eL=A=qZA$)2_@&Zatr&= z*$1*mJCpb!L!B}^81^8!DXXLI`8RYmGM~RiD}w3UM1E@suOiGA{$I1Syv0b~p^(P- zclPyyCl7-beyb&90>qxN+v993JTNMg){{+Tf%zj7A&5Ot5(xIWnIHoc#Lr=fUaN1O zL5hvTkxh{H8{@fM^YHQ@{pz0L60m)spmrl~ReUWj{D-BN5Y@XPsxxN+gmQE*rma0l z1PLo|?{L$p>U=TGIV11 zPNcaOfM-KQDczlT0upYN^kg-0+MJ?DoHMI)7481kHGnKKe#^rixSqPff&80=-(>XF zAD9}uFTxa&EKXH>CAatqv|p5l)vmMV=~uy=cBTzE8xA$% zytl?HSO>m3PX$%9s zOas~$RXFr3HNTF3kbbS121JhL7F&-P@c)w6^9j#mr-ScbeDOy~TeuO(<|H3_Q43_a z!^II?)SMP*zZegi;58u6AD>fa3a5yUkYaHQ)g8Z{24y``ztU+lk!5wki>f{I8Ed`_UNS%A)t z0fme5^YimVr@gx9E3h|Mr1@KDD>qZBK6igeW=-Kr{7r!1H8b+Y4(X+};}iQW;FPgu z-iH;rZjRE1{EYB?OTwXtLD#(CjzovMkcdR+lfBn`le!Hgg>!)}Aw<*3SDoos20a|r9o0(?M_O;P4G(G`IE&b=s%4-*1NT0dXD2ExS z_bum_+A!b~B21b9VOcEhhLYo|$6`(o%lXB`jOyJ=7JE|l5mWVqfgTFWk73h+@yXNb zIc<$=Dbhj>T@s_!XfmWw?-|us({yO$Y2=74-M4i*^>4FDkTm)wt0Zhc9~e!IzqSIZ z+Kv_<%7GsJXKlcFjr)m|ffkM2L64ZLt8}X$?fweEPg2mL(t8O(;SPA5QcAHizC2JxtmnqdGV5!4G@~ z!b)WfK^wwKTa5Ic?YmR4aCg<;o{NXu;i`It+H=VtvRtX3w-$c-t-cNU5p+Romc`XqqGBwOF6<~y&NQ-v_dQaz&l${wNniE9893Jm`mf?iz=wuXQv zP&x=So#pB054SlDo;tq{J3^;$3cZC?Y1W+-P5%>lTn2#OV12!5X`M<*Odu+|$hm z2CrL!VbZS^Ki$m!`mFQqU11H-Acb?4iDbg(gGUxFJ{;c}uho}<sKL*$8Hfk z{?#w9T7)qsZp&HTt+T&C4keoy0ZDC1Oan7ORt zXjFh;7lV6AM?L>Z(v#wIWyPJ!5~G*02Ng5BktTzc z=_s7+_DYiNF{0*QymV9Nnx}2sC*^09UW*W$_OwV?**9JI$g%Qob5-R3=!jnHy=nNy zJzTJTQBAweRq_{Wa0G+$wt;ZAM$P=dXCp@U+=;1ZHuH!NE2fS=MysbCey(DY{WQUH zKijK&1!LWKfJ{p(WzWHoz*~+Fd|_X#VFwfwG&uv=fKjAol!2Rc%)&$rcw!bo3IV_H zf9!rCQ;(vtlzL_#M(B2bs>ad$NZCqm)}Q7^$&IfVnM|}vM07l~neQUHvg-+BOT1p}yYOdSQ@Qp}SP}^m_`+=E%*8&q5 z@2e(n@9^Y@j?p!yP%KhzJ7WZrIUX{W8v*!}wKGN7F?KYoE@pG2C^;;%+N7PZ*C&jV zqXh4zacg5FDgjA9(Qd%41)6wi?bk!)VvMZ%SC*o?;JA5366+ZikWdOYa4v zZm%wrs|(VKF5Jx}IyPPWR$~RicC0VRbDtXmFu17pyS$Uc$LgS+sD(a>(fGaS+XQEy z#h+2R`Rf%MiMubn6Fht&^be#Vga^6br|B6TjP}KSpQk|^;**dKPjBz;HxP&cT$mo_ z9uD!~U1+FbYQFZo<H9NNO`VBl zD~ms@N_d=|1X;8XXP^nXpIK>>mqPX&8SU51+(j=5@(eEZdL7d5+}9nk9b+Ui_EyQ) zs_)@VqgJ39xJF9e0Hoxii0o@PJt0nJHf<>ZAu76p+0Tf*p1_?DF;s38uA@{;LpZGl zIUUuUsf0=7aDh&N@q>j7fo%MpdlDfp&@)h9+^B`W7Rxlnob=ncIp!g(SN{4bQac(; z);u{~u5`2`6J;W1DYDO45fOC+gs`7mZb@ftn_9qV+??_^SZ zU{hn@!0t|$G=?>vSuyTBt9`ON#Fa%;vJRs^vcEw`R$F_x{eXCYO-R;eqdzNHFP;QW zQ_HDesUrVFgXr#6$~<|dwRC+zJ$ea7>s@o1MRqy#WMN0Mq!72&Tnxhp)6Q_ieNYhU z9%ku(D-&QFhb{+PS#|;#XhyjzADWTYcAw%xNlci+n%X)fFZZX|})tOT635-3y3U zq!#^+Ng?CIx3XV_F$e|d&}Bm8V)+>4)L%Ox%1!bGp3q;N;Bh^m70skfnIA{E4V4(u zcPESN^r^NsiVE<^@{BNkh{~jHF_X$?@^bcos4+0DOi#L#0;0IeHMeMxa zQk%M6Ng$WhoU*SVlEI0d5XtTJXdB7yji(_@@4~z zs^j}HEF0;n4PV(?&jt4?ZBcXtE@>}m8=PA$oeb1YJY~$09?#b)NHK4~qmtm9N=s)5 z*Rg%_VzRDSY(0_0k~&E;iKeq@jCOAF`27fzL3$tlhiVe?9x~l909+QE&cnMx?gVac z0UIehNQ63b>w|I=8Wu@5As0=d^<=d=Vx+g`l>fTJv@8bo&q0Eu`j4Kz@E1wE$SJN= z3Vb24C}ijac0g#(NsAf@tXkN5=<1nBSA~SkW)XeoSEcg^a2_6@{OZ4?;Tu3J#+>z*EZ()SS{{M$H6|q5vFZQ_$zdN}#sjx!m)?%zTo; zUAkLSAs)<56+^Yo5MoR$9=2!_cnZPYhE7X+oPGS3GB7TN#j+B(!Fi=J7BWuk^$f}? zDk}Uq#`pxBoSctBAG@u7Btm5^M2X-RdL%MfMPq8`7Er>H;Hr{J(IS_Tt4i!;*;CajC94q-}&X{S}KK1+VF={D_v>Z z#z4``BSveFl{wdc5Yj3Qe$fdQKHRQ545H0KXIlyA`u=Tfn5D} zeo@#I3oNJk8^N_-#g94=1Lu*|Ls0_BTg>x##9%7xeY@^)#Y7ObVA??gNXVfdseVbQ#1o zxJL=?bJ~Xp#E%*q}EYSi0j*9mH|y3&_rs`%p$$JepR-B2`V|Q=f`k zU%L(G3tc6bG5FR=WQ_ThyrrT`F7VY7SEKLDg&$&?>oOQzHk}>Q_l?a<8tRH%jT!y13ELNIyb9m4JN%eRrv{W8=h$r zykrHSaXB%hjTU}m!vL9kgA23;(gF}cyBMpxRr^U#^af3pE1qU96F-{Qi@~MWFd|us z7bZGy`{YWv6wa7zy25ocTS-YURyNIwMNo^2qcujYlg+j}=Crf2EhDR4Lp+DA1NUSk zrt)M+vFtguu!~Gz>Qc~-OQ)LT4m2TlTe!f;Sf*B@Pr*v-oobx#2$W*+9Sc^D@_Enz z%-xW`yQq4k+Phv)dp_2O&^2qvxa-2T*GJfX|E=ad?{@}CrLdmzXWzAT6ZWeXH>{)S z8E^FzIkR<8;^ZqXRCn~eO6xtxy&I>tQVfmTh}MLsFus6Ak;f@_EKrgw<~V#8Ki!?n zXe3&M&Rj;h$D}=?@1himE~NV&$7QJEe4dB3ew@IcQPmQeP9Jg)TJD(6p1K#HnP*po zO@5SDsF%*L5G>A4-1(zzHPpBRn8UfBC1~22A1R-XLKtRpYH|*p<+Z9ZLIebVy&GfM z&{9M{vdSP`AiF&1b(bbBTj{yraGc(N$di_)b?l*FqDljJ7(a^Bfds*9!w;Zd_02{Q3O0mAo0{v;zzRws-S2 z^JMJ1Nf{zKosd^M2zfs8W?;$ha5|~A6oR{2i%U}xIqDq zjz`hZAbqTlPaGg8d3E-yi^RU0r3eW0iADY}2$GU1>%__}YBu|b45*toZJbS&<$HokgI~`w$mkaD=Ss!-E@|cax(a?*C zt>~7VxLQOhi!{F17uc-72WAFXjmNR2D4Aj3Bgl@d!`pOR zewUU_a5Mhcgu?$5di0jJH-|c~^?t{OLb3Y8O7`*^XJ>*Pgl+v9T#(r?Fms$7VeHQKDvASF*gz zwm1_MHW?*{xzJffuitr~6(%wsnSNa7M9uT$%-KUrq_WWRn7RsPxLSjkquD=M`s#t& z!-=RgCkipy#$PsxL&aJTm~h?oK|>`-7F(p<`jnRO>W{2(@AiLu~F5Tl;D;sba@sD5GK0;(m9C zwuX?efaT~VR$|ku^-L-eY5gfL0kD_UO#DqV4 zWG6mp{a(m?=FtyVEizAwCQGk!lxT58oxHUdq^@*psgrPE?*7&c z$dg#?gmS5@d~j^Jg$M_s3I*x0iXz_6#aRAfvA9t=uP5R-e2y9 zqzOVsRDf3DY`Q!vFlpca37-Rj22NmV#DlhSN+2VST67zrq8%V1h6`xU_mPs9qt@rZ z$_7Q% zkX?lLaZHhZRR=(Gt=|S=lfBr92eCE1Aw4VPI1Ts8m`{nAudi?adE> z`(xDD8%y?jZ;=kDs-#O8K6rbF77Xu;2mP!bfOLapg%;>;m(XPV&HG9&v`qDRb_+7l}6!H?nEBburYw~`6`2} zx>qV|>n6%0k1sCIcNjrPWgmcHi6K^zcfxIdFiM0(#f2RDhx8{u6RuRYoSzcT2 zPTn$H?uVW@Zrw?$yQpA${xxvW#MK-Nft6e8ab$H+v6}ZW z?bd*iWZ#LKx8dqW9>};_u5Mfg-Mm@91>-@jhV#)jzTu+cOWi;MFJDrnRLtC2@yl$EQ3`RNFSIY$angS)x~q z6q@=aJKGeS5^0{kM;lXbO%oR>|O%`$p(1O^k88zDnhA}ThM^&F;Y9x~)j`{kI=b5v8ixH(b+Dlfp+pJFXXFNhj%G^f8o3cx^f}>60#~12%)Ex! zbx#|gY`x!aJ@cImB1v{lq><#BWS4xqL&MKXT2hzrFcr#Drqfa0Z=L<3O6x!3>?RfEU2{3-r7sVt#OO-Zx6b=9vKtV|~I;8k{0O;`C0eXTl zli&L+Ye6p5MUi3DKoX9|hd*X21 z-K(BW3L4?7|3OlQYBSTw0oqpc2?8_3mgq1P1L-fu7Poi|Cixm&WBAKGsSso_SbFPO zcn-*Ff56vv0%BQ7y<-5>MipKf$WhJ#p*~u4jiX8OJ02kJOZi;NHUL5)@G1%fq&h)k z2^R8}ccfftm|vE9(`UGG8-jtj=qNSqWB?vk#Kmz82>K*^3?;g{@{PdQ5|_D(e1B2c%livpXLn%2y5brzW2^ueyiA;T<@;I zR)DZ4MA>A@g__KLeUq7S-!-{$#qzQJ*Fek{!NcXL5<20a>vy92=kv3DADFkSBYZZc zu?I4ozx30fy!c~tL%vo)lCg`(O*kK4-kjn^Z3dD`gPP}{arwG8*ds0lJ8KZ!UfwYH z7KD=n;-xN*tyz_DAdIHgHs0YNm=(dXkNp!44+5&1D?q|v_$6~WNKZ&eKQe3z9=M`0 zxmvjocIVfto)s!&z?FVg$c>lhx|%mZi@sd?p)RW}YgS%?T{00dUH;7fge_PSyt)nauMn7X+d>i)>hGt?Cgm)}Z*P&7V1 zvd+h_l#-PtK_t*)ezf?^p_V2V@npgu`65H}n{};WqnGZq+X9Xw3{1RM0PwHrLrLrxg@`Of5L4tO4%&R_5=RN|usKVz8ORRB zmy(7ST5(wlsv09|(r5es9ID#)P6k&C;x_h}r))yyyc(eS-A z8=u8@9?|UFl@Uqu&x0Li%GkzsQa(J`pAn+tI7HmY@M*Q6}e$O&7h zgc&thEo-%A8sD7MvUlWL#zb(&4P=ZKA^(JZ1Sj!()ACu5Yr1THzEmAO>*WUZ3wHGa zhoB9}4Emv@>FTng-MjQ|N-r^l#=_s#_yYHp4DXmt67=V@9rsXN(Q*uc7}@vVL42r> z)N~6rhQy=8;i~+mwzA(;c563DTXPUU*(MUJ$dcl`R=q*fA}xiiFy9!{S)sXdr6^3l zVfyOI_$$PY1Jso`cnz))!c7yq#o{DBe>HrwEn|oV~4Hb*I zHsslzFVJ?JFndy!U+WC^uUnCKbwb%S?oR1l*i{j zd}6HR$wL(YNxLU+O=0Z&*@fx|^x5l*_Rt$v@vWgA&+Rl_=Q?pdtpRymNx3f1WUqqD zFJD%mBk_42VtaqLvx|VakU7Md(G=%OwRKYXp%)>#@_F(n1AAi3P0YNvAvWw zlqCcRUp$?BSVGhz1>&&Hy}b}WLF7iCwDWPU@)FBCnkX{0*ik-ImQlmbWxkf#=N0Qv z$F^3Rf*n!v#aj1+T(Q%|7$)}Y^o28t4;h*`@@u8i$IB2q)pH1j>I@Oz4SsNpPl#1# zD25(F_lKeisRm8CkGvt^S(#=9EC;e_o=^56YwB5U59=87D+jEfCu!>p9;|D0G+kZR z*$bgfNN(Zqh`ai_%ibe z^x<$Z9{I@Nv!tNZt=$ieYN9vcPK9uoA(3<6t#lFB+O>;SI1`St70tvEBz(H6(X7Hd zL*`AFM&+5Z7LMgQoUUm8V==$L-=-u@DVo_L#j!KL$CZ(-8VC1m_bd~ zNu&!3&e7V*=h|O&KE-)6vKx*Uy2iehBReIBE9hznrDapF9|HK2X12($*cdO1wR%|@ zzZ)tsEg}IEZ?0&?w7&~D3QU*A!1DaA4g4TI?ExFHYh9!MW;!h?^RuaT6tRx2+ zAC5d!4iq_ouUt_{n!8+_tdK?vsUy2LY&|7=aGL0VSa`=JIabY>VL>khdh7WOYvtD+ z!w0SC zo*c{&HP)rosR1c@gCa%o(26lriz=`hdkXTb30!K9b?JYH3fw` zD-K8&ekn%B-co(X_n~lZ_Ujw0i8{(i0LyY;3~9+4k(UoLW^Q#)oE)^)K-^SN;*O6F zMabia@+rv~1{q%(zJ?q>J*>!hu832&2EUz3e<6}(tivaihorQk+MCm<4WVd&*?_h3 z4NGpoi*1c0d$!yo5M+$dF>*nE#WN@8e6WsJ)JJ_sLmQveDI29ARt*u zN5o~*B7bq+H({5dI``$gxuYxZHwgq!`L|;JJUhD28UT5Y`Qc5@&H}c$@FTl!BF1sV zZ(nlF>NHpMRd04!!sd8a`jQMyRZoO%YiFic#*HVw9|B>Wbr$ifpPe>|#aoF*?|xmj zO4?Hm24!}lCS#j>^z>jFwR!7R9gSK;-#7MP;hl{hjT{5%vT}=}-1H=~BM6!yRok}b z*(7m$lt2yk#!-CUy_0=WdeiTX(=XYtK6Cyq}L-6HZztGcaGfe0@ zswEE^p-JK_TdlpF3ei7*i6d@-|3H=^?bVrNCBIn49P-7(-H>s<8yR!KAPMB*5TlSS zHu3JY?eo67a0i9?QA!l(JD9-w?!C8nLz=ezP#A7)_NRkVXk&!@%*VqEsb136E{A+HC55@;R|Oa)5)B+MgA6^r@F3Nqrw0Eq`By}6`Nc~c7%`4&0S}*+#OYY1Wq1= zgGx$pW{p1LZ#QR?D8&Zju*#7exsttcFM3_in0W*PAn)_KD-eesLWHq8o8)NGEgWSQ zJQ=IXRy@|sZ}>=4aHaChUIiDHD&ncjhY@%0xtA_qMY5RIz2In}N^+c=oj;mzgCO7e z4bS$6#+0M)OMA5^aK2g4gQ@Acq~4RftW-pI@iv^c)s}@BHZkgUHgc&gchhYwdu@(S zZna)KxMHVgzF>^^Qy0Z%=4$iJx_8SRw!TDx-0_h)RZM_!e^o^V}c7Q}*h zxBPuEF}%E3J5khQz8+Wu3zaDQ8V7R&6M6!bkPYbJeF|g=ETLfdk7Ed{>c+#({SQ4T zjMdA9bLqX_7tQ06J3;ESetx3Aox&9x>`hskS_}($F`S&W{T^U)T1SQX)yi3cL^YKg z`Up8kOz{^}(ai3Eot)W$Ncx^U^ZLzbZuGMo@NaFnu%n%snaTL#4E_nZZxGPXHnMk4 z@LX(Kg3#bV`z@%$0&dT(pN4kb!cJx}lxYWrqvu~<7*>)nLO(CxBzgP^jH4>4{Y%SUNTy|HN;b#S7`mR8*q3`*48OAoB7~0GhE?UvAL;@*eA2& zTrnc;yu@kP0uoc)HtQN|+)nY}0Qx(cicDp6IwBZl8T6O=Y$nQxfKkR*&qTT}RHSpg%*I3Zr~5#>s8?=5Te}&ntvtwBP)_%e6D413 z$Z2qa{Mk#{YqSWwAWMIN@0$I-S}Md#5JR$4+98-7Y=sT6EJ#>qa*)J<-p4Zw-AO9YWVI{)0LFOs-!AF zI4Vws#9l)@Wa^UiVszNr`>1Rc=H(&-%$8q_81t?aK~Fr?O5(A?xE_wYd+%nykRU7j zqdTVAxGUxs<3Yz=w#~Tv(qeVtU1!d(>nASQ$_hVma^+8a?}X>fvt2B<Q#AlHkH~RHuUY{bbwgS|DkkMuH zsrDVNbTLDbrbD;lh?nfgG3fDe+QEZc;r`t@>J4GXv{VEb3fJdm{V&dq31hOx@gc|t zV*qY<3`j8k9;m-eMerL$rAY#s+PkUvB9n}|>NP?o;D1B#go0gn9Vu=z|4zNdJMh<^ z1yWzo;I9;nWg#xUI0cr&0;1))=UE~Emf*;4eWL$EIDQzrcs9vMnJu(pO;|U+c!IHN zXOb6MeAM-XCsR$<{o{lV6F$i_G5nBDr+BJ7zt)=>EB=a6&}LcyKHS82UuQP7D_2IZ zov!BY`4%%d9mV0Pfo{nq^;qNiC(IekbZ8m}r}sEKroM(}T$M61`AULeq3L`I6}4BF zxQp*zyrB7mwSjec1Q-+HavbBVf1E5ewvEEvby_h$QD>_aBk(Rg=Ie0X8-2U2G-ATd zqc!%#%TAGnjNze?%Skv@&_cgaCf|E4|LrgN@1yx*{j#!%)bMv-f~^Jdjw_0!V#%lH zSE<}GsQ9131|fuxx8dKrcr9VRT>a%lTylK{ymRew8~i4EF~N~|K>7!n{0JwZ7#WGe zARMFb(=C{MOT$lxp1PB5$$URE4vHjpOFH96b5E#?&T`}6z!g%Cx2oX7v2ZUntaXs> zF|KDFE6a9fUnA3#EE>&)4{TI(s;$G)gElTjJ#`(Pu2ezQo#C8f$7>byt&uCoB27g< zm8V~w)L@ROlApP_8ALeJ;O8Kt!ZTxMvceGv(L?5s1mJYv_!XzGG#(#>C!674bL(F( zv+w^h0<)z1F|JwL{|3SC(FQFb45ozHBv+&I2VkAQv}m3MKzceoAE25&WviBNNf2U2 zX8q9I*in20P4$fIOiGs^Eb90XB=rFcF#i?VA#e_^ns^@or>XMyt;>o#>Hk)e2=nu2 z(_eSKlD|Xy`-3zrU{glTCawG!kN$7BzP!~|jURH)ALZerPJo}A?&WV~DyNR*8ENFc zvmsK4F4SYl<=<$oP*^%%dx2n+!SAbqH;~ooW;a%90PQa_)!h3pcW0R0IJp2DVj1Nm z^ZRhGH3fnzFo7wMN?L?CU|(XI>g8hIZ`CjAzx*t1o(RoW+?Rb)wQfH^dldH@EGB?8ZsBXw{T}y-X|5+Mb zFXpe8E7IMgb06dL( z;y?q>aw9BDhU()xPodvX;RaGa@xh-l9)l$yiP&RPAl{fX0ivQYl%9FZ9DHKmFQNe4yc|$Yb}9K3L8(Sqg?bfAoCei208K1~d%OGYCcVY|Cw4)Nm3r!9 zG%Hz)I;6?_^x6-?z{gO&kpifdNXD*PluM5u;r_5n*>xM62ientH~mljW|OYewn2|; zQ_v@103`RuV;xlxVuDr{a2M8qx zS_S!YKYI{*1DOgF6hb|G@jYzmNd^Boa=qjzp%~(VzZQ(&fco|90Qst)(W8N?ngVfS zq#e|+JbXomOfx}`0Bbt{#CWBgYcrR+F7x|v-Oss=R>u%urC z6K@;KlUxOme4hjApEQlIQY>F)V^kCO`3fZq+NK%QIw^vB#O=y=f}&vce>)_%S2&%gu&=zz1KEmM{gn1>;5$-XfNx10 zSJFfu0wT;*0UHq8f9~}B;--0O88~l6VAC*KkCz9IdPQdwA;0JP(BuVrucRPK3pYU#$`f6{9=|Lu?kli~laRU%H6162OuO zkEP`ocp22v3=pMs?{=c}5yt6@c(4x%{4HVaJFj^ug^#O{{fq(-E8I=&k@y(m2UNTUWh50upUvn3Y1}fdz3fg6s za<&RR=`{u%LgycX?MGyU<*tEQ^ybw!0t8b$1|kY=y^Grh@x~AFW_OX>>H0|j%gYy8 zq|2=E;KzK|CN+kTe+aJMMrj6=%Qzl{xx`#>)XU3bfiW)1aT#|Cb%bds+$c$82#zS z{#W3p3pv*dz+VHuLYJC?^-o;$Pb>Sci=Wj&9J`gs9c%>6@S=wy%u*CrA1jP|hCD zHLZj=fhT|es0Zyoxo(#YeA$Pe5*r<{W-{=wFMfzQLjQyF#0X>n56eJye0_v+31HC} zSxYVd$(4UA0Cb>kFs|*EeuzZPZ(Z^SUaLo#UPX7n%xAFEUjZp_bOrWbqMqzX#!k3W zD}b_5hUdRVpEu5wl;L#-RG4SmEMX5E52qNV?nD-?nC@2HWYAfb+c&MsG&;%w{mTA8 zdVYSMV1Kp^N5c_pi|-HihYnwmQ0>Ajy@=+Jog$G2gz^>Nu=VLfc%C*F*_zj5!v;w~ANeT99C$|2Ya} zMumH+XW{*E=Rlb=*sGp+-rtgf2AmXX!i@<^_2!CNL&45t6rIIE7d55cy*h-mTWaaR zsQ8`_)++;kJ~!*XxCH;>5#iG!em*cR=2{E{GJ&7xOZ_5DLeN}+(ps#1bRw7#HNMOe zmwgmPS?bj(OxDNR#OFZ^`6Tp_YcifX{)+i#gdw!r{)M7{isDj*RbqDy6{$g;`+4K9 zU#%=-&(W4)Q}tZ#rA+Z6aXd=f?4hR9>w^Q&7az4l#q-#yo(K$akKXh(bkvJTZDb)1 z&y9cK9@Cj#$JL(H#MPETB*1SUzJ>|z*w0Nqh|&m65>Lzh?B zY7(&kQ*)|6fTwUHHI+eal=DsYay~dWIkTBNQS46Co|LZc!-RPmxYXhanLFUb$NPD(GDi9UU z7xm>@9pj6m?k{zcZVSrTaZD;^Ed~-07@RweOOst45V&V~kdRGQ`gIvPD~UvmGk!nF ziAoQSzqKc!V=c;Mi#N{cx+9J;z+^h$##8C|oCrI)WHFF0FHY;MAx2Um`N@i{a)V8> zJauZ7koM1_1N|CC=Zf2e=)mt7_7ki4Eel_bx|HYVm zRkP^z^hIkJBghJ6;nHcpW76dFVXg}9Y0XV52!9<)^y9tlnHXbEx(eB1Uv+v!+vgoz zr3wv8)f=Cen0?#71xF@X&^&(ae=P89Un23>2KTvaYnBJLt?j$^TdgGsGyLCg(sSaF zs9C+IH|2d1yz0M-&|n^2whGPsMVneMJJw>`egC-Z{GUB~L^S7(zL+ewT9kG68;!A6 zm3rl0NB6lplsOCbp&Q3LzJfWA;HnNwXW~zYi_A`)G{k&wjhShY=~rJCIydT@HED}G z;=raF&!vnuYQAggDMNW0hw>AoS&5Z7T6-sBX9UxEwFdIzR3Q0{?oaHyabcEzuV(ro zOzhe}JbV_1R1*(7n?_D;6_2q)8ElUkn94^szrWHbmAZAhGijaJ__KOb$W3ehsd6^w zMWI2Ir-b8PW&OaGny~#b2$JYzsfe&VKRRF8)zUS4=h~Up87-!0aQgf)%SKv^&?kWZ zpIK@NHn`+`97u39e+2!a88rA~hFys1qH&y=+;4E*+RA?Ehq$gK8VV^)^re<@ILF{Q zF;oNXHTuF`()qvB7yd^^zCMK63?S+7GAw?Rq7Pv0uf05(y@x=`nuRLb7H2hAkfA8K z4OPzg#G)qGRZ2biw!s4~zWRr+Qe;(2Nf zdaOgZuEJR;L4%iZGdeod-v>`%r6OJ5kcD?Up#78I?{?RD1e zQyag1zgg&Fbb4f``rf-U8=`{IWR*?zAx-nL$hC!8Tf8}K(u1F>38nZYCXovn@=jH-{3QYcAYN7)*CX{9a^Qq?a7N6 zj*mYy6)}nub*y6&tA6f~_!QU4TA>G#82X^yqhiBas;kwLW?9$6{%(yN48oeK$n_YD zszl3?KI%HQq>OYe413kBpF#c#OucZwdRjf@G^PuATnVYFwO z!(ZYmvOhWAQ|>*{T!_n!M4vCsRa>oWGIZ{S=E#VGjCB#dHsZ~C$_LxA2%TTxA)M_Q zBi0<@%VXN)#lQ0@uQFE)?xf$ilu~~8=e;ka;|-1do&~Qaoz>48l8uvwx~$H|n=5$U zXmk|%Gj>bBo7SBt*nPL^=w6DF#*eF++ljxDuo3k7$o|w*ua>`uzR0+Jt`bM|kD4Ay z4H$^-nR3@gmcJds5r(CxyOi!?!!F9_CD(Y4sTC9X(%7q5Ki1fYw{CG0w>gceh_>Ze zKj;=>cxg@@Z^by+0dEMeadGr2oD;yQ5eBIjmR(ha9p(r*oI0a}qtV{o-|w;C9q@8s zr@&i6%V1-Sy}15~VBNu5m!oL)2r$D2D%5wE)NXp088da`kQ-2dFw5IC_tlaUE1gG< zm8GvC!%Di38UjS8>aHH|H&x0w)u>N7nBp4MJUXG4aF?ad)_!xiFAt|%7WVxQP93cv}S(KwXW8w6i(+FYdurNSegjS@$*h`9`8L&_oGA=p8AWd z4wLPk{%^M$yA3?(K$7ES(%}AONNQx!h=A)b%bJjtPV7zinaU{J42Q_&sOPXfQ#7d@m zZiyAy`H<*DsieMr2?|V4i#@$aj{TK1W$6IOd88qfd`mDv38H5YVzo6B4^$-YrC+%* zbi9Tjtk$%J6R~5XttGM4f*Nto>f94BVb98M>mzMWw%q0I?R)niIT=(s!jw=t~ZonY7-)zSQ@bapDBe zFN$XvH=Zgeer&}5_G3o<-c(o{*3v+-{yszXh{Nm*!V+9NdE~RO*c0IW?{z=;RLTwE z^>HcoT*pUdOMs4w%==Y4m+W1cgfI(EU)3GuCg~x}m(Iru^2*&u)a7aZ39`a@AS+U2$hwC1Mq_eW zJ(wkScWra20m^hOnafi{S1k9H#W%;hkOAA|!pn_-z}r7I?mK+6!02)pR(oi^5sJRW zXQ$Jo$3%8~851G&M4b=%YF^A>*nPM!R> z9@_uti!P)7Wm`Z?-)OXV`7=^^8oOwHt{;nTi!NDG@x)22ICZMS%v+0S!CX+FEwCEp zgjlK;wP}~hcqqPSNb3R;=c%3Q#KiOa-Z1~qnQ9DBB6HjJ#PBM{hq-bCr7kZ0_Ghwo zs;xplCs26ypuR_W3q~iGo~kf|CiCNlr`)gKZW*)%B6Z_#aDg1P@#8jM!9`-o+Hkne zb`q?nj3IugeZKQ@0j}bw=2{y2DPxZ7PmU-?7U1Ul$ODuJsr3cn5Kkw|w^Hd56sa8W zJFJZxk`3kh)$7uCzO;Q(P-HpGKedd*MK7PGV3q~}BkK#4#v(hfc$F>Jdw?mXpbQc2 zK3QMk)9Nr|x2HB3tkj!&^)Pmw48wFkxC#W#X`xvHzAOBC-3!G}JMoZD=N< zGw-0P+wG`0R&k*n(rX&cX)8{TO%md0Eq z#qE!Chr3G4S$i4nFu#ezBwPQevi|jo+)RJy8Upbmk`7*EjMmTcVuG+FMbA8`@EC z*ECT7KhE9)EXrtKA5{dTK@kuH2?dc<5D<_~ML>~eC~3xlk#3{}MM1hj>6jV1yGuZF z=AS|g_dWOg|NCq`_j%^=A!g+J*0S6JenQEx^r-|D?eNnj@tK?0m zYCCDheP?wm%@mRFjjJPg~Id4yYyZba={3BJzeYFmX@E}fofKr=1K7mQ+ z?5xnLI=jDn3&4oCud}OjPy?t^!RraJzO=zWfPcz2ILz78%xH#}J6?D_zd5e5JO7;w z@wwZ2Oz+iQxuGH+=i$owAph1NYFqSEP)9BWN$PE_cnj*e*U&&DDKaMbYQ_E7{l5aV8I}3(`w!1Zs^eTd>h-k*) z_eZt+`(oHXIpo?*s8zb%OtWlu7PZv`x>dw)*ZYNI##i4OdJz8dkdEm+?HZz#R}>YRyUIic6TxBxao zId>tc~L@yjW|E+OviAk9Aw=yo*A$*EVHe^^#);qcr6rnHBRyk42)A^F?990?48sA^T z*P6nmm8DiS^k$O1BK&oV3k6;4>%OoyA{3`~xh_o+9XoxM?kmxAhGmsF8B4pUlzZLn zH4%62N{!x-NS|KOIB8G3a*tUtJJsfBRq@F2O!Fz3*z)yUXEt{RDg>J=fGBEKnJvkuMB^ z7 z@LLYrs-s^$NrJw{N>b|Tg$U-P}iq208hXKAKcec+7*C7+BwzxQE&nT*YIn4`qw zIl@oP>m0IP;;nOJV-miUb0Y$`xbtZ@V#WKD1bb+g?rsLr@@}jVzS0uzXW^izxr>CT zshYDNs!`C<6$SDe7@&%UssD){QcZT04>_}(t=8wEU8d!sU)ZlX-xjK#n6;f+wJT|H zKb$)!CE=d3klPcrnj^!AQ=#%e6Ey~Ts*_=XJK9f0*2WLg)f#uzR^Du8!Osyq9-~`4 zVD3NNJ8{AkOnRn1-w;)4$>u1rb0KvtbylriSaWE%%&C|hTq{j|o#nPTnp>;Nr5RC; z0bC2A!~9EmY=54O+ZP;dN%iIS=k#UI6kgYccO9Vj)@YkPQglM5Zq=^TA}DnEi{1B@ zm=#(EcpN%jYn?2K+HUpBmcEU*9`?m47$Sgd7U?-we22}Pj?6a>akqy#7h;z?o{8%V z(|^um8`_6)bm`|_Uwf39uOC;Kgv+c}^F9+Qa*{Z9L|F@j04VyiR*(Kv zFo4@vsH@xRK^(TilTxwJoj3{E+nTueI2lEwNH-`8rh_-ktu^kgjqOlSKX3@j8qScY z22-QF-*UfrLsM$BS?Pn1pJqpTs+f9Ac3%tF8?pu?*DS-oJPK)ej3!;m;VZHGqffah zo-Ic7TzwQ~ONELhuoOO^OP(~ z*`L0v211+fM}Rzl)s_;*Pf=3zEUMZ9GiO`XFB+-J0{I{Ux0O|=$aVSZ&&_)9(1>2gEppAS9J!L zW?N4#n4eAnulvyyP#Qd(ktXIJjVtwV08q(NtDlIU9v3F$WJNt0gNqfUG50xXR3k#% zX@vB~I)^qxHFQ3gV1;OzbSYnKC7+=*6>?134{MA8DT4zSon_L;Ryf(OgI2Y^nl07b z?nfp19xc9rMS z>DiX{Y|^U*2WnVFf%$g6PPk>+_c2&|Hr=jv+(Q+lB;%3uYdN{cZK6Ftc4|Am_AF^D zcn@7ieow)+ATx7cBxCFAB**0nnwa zb`;aZc_@OSKxbF)d@vv6)TMkC)jGggpTSis$46F2>H-&655l{N9Oorv8zN!Bh$fXV-PZ#81& z#?K$Fw+Q%yhYDTbuS|l%HwvvI<+ltA(@Kin*5_xoaPiBJX3aqFqWq<9-=KZ}M?3A9 z%~oz<`Tb`y-n61Nr-<O2lg9v>POd|O+t!` z()ZFYv*`}~Y_HPtR;4cVfohvPXa7=eMHAouHt3(gXj+o3=}`>&+OI`w0a-y|eEO9V zB}InZ-S4PO@#fs1M$B9|U|=D7Z}`7w-`42k=v_fg+)FxRO7alYuesGfoUF0aR_Sy5a)4qG4D$b(y1X~hbv^fhC!ev{sMZ=Q(HAXR$o7lR zxj59CYCDhWznaN^3OFB8;I&Q1Rug5e#hVvnn#_+Kgs+a<)!sfV1*kWs{sneY)a`|v zl^M#Dd65T@U#(LTSc={5d^aMpjCG#I-u-6o$33wQ1taHu+FS*nZEf>?pqt)y2fHzw z-q4Ci%k5&VIE2zG?R@Ywg~-)VAMNI+rQ0N^&v6w4*o8mG1q;##{Tarxc%z>C<*LZo zb{0?v=f25^XefFey%+5h|Kqrh#I+cvqKoSYHDIf-FcDZHFuJF#e*>8}^wmKy#vG?5 z{j5rMbpJ?J_NNt~igf9&y*ScftK}DuZ}dZqTf!q?SOA+`ug({*V5I-^`N}!kFYa!gx2`I!xjCuwK4A2dsi zcc(#JMGy2Cing_leTVFt0K6edz~%pw6911YPL_ZTjMpM9Poa<5$J`Tv%?jg1iAIY9 zQw_C3v63rR=T)kibZ`0|Zt3%>fMCLkbSkA)h*PeugSgUUkkmbOI7|@GH!ZO9Mp>Cf z<`0;#%mErLFr|czKij_ZISEH6TlK0jxBv06!h&(%kJ7H2jlv)gJ_K}~VRL=OWIvexLw9p}ElKP< z1IfI)nzl;(bd@+4$6_5vuCln|DTdGVpCY^iM&Xu%GaW)AkE>*f?k5l6AOR z+(BD@*ix6heaz?<;zz3bne9Z!(2@KB?UVag5eymcCx)41>vWws3nZZ~a$x$&R8Nro zZ{hTGn(#s|5A57|JksvSYMW4f2 z3oK@T%>|6})@WUSHD(e0R7z1_yVIz~f-`KTf=z(A;SapZ7 zeB%If@ecc2`-754P6Nt3Upt|FHAj1k@FlPQg&b=3`M7Jy)JeiL-AefywuA)`tj<>WqDA<`{hn#2i4}z~}#@PzUk_N??z6?8I4qY~ts84Ttzh-|r z&J3~G>zox!v&i7a0KaxY!KD6pL;+~({6uJ(s@#%Rg>$A(D^xqmw(r_kVmPMKo|Su> zesaLc_tMpmI*c2{6U z(x@{QlYXTiYn=_Hn2e0-?v4hUEaWIp<#Th_{K9md7isze(>%-H9N2uN`O0qO$F`w( zlFB`)t#Vg+y}z&gz}e|1M7$9?#35s8t|$knX7&V=ng8ZA^uAODo_GErp>lWW z)N5Bdh{$xAX1DJnOgmwHECji*u%sYpWpOd{YWMrCq&7wNJU(k1sw_>mBtbyVP@MYo zddEktYG!@Zr=W*!M-x1}n}`^d&X)2snX%;(P*qE=&yhST=&+vhV7-t08fsN>y69S+ z5xx5QI3MtCp_&$Hz55+1s)yhFhHw5e%egSm*gte%i`SlkVJcTAXHjVY$V}TR>5^=i z|2=Io?3eoE&0j4&V8n8IhJC|zy3OML(Q9tCRzLSrC&vAQssdwRhCS6%nym0YGj!FQ7xIhGU2ZJ*LtSq{`b9B}h-|sU zWXPr^8k(O&+(px|+Todloe(+pA(2Bn!|JM$q6WwZZIREd-}qAQzD>qHU?iVpe^nWL zgEM-9qjvRiU6}bj(4qpAg8~4Y^~ad%od(wBYmk0n8!FL$5ay9}6%)i&GQ?>}h74#% z5g?~rK4tw(W4w?2saAX%W;2w1x5T8?a0F0bWZmFLpmq9wMP&GVvIO~$O5sXIda~w@ zq+pt4d)02wIeCFqr$@Q-S`gsQpcq-_Wb%g6h-Sa4_C3urNWpKTaAGP%j26iO9!M)m zO@*2@s|NXln|6@FTO|C4X^cp!Bu=iC{=2^s(YvW1=9k;|S{nGuhUR^CcpTd)jlLPO z60Z&dXB4k4r8PK3bN2*Mm-v2(ErQbk%OnS3yDCcT#~Ix>crCH8^?ILCLgbzAPW;_# z+%95_FuxqfFry(S!o_xuTL^e!B~k|cVYlBp?s;>m6jWB&+r>eL*k#4$YoW&kgT{Cj`4by`WcQ-)kq3@0qIay@rg-^zRpnse_?Yu?>N?W-` z+)^=j-(oq}JbQUx_fctlrBhdxY*6B3Gqw#2r*7x|6VK8Ujrs=vZkW?;5m1qq33i_6 z`cuIjQxDtz4HIIuW(1xde##>{#+8- zJFu#+?bXd!mM)uc>}eKxd+?b(j7IF9R7LH~<5JwV_lnWhINb-)mijK@IaS3@)gSFF z_LEcP4>AklQlwaw*Dc=y?g2^;j^%Nr`&1T7>Vo!4SNUhUHZ~K#(#Wpm6}PsX)6MD= z)BD3FP>1#d5jn-Qvhk6A1rzpnXrv;nJ$~jwe{HlKJso>+<~(C+F=^tBp>b7+lxt zTeTvu+Z)&dj3FXZp=!dZJ=^q%>1j{XEEQstRS|oS!)AkA1z2t;YIQe<5cwq$_rXXq z0N4~S3zv>l|M?JI!b-%$R)s5Cv!*h{+M4iH62wgmkyJQi&62{n^lGdkpFgo*44wZT ztCaXXN@lh-aIN=8kqtUzZt1QV92%)cCH`odaZ)G1UNJd+Y6HuxLh6i43F&XaL-p;`#zbMW|m=s2Sve%;- zDDMJ%ZfOR^TBA&g72MDYMgd*4EZ`E!L=(>wNDfGoExy~#yy zpJ~Qc(ELs(sJB!e&MB)bCJ_8LW(*qwS6q57WpT?G`zw)_k~tdsH_t5>ir zT_HFUH6b?%)z&>O%B4DYitlB7QM-Rr5I!INCSE}Ndeb4I;cE)7G8Sf3r%sXj%HQYz|s$ZVse$bq9sWE&3>H)h*!Siy9~SvBN5~V5 zPA)1J)@%OF8U?t41r4$8rofDz+{B>oT57Q`{UJ8StX{wj=m5U{Mfh&)_W+tC*ww0; zwu*mj>4MLm%3*pi-*N6>8luAm+`0IR`+gRR3ap6fEd>%mkC|VyrL*{B$Y{ zp6}FioGX9T0Dp#wP4I=OFrR!~0d~)z2K>Zqh3S=QM8s8SfdEbmYzlxN5Uet_NI`-Vm{7^08_Ui?po`s#? zmH)yJ5b}P1wmHyDfH+Ey&R>sPDgJRXnsAM>ugckG62o>K{m|$x5c1-A-GrxHAsxuS zy0v23(+kAl=K)1@)ZM3{ZD@T@&ig8<4>HOC`*o4!V8@drAh3E@F?OK;m7IAch8z=@ z^fm_to9Y+9pewsv{P_3t`5!mE_)h?wq^w?ws>I-bPcgj0_js%R~@}*W2-`^sK0gp6uIp8Fm?~2nf z?TV#9=zm?027H{KX!xEHP;#&Wl2}I~AUQw5h`ak}ISL3G-UoVc;8QX{%4BPKX)UZf zLAVppFg`z2jN^X_n10>w@E%*8K#?d3JsMv{-!1{&BBmxZ70s9nBB z!Olc*aMES_b3U$1(Ltab#{Fc^C+-aoZ_KF~Pt5bg_(C9)TGc%_;W#zugwdkK+ya_$ zcWtyd2jzKjcGy>FrmQPh9s5;LKy^Cl@)g{+Th>+IB)@CuyB8aDsCmgdng2^J<+u7T zhI;@1etCssvqBW`gWCV05ZFil70xMA`B7fbtedtNZ@-aLY zn&^tfSg3KzXZ{XW)A&mmC9B=}Fy)Sit+aOlRcM?1t4T2f8OydWjD2~AQWPW1f*v&u ze1&;_RgC98GW>*OKP85v_yK-jnz~{4Rs&#OHSwo&kdI4956mfb ziUolww>wmNPT#auZwc9DVz}^1&B#&TtN@!hx9fVoKLlQnbENt-tJ#b7`UZ3iRvHne zB$PF`n&&5q<-A&9B$at63SwK-Ba)g7+T+CV%)>+~ltKjJ64aFER$BVZtT|?v70v zr(xobG(Z{PJ4U}%uktuvxhMGZhtvX4*9!$i_Z>iR(>sjg!zH%1)K3~6I%SJVK_9+= z2h9RFdNXU;xAq%JgrA#s$7eCIY`<^L1swV>r(5`7tWT?7mdVa&k2Kz zqx|1uWr97eh75w0jF|QT<(Jpajcn8qrv|zeV9U}x&W;P3DRtVEgf~(f5O6HoJ;1UN zjw~5{hGAtdH0z-(GxGhK12`x!CUOJ_Y4P^eT%4cmZdPs)jDM>E##8&?PS9h?LO^jH z^mB7U*e&{XZNDaDkJoG1x<>@ebF)eTJ%FC4$2%HCIF7x+ZFcz6dt{>S9gdhKF>pb$8dN5VEc%TB zBfd(d0}P|Cy$<%0N{Z}f20bU7$c`_#`=o(htZ2aN!2wP}0>OjEot+b8>13b7!A>i^ z0#ds;ypezGO$y2=WwsU8_z;K=usr2JeS?Xe|C)<60bIp~RQ}Q&HwsTGGuI8agmO3i}3TD`2aOK2s=sC;mS+$-n(JBIuGI zGy>J3X@D_bnWw;9<t3n^!MHg z?S1E6h|tgr0cxbh`*L3zpM`@`1E?Y5~Pdpt|4+A{~Bq2khl0#@TGL#E(HQGe>Q z2sUaIFXt>v+?(WNrhuTRd5@$LsB)NiLLDY3)?Zk=87dI|Im)wU^^A-6XlU5w&Kuwkx%a zB~tIqcbdqRReQs%w$H!1IZirf5PYlZsv!7w>tcaW&QlRRy`)2&^R3Oyt+J&o1DO z!^Yx{Cq+cmkKW`^koagOLxLgXe~233f}rYDugi*>3YhU39CIjz{;^U+)4Tn_aYr=BflKd_jLFA zN>poDPdKz&cR5*BbH0&Zvwh62U$GeVf)F0nfRGtjqRM6^NUoyM!lCSBRIkInv0Gnu=M86rgsX$%N<_yOFu z7qjqy2)18YWXM(NAj)YbAhZ7Mm^@j7mZ|~0(w_l!^_?bWdRnAFfc``i^>5GZfBWH! zBm5U->PKr|%ziuMf1K%*j%gY)tMloP@L$_AS%eATfpk8DE92$&0m*4Z1F!Jork7;J z5FR&G{US`aV2 zztm|0Kfx27zCn^_g?kuG7AbMb)JpdnskeaUs}m65JkruE!)15NYw_Q!F9+%|9CuC_ zrQTzsxaWMK@|O%55h+?RoaCB26$_C%syD6@LddeT-kERuk}Lgiz<W@=6jFV%K zN3!UDw710W4}@nFkO30Uae8lKyAI>dsQ!8aPbBMM6vJED#W&%E6!JjacA8gbDNq9W zw@EChW8uX1JWksNBS4k6QETZ2it)0q&r-atlB_l!A!DTUJ0~(GN$jFV9}*#XgZyh7 zByo8Y<>yA~!*?aDq zeUUE1g{qK|OFZ5KN|aeRMcerd9F^M*goDZCh{swFdKCfl_Mm+_tdx0NvH|M`mahhx znpP(33=Os#Sz>+YK#=ezD_YMLNpU}yxk+ZOHCRL92awIVo%--0Ew=V_aR*xt_6Y;_ zC{WwjKO|Rv!Uzr!{=rBGpeR-=!tT^HoT1^Us$ZoG!YF7cT<2iq@_}2_d*kF&qNh73 zOqzITpQ}>cdjUv0LolwzSw}uWNjZ)aRPf)_(6m468X>Tn$18EpgjwL426`@c2?7ggydgjj?x{ zEb$`g5zU{edDEl6TzhM9xd$8eb^~kY<{zxyKWrTp#@1=L=NDlfNuDGo60%Xf!|__e zO*OHhSYXmgN|(Q3!XVyNrR;FMKOHy3yDk3Gi- zYm0qA%|RHrd@$sWeUqJHAxqd$21W7r!xE*pFg)Bo!w)R$y=n zc%P6cEm&o(?^iPQ^`Ixa@(Z8c$-X6X9&LPA_OUOrAD|e;@q-!)oho94Yya~rA_@~g zlx}L;{0<UGIJZ&Jphb>1IP+x5dc~?fvB%uESn+iq7)1b(TFlhcz)e=OPgO&1lMteMe zCYEx8gb{U~zXfzFS|ul5HdNed0EGQ0^>Uz|jjCld_YEKzYMkD*4O8Ur-8iS$boaUS z0`H8B!Oka8yDZ-~X9Ps;$Gx_wI5@HMY}woT(l+(chT_l%-)jz;LqhD^5}rVsE9P>-Mr=p_TLwt85qu9)S3luk(1pqh}8zE@EOAI;F_HQd=i z14S?bLbR7}U&X6E47^q9`;JJm0CE}i$Z!NWDNCBcn}@4|3S8y$p~Lep_YYB~&FRr+Kh&8>nZ6`z6#qoAP3@?(>hq|-bFc|$vhg9n;fAg~sk!m=eDh>vUk*}<4H zCvql!8pX%?b_}N}~&YRC+Ho!aXa} z`SA?z21@4IEdO7uQ9Ndi;`durey>p)%kMR+0lQt#VZ?;W>ryt5?dCE6@fCq{G3^5& zk|{wd`PVo!>*<&zug{DEecbsUQlu1RwZ038yI@OoePn|8uoBJYeHNJ11afEOfh)04 zR0zn*^Z~;jZJ9v=$gH&Q`z#a97Cv@N5?;^q!BotsQIZ!&OY|cSW3WNad(zf$TN;XK zls%}heE@dQvz5*f=+&L6G35E=^!DMAiEaR;Hmrv_Tv6c;RqVM{JY?ndG&-W+s{EiY z)%Tt?JV=6Of;|KR&#HJ3l)H4La5l#3Kd$qh5IG!AiMtOW?4Pmzi^ZwJEKZeuYv?}~ zhZ(atSrvg?74n!shjV*-rV(fJIJp5_8*?kI?I-aYgPzXm0mXu5*ozK%;dZyvWE@2_Utw)?q@-JtGje;Y< z8p#1^+LTS%#fUnY^sCr?BNY8BOjEL|Y}dC=ooyy7}s1<$MWZh5!oKpOY2 zTHnWJPau9Zc*Y?eo=0R(i5)2OlRrEP4g*!E-CuLT3Lp|$Kviz;KHJ{1qo&qW|nqFu%=v4cs=YIa4kX z2O}=4%7%3VrIxs=U10p7!U_U(@CGtpN+H$kwxh{c0u&5B$Nh}VAEa-8 zXRpCerb$XbqRKYvbCW1?vm5YgpAjF%8qSOYos=}si*tZ8YciNtFEmx8pqPnQ7CW=^ zx`}VmkB5FibuS+7{jnG0q$&8mZ7_ZnN$zHB@OiuuW0u%!^E#=V-;bgUI7H6I;*Hd& zVMP!!QASRPUjy^cFv{fTMfV65qPQXodqj28l@hWy=#yLAB%Sfm-)C=+vT}qJ zgQz6M$%1;(mN=pWPK+aWBCR7?DIs1dK?ly8YXOL0V5Md6MbCb|B0z_eUHqX42C7SS zA06l2wkPL zi-VQ)P95CKf)lR*oyBWqb-w$&0Sy?*KCL_hFM@QqTR<2PNrw8jcWSG~!-<*LHGdt0 z8W5kD=7={RyXK8f3Q$1If#rQ(Hl;%KCS2AL(Km|!IssGW`<_h*N+#kl_AKd zjAZ`;L~?|_ff#9=kE}Cn#X#mT0Cicih`0N(l%Eorh*d#5%c_IT=6E?4RS}t*&$!&b)w=L1PL z*cOJVKznt_U{VV;eO6BFr|`XB1Fw{`9<+&F(QF*G>Ev7(x9$0C*BdQchATmWcvjz) zia!T`9*}h~<>^`6;Aby3f2ZODIf{{9^OH}S0G|1@3lB%dZty~GR0as z{1e2ou>6Q8K+$>Twz{_ACkBPo)Yust>^B@}qLMuY%9m7)PR?Vv&nkMP})j{^a zM&o(>hvX2-hQ7EDraVjt12KboqSuU?NJ&K< zXZ3XV5y=t^QS4ty3@?*J{(OOQ>$U*AwwAr5L!At<*&LVcFQL=PY+CZ4Hs*clra!9# zrKOyq1BN1q=h;4CmJR>Vf1rWN6@fwqY2H;yf@$njuO_OOk&`7Qp|>&W9;gudXh&|8~I5m;~*0*DFZ7j| zHiECO+&zIJ#nkDyxyr)=r7;)|3X({JS&X}PFQKb6bnPQP(Tj=lm=s$M&W9>+h1?c# zk=|UA{qrUOr|s$2K0-8uX65EL=hqFz$g#npSf}lRE_Soec>}*M+Y_%1| zh~ZQn+re1xC|Tsx%85SK;LICQ!W(AQ;CCwX}>OK+0^=H~_!gxbMrMcv%8SjvGaRYt&k)3p;H7vwKNam|s6KQt5x$nj) zJAZhf7-Jj&;sKX8^NPYUE}>hb;YQsiCJvC8Bdf$ zdf5U%Iz@q<=+FGGTl}wU`JaB`g~j;dzAqF`=rSR-hpm8(H{kL{P5F4=GI@;T_Nmo7 z@{<;lk>{RLRVCYFQ}M$aYX-H}37o^4UhGl27rxJ(j3SAIJV-}i89~j@c2DR%+x5<5 z+w?sz?v(1iEh;jv(93^J-*cvYoa7nxX1FZzQ_X7G>k|c|E7;FB97GjLmVZF>s9p6e zjI|7%zn@M?t}H~K=l!DMY;@2miQI8a(%M`Qv|YYg4*Q;?Qmsv4fzEylKdmT+%13qS z;CB?8w9cZegx76?+-kO6j}|B5(zFNpyzSDeRaoPnom55Z|Nnh#KR~{t>1uI&=w;Bw zf#`}YRh5Ch!+jXX4S_;xE2=!!YANgUNF_@oQcNkxa~YB4RLw3je!?wEUwFA(>$FZs zC1iDNobq2kkL8z>Q6GNWJaYBfMWgGDK-;w|xouie!4V!;H}9hLoXqpj(((;uuA}Ij zTadfI_N?1qUR`JP2^y24L2AgUgLB(g7tR`79aN*uqLhHNc>eC7M@(k7HOUj_DBg2d z)Mg0Z6}L8DMZ2LmjH^r`z4jtz{NbtQ%CJM@C!=Y(Z*VsC2UePlAc858RK26@{TIjX zU&VJ7a#ndzdNTv3?|K<_^GH22t1tB2K~tUAPbj~#l>KkvNi z)nz;_(afoBsPF7oPFNG6=qRkrnN`i}VV>WpXV-OyXSaLF-ZgkbAVkheuvjy`QMpnp z(QSvW{G#H#54z-u?4k%%Z6Ib3g(Le>OC4@z`EL$#G@dpd7c0oKxZ9AUs^J%cea`)q z60%blWvY6Kry)eQck1TRD;VFFXYB1ixQ*nO}TqC^oIu$M3$9TzFQ&M}%!zGp39;(xP9eMut{zP#?~l zkhan!B^+K9nWT$j=Xle%N2|5HaJXChL8B9>N1KEjRf9Jls9_RL^-;7ocjeKphnJJpk+o-AN>6p#P@`Uws2o3}HqmaQPyN7tX&5DF z^Q7QkTmWiwpXS82n{IfG4ZI2(l^L>rp&>i5@BQ5Q#M5Q-@bi<00j@oF!UihFuNG3t zNARhm-GatFuZ|R+q)Wt?MVu*DDH(JTSptw}v}%c(nIN_e;Z)Y^E3yEDyx@4bpq zS9U?~`?ajGA8vx~<;!x;PN#MYz9sz~3x#Luw>tH@SM@U-Vo-8s=hokdZn~91Jz?>c zV+jidFO}PxED9H>&|FJOx3iWOO6y6NIKEW2D2A4b#k1df1d-F7zdh=zb9A!hIezfL zCaO|z(R~M5WI6Pr!WymAEo@<}1uItLqd)OEa2!O=nRMvTcgk)4%0r=R_HkETiea7m zoHS7s(qIP?VD&U&{N-Q1#J$K^on~U+?lzm{W#UqOqda)*?lP@D5bS^E0SOqposh>G z09|3%F2lXnYk0*v(uib}O+z0ZK%sEk#U-28v763jBfJbFVn%O#4 zlUIP*H1lEm$i0IOGX45$*6~kFkE~Vnn~mwepF_@%GUR3S4)t7LzyD#DWRx3%Z~Z+q z5O&7w`~a^@>B$>2!PR$kMt;z+c-GB#=P9XH8^!Z6mAEVU@aMEkd0qlpPF9}> zHNq6f1R{D8T)VAn^qKh3*<9S?jt=jgHde=4I@8c1T=(clUbTqsr;GazzVSKkGuex@ zY5a00B#qYffR1M~CW}pcv;}st>7S;Qx*v0TaFS%x^47J@FXLdy>8>EyC}@EtL#fUmKzJXF=A#V1;5Sm_t# za!xC!m|!^aq&_O=>vr*5svQxJ+BoY;%?PZ`VZXw0wILRA=N=;GRnGgWonOt`Tu>j4MAtkB zeDJZwTGM9`u$O*6|ET~|zQZl_T-0vKm!I_VXm$>YiC0XI)wAL;S@DF^$jeyLbMdbM zr=szDeg(tEbHor){rlv2x3?UgIE-Z(ktMM&D1&XcYu0_|f3xivudxEVHnd^Mo@2r5 z*~jc5fYR5Mjql?(xVb?2h(lT`e(iQ_1{42X-(_N~QlG1}IY+*8S7W0337TZON^fOG z=1#iJhE8X;Cb?!jS(b+MC*5XJyp_wYSQ($)kas^=_ePW8@%OR$5VMR{xPlnUjPsWCY#=Gs!K$={eN2PVzET8a;@aE$7?$AMx zdytGxMMmwhuf0N)y12nkn#gnsTDv8+)+`zBb+^-_($pk&UTFpLR$}$HH%sC!5v}Es zIn=kGWY!VaUSnt&b(m66t>Qc1m;(mtsn<=i+5DwV5%xUIsGRX$=3kCbE)WxFnNxx1 z7t9eY^z4NI9@tfPddk#BB;PqIU3khknGWS-uq$5)2M~= zF(6BRT9)gl`SvtttDN7V5V6tpfm=8vsaS*@HS>$?&Ljw4iV;gut)mRmc5_=oym_Ms zv#F#DUoWGfs`G&*xHB{j!t~u?B0*Ih_{10C-06C`SWn*g+c((07hydk8GnkRb_*D~ zQ&P)glKxZBBUNEalSS=t+_YF(4+KAT+{>R`>{DMU$K;Tka+o202MZPVH+{L zU)MQ55xm$L*loW&)a{o^Z}1^|Z<4A56EJXROp)Q`J%4ngWZcxE5rnENKIX79T>Am{ z`@ZIsk(?L0LsORT9yxH`&Jc0_fY7SW-sofIsF%RTRj~Bms7$cpy?k>#M%1-Oi+X2g zuH<1Z0puyk6Bmu8nx7Y(-G=lCPl;3XaQRKY&Q1uUQklcdU~rp#N|Ht=jLi7xax%w* z*vIxIPdrX6`j8636A3xv9=B)qAq{-&q~kfFZ%XH@h;d8YfxZAT)DUb&EH7YxK5)wzZ|wP_pX1eLIWh=1jeeQK zNX$vjAY0jWKZUmAHAKQSIx7Z8k@aUQm5x;9ApD^|*}kHl%fP>jtckg_?q+7OfgnHN zh{TmFp9sE*THO`#eq!KxZ=x zdoV-OymDtRcL_iIofaABdHUQi&@7Ds(A@$FFScN=OZP{gZx6NpUadU4NM%vMWQ z)B>D|=arkYjSFpOj;sz(%I19^_fd_S*BsF`CSZP6JhC)d<@9y75uprQVaSfX- z0#5vnK~L)4nlfPxPD!sa_ZPW)936QD{AKzI_N10OFfI88K62N6u_}Ma+XFAl5TeL- zx4@TVkLnvny{d&eVU0l{C%5A`uHa5_Y6vq@NgLqNe?Nve413DIH*f1kM>blyJi`QR z<-6lPpx09pXO4$@HdX?eO2$LCd%YVNT$p`UR7JM;ma(JO^%Y##P!lrZ5zT>ETd#b_%e%X&hPm_^Nvd|KBQpV zzt6Lph%M+^M5@l0JN9(l;jEE~_%POi8MHHLjTFR`z5engZo=?l#TSaZ|CMkuguC`c zm(rsWA^@8}l~i!}y1wx3-dMxKhH19=NnS_Qr_H?|i)ZSJ8;C;Fm%m`Jj6FX$!%3sm<2>uBRrsAi9F8xyGsF|1QhK0m$@txIje6aV{Uw;SI)0JiS-EGf zSzBUf-Y;zlhA{@R%hNwre5on&g{p5pB_)jvf(iT#1R5%r`A2Qu8Q?n>WhU)XqP{Vh z-wI3bihSGiJnU)zl6hS7K`c})>I5i6Wxz&f?@8fcI`7_fdwKtxqs4&DhJ<(h@|*B4 zfvLX&1u}*+7ruPf{7&#VzJ0k$S|*X0^vmnJ%L3a{6T4a>ouIQLESMML(7zSr&U1B$ir9B(`O6@gslD0^0yn0+<%vw`{#a zS<*7)p8lrPWNp}JI0(!O9Y}=Zz8(D@<*zXJ-k#GX?-8M+8EvV51z~g0qlFS8#Y>b6 zH2t&EoQ(~|GE92qMxdJ%ogSFmwt-E8P;( zAf>?2Fm!{ofHK6;B_Q4XE$;hy-fjE-z3+ce=9+7*b*|&wkK=?*>ECUww5wV6JdA68 zI4coZk1SkPexT56_4dWmadK;=LqPwt=>O$906+E=z^VJKPfU`Y2gI|6(-w2aS4XDO z`*V9do${KV8!dj|nNNfGsq!F@FZ8_~QHXHKsPj}upn3cFhhoB|g&IS#xe1D4(+ef7 zRBvIe#4#z?mg@KYMJh!PlLQUtlD4KD+ricCM}Ak1M)~dC@8rcTF%+%U1B|dA+NEf<6(c3LML+E=T8WHwaCQA5RgRI}Dux5eguZmva8QupeiJ_+7dXDht*2@AH` znzN~>@&?zn{0_x4Q0#}y4SfB-6#JP#y6yknUf)$mC5!MBy`moXjNH?(#>yO(hw6We zA31Mrp)lRsxZM+z)BBrZ0Wq*NXCNVX^!3NKURrb}S}HTX+5okca!2cHo`Gkv?xXe5 zoK#T!G;MN63y0egnTcD^_V4cZZuM;Hi*+Zis}cmrc+j}VDS54%h*ZuX9abNl;VeBn zVIZxu!pJ&rxi8~;tTn=JmE82-qQuMcv@(AY#J4YT^4#5}^=x&z7BnmJ1_YNxxbBHq8fL)}V=J{2jQ{Hn75Nz|v{w_VTzwxX+@&RPGF;$oVW zN)*CnobknJcfH*3ZyluGWc&mvG?zAuU7SW>*ch%NPh{lC2PnRkK4IML!i6ZZ|m75|#VniY%WeHv9c z^XN#_QPfcSk3x6FRK1~3eDj~H+-bjxGgfZdS9$LICK=9^WBPKsm=hf+&~leEzCTs` z71%A`Jr~DwHKP5SnV0vnq4r76x**(aBkA|j???P*(^NAGb(-Ng+$^R2GN36se7U=B zyJk#IIX$r2l#t>vY>>31Dg}+DLqHR*L>S09%ivqsW+h%vEZ3~3Zo9^V%gF1|@9N46 zD`YRS}(Vl3u(|O zt8(&J%?&$?Z9+nHUn`_=%z3;%j<53cS(0_iyFLT@I$B;B*9v?B`Au==)jPy)PHoi3 zI3aE0Ym>~}0 zwL{iF{4^`uPncQsRcGs_n-aC0CY{xHUgzhtU!$q?`WR07{Xon}{(;ju-K&D-E5gm) zyFM*RXv>6Ys4Zgq3&B{FA<&J@t|Y4BTk;O$A6uPUS>Bl}|7h*UnO%3a^XRc%(&2R8 zt-F-%-jncg-D}Nfwcau`tZ?s<>+@S%@|O7MsHJ(9rk>!H@OZ76#79k6I|+0+n%hV^ z&!B4o=lumYC+w^@cya)88702mPL14*FQB*WF`>d?>FdC^v9blDWBREz$7T<~ErDy% zM?y?-rlAh|#3%<4Kf=qQv67!#nCM(&x{GnjohE$?_K3-WvZVZghRdd-*{R>pQ%IvyJj%T!Xm^S%lbgOWQyr`%ORFoahCZW6@Q0e!{viDN^HG+4I?V0$QHlQk>`ghQ|$s zGZ`w=^U_iXo#HE?aP8KVmsLr()jARBj+T%ws?O=&(#D}Y08W)Cfb4az6|AsP4pbjs zbCaC$8WDO z>KyoFS!smf=)q@mXUfoP#}+^YRrizhfI1So0j(fXUkYCrBPjaIA>%%bu^O31YhfFh zA&CFf8F*hfH`v;gc2JrjgqIER@y-DU!e@gK)^VM3HUp-iLTnb~xg2a9*(P9;yK9lq z8_z{fgA13^V_!V&ac+VEMp-FYP+9WqFTdqz;TK>#(o=o%-a0w_=1~qCkJq`|3HVrM z0RBm+9DRHu)`}8UHf%kA72oybi>g9a=RwUd;Mmmx8p?ffdJyY8s>7z`wD}(@z&8b( zKo|9CxJ_oF)i$9QbVpaatwBvLfHZ7K^cWPLdx-IqIse3RE8u;}Y!x5g=zhRS;B{w- zAPQUoaI_!kgb2KC#co;bO%ZW4-I?!D`J*eRTjeyrHITXl+#<2STp;hn6?<<7d zQ%Sw7lug?zb?nf)yUr1^1AwPT#r3-(5fU~44TouyZ9&&RyoAw1Z+4pBH_0La8qG*Z z4ONkm=GPu-wCg>kOeTU>dj$49o0m$n;FqI;WmoN*Wfocb{F*G!+>avihAb(-F6tt7 zfZ1c3KuX$^-EFs{dOW;5>3h+5~EbmWob69$wgfPF3zhBbgUS;v(U zZpkl&my#DVQ`M?oSe?jPskfV7hBzD?a7k7Y!)gGyX|@n9G{C9dS1h)PdS+9*%8D*ineBt znLd82hBloSU>p4S-NBkWSbt+(B8jyQwt)mk=SvdJgB-E|dy2P1TpI`Jxy3`VOAw0S zbbtsAXFkJ~K6~67Rm26=E_pg2aRh7W4FU!$3HIlh3(@`tfgcrjw6^^Yfup5Q_||6c zapXF$HPQLs^1;#K!)W?V7(d^h*Ha$?%@UNL!{Uo?nng*~)r$o=I))`mJ~w*bQ9Q`H z-*h-(DQJp(=>m<>Xl3zpYa>dTdGX-rstJ9~vF* zQ(p3Fu`qyQ1AJz=0Se{kZ;o)xqRHW_mjJbwGBHeo3^;#UUNSh$RWQgBuyN3cya4R? zQfj1QdaflV6d5L9GtA0Y44hk~Ka_hYYM{!_SBRsjglJ^F*EP8G!Sjg1dOFZ4_El16 zgZO|L5mQWah*Gl--7hO+z@rAMjNto(U>0qkKin7lV?IQ3(xUvCs;1a06odVJ)73Da zss>tn!M;?Mc6h2fg8tl#ui@w4UKw1*{voM~#uc1qP_9tN9=5@2Xt%fk(5t*w`zXi$ zr2IRs@$V3sP=X_QGwuDcLc(x+ce`$vBAC;V+=O7B(igJc!YG$}#_l7(7`hHV{`yW( z$2pEPG;euXJOiAHNF`N9@ICK0_ld{cSjOUsCmAH9-@W$ModKgm$&ZSqh9UgcgXzd! z(SjWvl5`gXCT@L9OW97SVl3RlT|aQVM-A_+%${q*CD{BIQoruKo-4$XvL;d{GzImj zc^iP0lyf?i8#X^Pq+47H?>QjMJcwQ9D;UimH2etOtPTdPmO>L z2K9qoF`o0$1VF$;Q>g2*482ZR_*5FH!FIzo>F)tMiEW%M^W@p_!ADLvcueLx=;z*z zHBBzzK)3E0F@v{^&TaPQ1~qU|Y|65PBpvsg^N~Ss{G@6EuBhM$t%uPg23MA+NyF@0 zAyM*@@T`gfer3IgI>Wr0g!oZ+=Kj1g0`$xB#^E537>5BiFPKLU8(gRT5efG=GOp*a zHJ`LMIhR-j2@NfEHH(1f>Wp7MVtYHhXe3rfIoLrV3t%;l9yp8C42hMl7 zYb1W)Vrk`{mwpKYw2r z6S90P_i8T7s`9rcxlk#SP1(z9J0@p=_40n~qJP+2);cg_zfMTC>&7iVgilscn2V@j z5;9s!w`$k9-4A|ibqr+cl)ozj7XFt23N!_tM(O%|3Zc7+zcnlb(f3`Dc3dR@#FRBl zFYU+2?m+bCJMVxBvw->*O5oa6nu9nyd|bLwWf_{iG51wT-nUnh==wiqwVsg2uSAK-J1zp<1VNEdSr<`3s6nt|+v zVZr6b_av|<+Az+y?;-AWP#GLn6`94eu|Q`bk;LeGz-OV8MC=^0PI+;AjcrZ$(bnk~ zp0|DmQc~yJ{0;ruepVB z#eg)73EbJ*=gCjghd@E8UG1Sz?ERU+(lFL`lDpT%=M3xAcAu7guy!3;0H@we9?EUI zxiiFS&y~s5W+u2sa1@I=R@gm?4774=_PKdYock*N02Pphp|3D7Zug-`*j8Vkn~Z&s zgg@B4Op()Y*7>mf#awsTc#}UCqqx_QzA~xlC&L13IS5!MIO2s-*|%(>U+1a(cJc9| zK?qMs=c%Ob8>p4P>knJp{8&_r+tn~i!m?Tnmj3MR6#xkjpFQkBBlX3&SSR+u7ya;_ zh{7aP0dW%6uU}Rkz&5~FMB_H=J8y7l`Q_YX)&SSFJ)a6gJ*D_WWFd^-$uqzu)FZ=V z=#`+Bi=n#f%)u=?zqRTTKRJ3q)7gNgeK09(Wc*Lm}0D zt~BBMUhZ&D2-=n2E5;?x=}wF-Z5GX(TH3+~jIF(Rh2{Q|+hwyhDV*I=!u4zCxxG{H_Rt3vi9trxeEcX?et7CcSixV1)yKy6(T z9pEOse-U_s8`j@~4fi>(AMiBbAohiA2+5 zoD7)KorH}f&kK#f_cXj>Bu;K3i4CV5)BpxytI`$dm1ui?00cPC=-dJLjRU`LVzpop zf5duqd?|_~sWAKHdzL>m!=kCuhcioa;UgGu^6(cm%>GlC(up0HLfw9^-@$b_&e;>5 zcIQYJS}LC6pj{K>GN9joTa2#}x*OL=nDd->B~C8e@-!{s?w1cR$^M1WEwfygxCqKl z@gztL20E0Rf~R^WxcJslWYgb3dQv{<;iR!gBSK>SbYa@sQ6^_fb#}}vD5+`tXyE}4 zqO?4pZL`gM$_ycZadX+4o{B`Sry~x(L_qAXYQP~CxOxw8?nWL-1G7g`LXm6xvC+Q_ha%P3p z+)T4rmF+`sBBbRY*~Ngl*V2yapDt4K%cmKy2+@UJ^hV_+hSi_$6+XDbeeE>NczSI& z&Y&QFJgT9k&tlRQ@w*!oXmy;g@b0}NJ9pBQUId<)(HXaz$OP`h3^_YP(>2qG%i}Fo z%%aM$XUh187D5-Lj6z@t7mZ%TB&~%q&=8PQlisF(+WxT{7Y|#>)4cUbifxrz3>A#@ z=3IwR01N4|DPiKT7VP^0d~RQSx@lojiHQZemEZ3`B!j`n8WG3(K|Hs8QN{WiZh_q|2(M+4JJfF zZW1Q`^wTDAmo^HZB6R!Whc8=lgIwxqMKHcbFUd|R(JjI3t^k;uL!YYcdcK&_1Ox5b z9p?za71(<`gW&}hzpG)Kl~5`vQfKKfTy^@0z+heTi=SoTFdJ}Ep2nDhKW7Nv7OxX{ zR|-;qOq0V{#k_&P5=6y^vZ25IG2=E=d&tXP*`&<-+O{>35iFzjB1o6vsVuc@olHZP zzEAbc?G`L_C-7mL6;+N7D?qbbc!xH3hOuMKDIaHfZTsC^dYbOnEoUXqPJXHgHtQ@_ zFZHI`WM$MRDot>Nu=4OGa%CWxq?FdO8hk}q;S6u2o1YlPuEnb+twSth51*V=Y?N8! z{!#sDD^gHA(P*dKmnx<;$$Cn%pGT^tx4p$S7HLI==0#do$SGZ(&R1riOxjxqH!cIY zW>hfJVeUR^n)D}CX8N-i6L}U{Khr%Y_>fXTQ9G|F7#RrX&YT;MhMAB2Jtsoq*!qW0 z^F2RWk#5h)L1nxC9H~j+k;={CfD!eSE*N`xij~<*i_Gi`4a-rgc8SdbuZ=EsNpdGJ zu#3tBPhrwaVT}WmX6G69n*MfYC^JUt?%(@v>cLbDq@Ncac536GRTetKD6=qV*4QNJq(%ETf(ZPTzD%ej}yOMm!U~eLYmJT6&|AXg7*0 zU9#O)Y|c4Mg!gtkFEED{T?B=%4sG&uR@mbX&(j5y1gP0~(gZY8n6U554S+mt$Nndx zqZJEdj(BO0cC3ST#L`%JU1XXsZm-NrrR(T=2S?V~Ni2LBuA$g>T@_6mVNiSKei3OD zBQDSQHRdoGHMk(o0Q1ND?akd>8D^gXeSYIvklnUY#2L?~Djw<3F$B*@Z}{I$+T3l^ z?*?`&mpId3U=evNddrqP;>@C=x9GFj#d?=VS{)D1+IAPt6Ef}aOW@0(f1~} zChn!Va*a6$?f64sZKx@ofKhX9bJ~|Hq*1wrTgsZq?O`3-SfI7&(Ywjiocl;(n<$5A zsA)>k6HU-`e#)=jPJ9GGT2AH|66>W4+h?V?OjwF)wuq|?lWG~}CxCmsKC5z4HsW@g zCGYgJrVt;Ktm0Cis)oG*F{|`5U{wXCpx`rT=iUqd3d)#iD6eE2KV*X5uYV<6NYC7d}1vb^Z~83#UR;b_xnsV@1<11DVT4?7@@wpCS6_AzNb% zPD`=`0MGYlZezrFg>L)OZPL!d5!}l(J2a#$Fh#z+DAiQf8c1$U;0Zb#AHDm9DrkYS zk``fxJhA;{GQ2|ii)ndA-NpmSxmy3?%}Gu8KM1)I3!|Fb^I2`i|6H9?K?SZE@z4eO zDg>j*{^Wv~{BNYxrCW3|X^)PZP_QO;>@H9A7VCHaW0e4n#YR4;L+pf{%`LvMh&Xn~ zQ_Ed@C{{(i3MyJ%lTGch1G!Eu)YDq1=KJK^PZBdxqd8F?>Kcoz>7O}99Y0>D(ey62 zD@(s&t9a}HHFT;={z+10vIMSwzc)5NzKAB5$2r5uz8C@mZt=mb1b4RBn<-Dy8GqdzK7o_MFag;%UA#kwq;}yx{PSXS@n$)_A>Sc1n1(;Yd><#3 zp5J8h$}n__ZF=CbYk%dl;B#|qk-%;v47G`)+W90{l5_*h>}g$f)IVy%;`>RnDdNKE z6uJHkE`O|3Y&I7z2S%>8EJdpY8$PkGst)l)pTBy{CD^AcBzbqX)I-K6WRoO2dTYnN z$BBIB(o6qqAa0r+T5HH8Q{`bfd?t;oic!zFCxz*SH>jv3jQgb)kSkJ%@T!phZYX^0 zFxo6Y5*B`bL+Hhym2)8 zG!0Z@Z>7};*nUF!B2@71S$Dlm@_>L_$;6)sH_DcBp(+Mt^1Mz&PgjHrb31sFp)iaIBK5Y1}P*HKn!@T4(q0dxi z{6Zmaqc^44T!tKX(#=Q2tLIb7aLnOqkqewcxI-yVVbvYKgQxTsmS4;-KXJ$*B-oyL zDd?qApR>s_WW%PENE$Z7`_2xcYI)K^VRDnug;w2!92M_W<4=szjrtyyMXa!7r^;AlZM6r%ZnNm}*Pow?2KV+{;)k6I-#L0*Hg4C3BZi8PH*Q>z zE#I>Dgpps?J0^+X&Q|0X%L_T0f92Sq0%>v1&v&fA3{=0S$Tly%8p1TWuS zylULzmX<;Or`6`e)dHYj7C(X~>;n7!RH_&{j%!t+&1g#!+rU8m*{-aQpv=J%s>_+1<`a;rC z%*7o|(qm3;uWp=fcXBaDhQ&s^?U*BYCY~dG5@;>b)!|(_j%pE?p`8_I@1#an ztx4{6m)Sy>KPL;4_lPa@X5YnbT4?1%R}QnaAwCtO31UY&(wYjpBxG`hrdH&Zr=NqK zVJDYWnt9;E9=29yANj-JEW*B@ITSuxT}q4)TS8fQ7`6i0rQcZYYY9KM9Mwueu_<3) zg5|=m{)%5Dl(^ifpFJ>~_X|Csb!)L|HMf?x)4ac5>uR%cIki1AlmDR=Da@_VMf(7k z@Z!hH$7sjYdoMMzWppqcJFSBx3dK;VqNcQWG;k;iG**Wz3It3@Q&eHJrz;N)ppo_x z2HEpq>I7ZW9k+KfQ!+ktfdWSp6-UnG$fBTy)7@GxWglO!{!Vf)#JmZ~ukx#~7$?l9 z^Rfem%bjy)>T22F&{JhR68d8wp4&HX-K@~^EF>d#OKWuxz-c+8nJ47uDk>L1T6*$E zZxBK^E|X+Y@rxp4T^vPTsBZR+O5KUx2j+spo}Jec$}m_y*0tr%U74 zZKkKvWB`PtFJP%sOPuw0W*3i|mrGZEWfV-sdATUY+W6LznbV=Hk0G~tQGV>@Yc~fr zWl7MItcvG)@7Edusq8zPrK5mMIyB5Wu#Se(TQqxXoA`DJkk14ZWp>aqp$v3XGJ#W_`KS*PpMsz@89gNbPc*r1z46O-lg~}mm@W28~eHO zw|{E`F3w(M-+NLv!1?Yu_Kje-@7@QnM`avU41^U^*9C&URyco*f>T#g(* zshM!msm7|K+cBn$ZB{paxwp=+&+gZTG{*sv2Rd^i*cY(~i^Z*q zNc8;JvHeK>zUnTGPSOQy4;-O^x@wjrXKFrGd(-R&;Y6&jjczf!+l2Q_R?T5C1vT1V z;Yl%xR>z8Z&kn9=i*m>hUk(mu&J41dE|@Q{d3sV%D9NstSFe+{(oqYw4AZPh(Ddw0 z-;)&d5)(?+y&^^H_{?Gr4wKr>CGFz8Em;gYoZl;P-0xEQ!9k4{*ck4iGDvcE>g&$N z_d@7=s7pf-`c@6bAD0kjaTlNkS}VcX`k6sxF#rL2YSxj2>Iej~v{K(Z=eKHm91%iv zf=1G`?CO}Ee6L0j61e_Lb}&A&-|-{P`{SVdCntNN@vQX~?mxC0I_JlQ@lQ@@td$Z> zZSdXS6}%RE=k~x^O39I=+^S41!0JRY&`18WUH>xY?iW{l{-yth)oA13X)+Gxi);LQ zSGYw#i{VlLIx;~9GpEd}TlWQp6Vm5d$J>=-g2}(X0BfWJaqtMV{K=29^ir&3`&}*$ z(2;%9%8&oJ9K|Sp*D6M?H+(oCvvxJK0GzGHtEC0j^LBjecIEsaN2&+zfAhmP<-$=4 zckQ`}bAX7dt{wt+B=?t^nLJ=JeFJeCtQHPv;=ly|J*0w~5RO1eV*k>a#&EYDnqYfkn1TiO3O*{`I zrZoki!CkhbSP-mfM$=Yvi$W-v#is1lc$MYIb4>3`d&(F-5G}LLiV=V#1y?cdA>;&> zFd+s`8yU5fIG&5CYz6ZEE_{-X8i}7K@9weBcY!qP4#YUO4Wo{z%U^BI+dUBrzX24J zBKLG^3+5T(al#E^@2G_C!Z?+yh}aX-wccPnC&`DW-B(iQS)Dq9kJ^<*DSC-LF{Y{7 z)a{~GHNmF#G6lvvr8iF)44a5*ySeSTJp{bo(CVbmVQNFj8!hXvuN5>GspX=NP%fB+ zY27r>%M1jvtmko0-+Y~drTwPVU-MEk5b$6MvY|#AbH@e}i8e!R?2S$AHy>iB6b?3% zwbXne7aA{Iw?tl5Ae50+`lB1o_<5BVs04-&o1j@tAvDCbDVJtoiwe{!5$)W{7Yz1l zX~0VW$q9s;5_k%`Xl!2ZLS@;5B~T(Hw*7f0Hke-(R+{9P{vC1e^f}OOD&diJ3Ar_p z#rGU8eHFbe{=QZ6D*cnA1N5apb6~N?h=ohGXVhK{sBBE+Xbqank`BdYEg9woKQ-eUpXhuq#wvA-MhSg7ieoo#AP# z5e(YCnF0Ybq(c`wZgHsW7hJ@LYh6^mJE9ZMo1?R3=tdD)f)XP>ugOUUA)US=t2iHU zsa$OQo3wa8u>X z(TXrEx5Gb^)y2YhRZE54;ycR&$ytNRA7Y>FAtIFcx$%j~N{ta3zi6=6Ca-E$kU3pWLzh)rr1fD2u(1iX{6@A!r%r!I2#_Y(ysMf(VRc_ z{Yd`B9A15YL`?l%XL+7};AOYbG^icUm1_;sj&hB0U8#A}{a4=gxf;wShRZVp&dr|B~UKKnLyBYh(YghKOq7wJP)YI!}+n z5I0BtmN@L$c>q4)QCx|N=47_Ot}Bv?iDaSi(=RIi#Hdf~SPbGdv8<3}fwoYDxirV_ zm^yX21WLZ6s|p51@i1UX2jHZz8Q2t8j_Ou{uuWN5;^!{r0A!P&ykAbI@CCXen@hM5r;ubCxv7D&JH7M{&p5&tg2gg zP_Riru1aAr{x(oM|7@Y81nnVRl(7+;gl-6hoA!(Y{FBUP7<+4vDyru}vDp#~jry-o z2+;HcPbX(gW0e;{h_WoOxqks9=#4J?z zx$^mhDf2wZ13u;#w|%Kur-ddgm?-LS#2gBr2Fw?#5EyXFvJ!P~RQa73E>zm1pjj&7n?hhF-ixu4bVA$4}?}Y{R-YNSA8pscz z8hibalTI8fa^Rg5K)l4*mq_U8MUbV_D<6A>!}08oURgnd$UDd;+ZOy_a2(5*WJ=;{ z2CYv%S7rlAguVYXg#0Mm@{HwMasHv{%4$9+M__` z)HKfbygNE)Uig#0`}}~`aw0B{ZH6}}RSPy*b)nu1sw0!IHO^nbj`;a<%@?!~xiJ(S z1pB1*Nx@|Hk#1eWyy!ei(h+7jinjfHqs`XVWozf7l|mQ%2NcK}kik)~kPy>8V-ygM zw_#s6j6AXJagpZP{(ruKSoH~mQldfqkF2mZk)=HvOEl&DiWs*v24#fMu*5^Z+{HZ9 zY6No!@{M?Hq^h1Bn>Z3u4tAy}tUTe=2e&$l>H!>7VQJ^X37aC=C0W+XzO$#tA0xk# z{uON(4d5J*D3>J6jO_6I1Oe63A_Q$HpDqE04p4XrA1#Z{yfwLjC(T2($cfL~tnl9~ z0O3{V&`A6pY4U&|%B)VMF|DMH;RuRK&%<$Z4bot9w?0J{B2B|ayZ+5?Qu?{JzWiBE zcYMi!jufnAx?w!@@HA9C&6+U}@NdJ7l9Q{7{QF_xWo5^zn(C*aIl9pAkx^cLzyMDh&T<6OUkL)q;v92F%~}?Wq6T z?rLt}G5Jq$i?0LAY@-7gdSShTSFqyb4rKamS1X%^*0~!5~~8)N?f$>5!T<9QRQ|@kpjZ$ zG;ggb?C4D)bi{zJe;IBsYSzHX;^B;SBe~TdL6l_>fr@PA*#d}T)7KTw{?=qicf7$$ z|GWQVjiM8|m->(s^+aL$E3yifnI!_)gdYn`Czr@(KIexHHYcB!*xMsJ^^?{P)(Gk@ zO7#81uiRYsZBh6N4>7&E&RFDe+?F0PbQUx0>1Xdn9``aj&KScF;4iBcTwtpSk39d*?gCRdQs4#ll~K~GWI6xeQ>XoUhZ-g`*-E+)z@XT9DwN5 zCPIJrxSJd;ckBfhTiDF8+{CS!2%+WQ;QLmfhB0hyIo&GpczHDZOn)U{;jC$7U+aZo z(!r8}>^BddCYZMB$Uk+#M)$yot4G|%{pH8^jp*8F4@$_nS~ zE6i3`lr!~*OtIq8@GGIbn#S|<^8Chl~xiO-3jK8&q4}d-ELOn~VhC?n}EqehUvlj%jdq)Gc8|QM$Nekh3j*y;y zHO%ez@!>-xn+uHn9Df_RCLM!nE*)jwNEQ5Wb}V0(lnKE^F3f+BZi?Y*mynNt*A%>< zXGoDo)hx;8_OtBMRMT5XE67sLJmY;{t@d!RC_X{r$~S@B*Fr~K7`bRj8M{~gV*GvD<)o6I{>JxhgD8u}@s>sO+A#*;;j zvlGAlYA8GJGjSV<2b?uX)Me5bnH|un1g;qVb%i+Gt^V`wm#5_lflKaEL17t30?G=s$ z|A-70==U)m+L$L_;0E-nO@3}QWpS>U*C(UIMG z`Lh_6_YW`$ZM9;0Fmr_RLKa;2J{XN+)3&Jfra6Gk!1%bUOEiL<*us3E?8|QC#8y|e zLIJ8FE~5iDot<XC{(al;9j+o3BiHWiv_J*UB9GR6lcGOOz@&ZIi zP!>?t=0gncXsI9g?~W7tZ7*8C8NYz5lQ-pvJg|i3@nqirEtpDZw)B?}J^T;l29|E& zq%FFn)wTT`}+b{sa5Re;Y-B|Eh>23^KgZe(2HQ0^|31_ZJpG^c!PM(+Rt zcoXvhMaS4cIS9&Chy5hUp!+KG3&r#}4c_ZglLqLLM=Mcd7*TnuK`Mris z5~mkm)rof=41E7Stwra3Zh!T&He!VxrO-i6o0R)4hm++Q>4N=^ z1xr*0qE>HudZ^YHsoB2W=-*WaOS6dsiIVA!wBmR?_GF1O8@H|v&9FHoXuiZ5hqF`i z3KZ92SRPV;(}Jyj>QE8NN>buD@*Hdbsu|O`OPVHiPWt3s>*YV%{#D|fv*)6p6(!`j zBRnqZHsq3lF71RfV4ZMMIpEX!y}cWAK3@1I!t44i+%!}Wv3VxZecU-Ccy|-!_1bhz z`GJ4dYBDKsEkih49+I;=!T@f9*VXR=nn_~Ln|B%Ci@46A&4K;&J&I0rA|+=sAQ;%F_m>5jql|7OL>5)qTZSoENmBH>jG?lrdh%0&_{vfN zY_26xWl7c~{LqGNV@h&8_5`WY;Ds@*$>Y{1Gir2DR0<&_m+fGh1i!E`Su-d0oF^F2 zLUoVY@VhZJE7p0!P@+Kd;`KfWg`ELQ29~tmF^eQ?AN?npq->yLAFN=MSMAS(JyP!X zY+^K5CCOh*Ff*}A^{)j<`y5DpM%O$pQI@+F4ypFt817RZw3K0UF3vV7W1Gwq#yWv3 zJ@g#~*63E0)PyV2M?s5*e5cJ?-4^9yN7O>#`wqwiu z61I-1$<+vj4;1BwT=cz6p1Ga6&T2-q{MMdaZ`I17@*nC#KE=b-Y!=g#@zDPUgL5RE#IIIg?_&P-kP?Sw z-2Ilrr6-#>c$T|Id;91ip^P+o6=K4!k^M)&)_PzKA2-Neva;uzY!Fzz00xVjRzXG# zPDG)zZ7WnqlLk(o&X(-X^Gky}y0w6=dqEMQmvmru#EOBmF(^#RPgoyN42YeJg_Ne6 z%i*EtuMvrW!I7)f4PAx3R`%6{r39AxG$$HbjcHUPlV#mY{@X1l?Z4k1!!Bl)c#%Rk z6~?l-Po0A(er|pJ>-EC1SlPpH5Ol>S27uUNXz)+XTdZ1llxWA*k0LrTTYq-GaI${? z*@DPT&FD7GgTD*$SR~>+FN_Dn=ge=2t2(WZDo=9s%mrExGJw%Ia}Ov@G{gLjGCN4F z$05;CqeH=?8=j6b96tP;z))`!pVCUHeHW?{X5=o5kB*V7)-_O9)TYKM0Hmh;#weUL zaCZFW1xY80GKS#iaBgoW$>600pHAh-SAk-O>A?orv@cyf%SjCKA*Iu!?)8vxw$}g_Gg3&nbTm%xFsA-eyF2Y zeuDaU9VdC+S|F-mTWKZl=;O#xHg0_Uc=p`-9J|q_(F>;iOJ0B6?JmMLW)^nS>a^s5 z54KmgQI)*;>=JBdt0{hQZ2$k7W*0iOUE!-4qsdEs{flq%e(TyJaYN&iMJ5u&d?pm4EYZ@+9z3-?fAO%F!s$#CgL=$r92GMrU;dE4L$t*bmmxR6>^5 z6Cv&j?XaoTGx5Lci+Zf_J*fN_i^{KiroNdU&yqAS2{vT_;0;P{j{&-z zCe*?J;!5#`_kmvtW9p9z_UtDY8(v&6Fb+-rNiHP968`WB8QUE)HqX04tB-85o?`++ zgng5)*|L_>u5fC$O+wA7WmbuH<-VZge4<17B21`P0FwE(~u3xS-Bg^>B4u9bUpWzD2*B9Xeo1w;B z&%kfBH>5-XuS2k7b;g;!4LCvyQQZjp*z{T=Z%lha8sX?<6`W&smKD{ZoEYpl#IU(k zDquaPma7=`#@9NpzyAUzyjgeLU5S^$wC5GpEGK>NVlOAB3o5UmpIydI znrD6Gf4sRAQ>F``+zMKF#nAIzfJKKo%0E`g{|4`DK!IuRMZ(pPdDiw0e6?Fu>0YPb zP*B+eSevu2qcc>pJs9vF=bdKMHMD45@T^T+1Moq5o~5b{pLQ|T%BVc1x6s&qUR2s+ zf0CYY?$mMqWlPOh;=5{6Dr(=)K!o)uxLY6ygfnd>E3MdQ#awzLHrK7}p$8S|a#w+k z)M%WwTV3)V@)EPBrBTb*){DiFs$L)fq>8qDIlO5!0=zvZQiq#Z1HtdTua31sUc@2= z?vkowBZG+4{^o0|aR!Z*>dW11@sh^5R~OU}N>oVI!JWlOsaELYM~g%8hIEzk&^yi1 zo9${m$~4xxchhTN-5^Fugnuus+gecaOajDWKRJRl{_TQz?;nXCY0dd|T>Z7Xy4k*8 zu6Cq-1Tnoh85ty325eSp&5bzkLCYMavq(7mX7E`g^`-guf?IPlLbZ_E?OZr`LSEfU zTaWCgRy@9}>5iI%fA>?L29$@2J)$V#&r0RH+*6&T)j|I^=$DCk@Xi^jCtRt5p2v1q zdwm)iyhjkiSY*s!?Mgq%a*IZ<(RFh#i)&+AQ}z0aiG`eV)8n2!XW+FwOHTm0{@#i< z@ut-CWBt*UTqWd%ol4nBP23M*n!Ga-IWMV-*Wao#2Ni1bRwcft16*~m5KL>xyGqHj z5~z?}QP_*AG2N?SvgWwt{p}%#u8=hUBawGx;OTf3JwVB&`+2L}D0i#)Sn_Kz{{>B| zg?H?f7+(Xj?BY($Uz+EU{Fm+uo=DeBLT{++P0OnsS1#p|QuV@j{XKYM8dHM)Z7ldqxGN8gfE&)A}n z>D@mXEiu?vPp3LNSYMH9>*3QFsTmTf1!5*VLEmL3^xav*^W62E-H`~?gbqN{>Ni$Y zNfzwC8u*6K4clyD3+i`LB}2R$K7WH8H%J)T=#)acdY>Aofxi*<@$8e=;p)V&c>)L! zPbMrmIeMloHGbmo?B3bd_2oF zoj?4S>NAi(qT6dydJ)8N-ccCJ^&LWmKelFg(Z?0W-WGX#F_(vimoyk1aIsQuJ z5smlvt~contqtt4GtWK|)2=mn{~8p&D16QFeM{~YE$-ewgNl1fea?8~uSb@WKFyHD z*{8CtC}YSVc=Pa;wAs-?g7w_hX|I?D&+7;Kvi%5#L|5Q@U8;d9UKmEamnd=sO`E0F z;?8-!b3>L-CtnV~9=r)L%-JImS&SOHT!Xf~siZ$er@o~=C46y#lmFe&QtGQWoGw1t z`;NCx{(I7)_zgo=`LiHNv?1B2ml`bwzq>G0-d^mtl|zqyiHctSF{EFE20wZ8{fut{ zVp1`oF_!jt(Bf(;AfY%g1w4Khm=+ANG#m}Zg z#a_0$O%skT3H3OvBgiWg3IYt3jr(`&xyuD7Ow7juSnT&w1qK&! zMTFHIkLACSrHPZ%*2RxV$arhe^*Ph7LkAaT>T;aDoUG@dlg z=llCU&-Xb`&-tfU<-XnbeJ!8sv%TN%PIoqUlf6kp6>?A*R9T8cB_ecKtYZ_66PeB- zDSN!It|R8QsE^PM#Jc=mZQQEoHgUK(uk3xfb?)PCeS#d9bHk?Fy;{`y(lbOw(e_DU zcu>NF0n`wW<$Pr>TiI#9=0bX3h>MoHQHCp)aLVCYo9~JVs}<@VB69_?qRgiG}9WH|o35Wz+|WEKg1H+4VZTylb#F8{fK zt0fyc@j2EBFEGKIdWCAr*IPR?ilZ7Y#I^Ch2)ohacOg!VdEm~)#hlwTj`*mCsp;Fa zxcT8E2MbptOWLXPJvUySv0C%Fhag11Lbe^+^h8YdiU;A~c$$NMog~#9t7up2V!7>B zk@#aS=PpMpkfT4*q#sQrI&0Rno!R6YtUXU(GN*al*@yN1!P-Qr`>g<1iUEw$7L$jqJde%ZO5;T@+rx4V@D|>Jo>*nv+ z*`~3wqg?GT5_oA965BC?m)|uL#mSb_vK=YI31d5R11xm%qNZqbOwLtfeZ`(FSJN|e zB#d}tJuM*~r=I@im^+_(x9)|9(4iXA5TDq0TGWD7>R1LqEACs$1skCzrFr>@d-l_@*gn!4OX)hgx``w@~ zyJ_LmZ`Tvs2LELYsqg-SkC z^;Ta$^?S$Eb}r^jZClCoddi}S$C2@>b0WZ|BvYn-NF?6`RP*BoH;!CzwPvV(6Cm0y zBCs~~mEBI0{>+tF7R5)P-sC?4+2wSR(LlYL+Di$gvlIg#IJH2n#xj4Vez#tj&XoX) zt{Qz-JGXc168Fa8C?m}M7Q_|yEdTTngBht^&Qzhn9)L6&f~rTJh5I1r(Ul#I?lxYp zEvnU96A02&8qg25pZOpgZ?!q0U0Ic{p3j5Kr?GKX8iei5!cdt;+Fg}dW(j)((z<2B z*73JjhjO@(Ww>$dTy~)WiL4_-%i4UsO_q$F-gZ%Dr`f*To?FtsB~)p0Vi}X|5spMKKkcdcNUPjCj!wjX+Tk3BzM&7>H)7{KU9)LY66;I4L{eBvQ zr&_pDxq6VG(VD5mV!PyA|1!s#@ja3;ZU&7&snB{|aIR`H33bVHX{+VMIaz_NrZO~r zc++%wR>&W?XBI26s#1z}1-m82?Ri$N@e>16+i-vStZ&Ptn;~hF9a`;T%B$ls8;C4= z;K2j^q_GH_h z%n^Fax%NnovU_E^UWz%@)DhqwYh7>+$Z` zgm?=8%DSKFdA=(lSjDIEI6^ED*I8OAIeS}bG0$F6IrVBtLi7UhRMnZBqYRmNAp%e} z5Aj>L!H|JZ>;|Ibk=*h;cfe0Vpf_vdG~bI?vxTPcA5-c=-FA(nHt(un*-}cX>|1A> zi0ypgVCnvw)_2LJ#E(fewvf}d*W1)u3Fd zjC(&Nk0FTu^kx*0N6&w5E(u=bY(3N9>ob!cc(?+8E~8kV|e<~XO{ zjH|Rr;s5tXA9}|Y0Ays^)^@{%0!mEfT39*l95dEdT%2#XayVKWyVd5z@<{azE*0L$ zht2=Wcg5()qMk5}~imRXP4ROX`Bz$a)6s7`B zLBK~kI}z8)@>kDXIhVNjw3lFH2V!S2MdnCz)wj{nR@5O+q zX1`+0+G{j;%H?)_rXO$c^FIPy&x1Qp$EzPzs{(-VIV{$DMi|NTGCn_xDWt~Z?io$~U~wjCI~+#<1Lca|qme?2gQzJM-taCpJUC#RchM zysC`F$}l1MehYnWf3YtA?cpIxV0<44bTa>V>K*W@SI325EI^$pg5cG+WjmrnHcFY5@#zH{JKn4_l(;2G#}3cDranQ24zbYh!1atUgP_>cmJ1l9*_+dvSr<8wI9=oW=!|Viu{U&YGyurNSAto z!Vp7AI3FR-G+z;@j@VvXzCQ#S%6c<(F41=Y_Oakz#KnG*qCqo2u(H~P+}pB}1r6xg zz}9U5Y|)8!H^(+jcmI-#dX)k%Kr!^e$hF~uC&1GVfYved_^3%qI*>FUc)IU^I!)Z{ z(;s~boNKr1mjIVcY*;3scVQA~_5UyPk$()B98MnLABL3U4wx6yH8oEoDEy%xcut5# zZQ7}E!M6v#Bt`%&?fRhKPK^r_h5%0kspAgM7V$V!8Q92d!is1dNQOB8@kfts;S;NG z!P-6be&Oe9w=te!-@MdroCU9Vc9ZE4VPfwF#1F6y$!@0uWkBS6-HiGW4*30FRh&g) z)P1|!u7CZFVTDz)`qGM?EQ_i*I1kbYq!bfWmyW~B%&7s z%x^zX&gPMQ3bdRCK+~4fHLKP&dX7;UNpQY5?LHR;(A1bUp)WNE5kQji3(&hfMgx~o zf!-wvEQYJuW`Gih2n4+PWEacLL%GDl)=?k#F*CH|%$jBz?p=!i7W|FD*x<19eR%_T zZ+(E*!4YKH@*Rgt`vLKgjihb&mmOchH-!y`=sHGD+V0Ps8Us!%cWk6j`F^1xj<-b!f=YQ zUEiEj^>sqmT~vSnn33+Z(bo|v1FR$ix0rb?B<*^ei;Q=cXUb>%r(<6m`2&7}_+8ov zeM3_Q1Ja~*V&f(w0QC=uadP^>4zd!45eoJ223xD`$to;m;~~N;_*Fp2MFH}58j<^n z^Pq6!#NfE#1t|J^v^H`zz2eKK;tUY7{m~D&%ld|W=R{}#ZoIRe&Hun7%)RB3EhJ+` zeg)iTXl#rDa_^p4HtwpI`uZxQ?e5%43zxgY41*1y; z5(hvh2GDTP9(vGKIifA;7_-Cz)Oai`czl*V}YyigLN#x5n9B?Le9csN?Evf-~e8-gA;eJ43-~iyQq1GBRBuoW!Q@(c7)T(8est6^M+U0D(kgaz0O+&L)mq~Xi^7=5 zqE$O9eaQ@Q)K|Q+y;XWA@0+TjGu`!QMit?PSO>o2KO`T zn1|oh_#aAEaB-}JlECh%6D?m-Qe?DYDKw|OPVR*P{G*b;vj#a;3FG<(O+W*#nz}V( zU2lyfm5R;eY9b)zAT!h+>6l*k3UZvZ(}J>Vw6SmI2ok$U2Fiz>1i&uM*Q!&jeXE;I zai@KAsKm{R?;*>S+P)UBcIqZ)j1z5qo#uuUcUkylsm;LXIzswqkh)oVB6ffr3>BVj zKy0isk?6LUaF4N}?>*gO{<*bv-cJxZ!208wfMcA_NzuoK&H$y8k4!NuYxLU=fb!3W zkgtPb>HJpm`mX&Or^>C1ZA_6X@TK!@Xj`+c^-s|0mAT^4mkA3>H+)2&Mor<)PLwpl zx1u}(BILMY8!4dmd9}q28ndD)w|sjfoaNetVLjAuwae(x97DFzxsys{kgkdv!YX#2!MQf3qV`5*5DHe}UAh`L|vPyr$KPvd#MwX%P zE~0kjxMmx>kk2>3wy*DH&iLqrkPX+61z$A|v^} zV@g&?3=0u2E!J_+$Vny~boO@m78rTAn|OhqSSJiiaoyzHwtJFT6(&oDD-JUY)(?R@ zesKgnx#GctYMF{VwK)!+xQQrEG{vyw)lHV>KEV(ixE30_*7;%=(nRH^)629?Fa!@t zt`p5}{A!9udW2Y0{W`@`To6*0@cZM+O@g|SWlf*2D8~uIMbLA(JeQ%!6|_oWHfki`+vH2&f4vRHYNTU&uJ$!T<&*e+C2A9z;(p$ z@N+YDCnpheuSoACumyhr%x|V|Q7?nMIgm0y)^Qa`2uI6Frx{ed>R%wE5*f>*a7{`??#4cQ%P?GvNLI+r`HsIYhjh2ABiBC z8NC0-=fP_8$S6EXkONoAsqoUtU>It)x|%X)(TVa;C}+g^|KimC%Rj$>sL8<7F&q6O zDmetA1@uSZF=zx6SxKCl94e|-stR$*d~1;5naZT>tP$`AwE0Z|72F}_)MFmWw%W^M zG1IR6{>NBHp5t`Eq5V>HHlb?0*RWENsjbt7AuBpk#oh?H#lz&sU_CL}cYhy`B$It% z&hi;B`uBv~xG7?%z^cj}iEVha$Ln zUbk6iy>e@HO4SO#xdRTs4H~`qT3fcX*73Eyo@JFRu_v9V8qJtL{@H!}H|O$o-p5}J zQk^0 zF*y4tkhrxWn8iAjOhh497y$<%B*Fn`!A=GpCIZfa_bfa5AnvcR-Fv{N1y8I~bn4B1 zROvH@-hSYw%oLloGm%hcDu;j461xHXCK|Z|HRzl)iA#;)xzhu$6G8;ywxK z!3_(6k3}C7T3ZAIyk-Gv)qYMA9|fShfg`|>TY_S z;rwM=wPBDkx9ihS0p`QyAW;bmK;e^R(#zD_%#=4Dzk&k#tb~wi1=FVm{-YNVMs?=)TBjIacE`DI~c~F4gl0cfO^9dUa=!-?2~24?v@K zTwg5kjSU}_hsCTd%1-kAWM6}9lM)LE;3fEZf8;PMgl;;E=;U=8Dm(&)$Yf>gEYt15 zrI&}jmH>05l(dCOrS`)^%isX7?Zq2f+4W{$f!%2NmF>)j95$v$9Pb~x)doz)aT)Ib z|G78u)&aL#&(AgbDyQHqw|+zySL#ojL)1&u92LHQ75ZZyz%H5z{Z62i2vEqo_2fEPgXD%gh=e*w*>gVJ}+vLIdAH zr0&!rOHBmku{Vc-wX;C&EC-wD?bASWf)0#%6XDT|_n9=6y}R1JSD^}9fXaH@f)h|) z*SFUz-L2OfJ^ZI-_&3|}XDaIE*g-h@AoriwToC%GophnmDC5-y9wCX}$}&y)BP6-5 zuPPrY6$ieSxb8HF5;0Rk_a{o6L-_I+>$pr_iX6uj=e|#XwX+AB`pDa6Xv6l2$Ovx8 z{pN&A`nAi3+qV;4vI2z2Ka}@m!DyzeMnpm%PW17&V1=H8uq2@zr;*A$pRs!`zj4kA zY5GJ1O2|nBXw5g($y0@?G`49;=vwC|9ajWO-c?~)94F>3F)pmG!`3OfL=Nuw60Sb7 z*Go=Ps#QOQ9#g6nPvY*q5)oyeq0P#cWqQfu)df2MWm=^HcWCjV;U}%O&&2{kke;Oy z?wU^YkyZCymAI^5rl(YbAwA9{aHtK2igNp%3EF&l1U%)^UC`4vt1b=|5QLadoRCTn3Ty`<42 z&n8CUUJnY3$dmi|Oqc~+)p4PIR;|j*3r84cycZ@wU&FDW0rBg`jaRNB2U?B`_Ca#vYJFZ1Dr5m6ES+>fx{PWGgv{qIj40!6mm z7u;&PoP{_&ye&`yuVN&f^N=;-Z@JALMmCMd1&!*arGwR8A9U|>BdFqGaClHU3i^dt z!umDeY(8iOLc$~l(}jXD)0132?<=#!=Bt^+obVWOxZ^Oee8eY%Wa2KNMFt5zYKjO2Isr_f)65@ooWip z0g3J|=Oo-QtdJRQHjUrB%K_XaWN)M2ZY?|h$GIK$Xl|s`Z!V`M?e1!{&;d>8j&{Ds zZ1>@UUcctg#krhh=iH$Ap-xFF z`i0{I0H3*&O8^mS3Ew1ivBFyCLV@^>Qz<985ClH3A=nyynmxf(Q7P>@SL<4Bgl(zN zTEqEam7W>h60I(yvmJ^-Nq$hl2a99hv9zECzS1YP%T~Y@Lj6})=!az623{bn=M=R<@E;;jAOzlJMr^Fx%K@6@siiKqC0evYfG)zd=VbsG<~Le3RW z;5sXViToV9)5 z@=9tzAp6M_#*e^9asm0qLO{i-ASDlVb^w&@*fFS4kE9#tQZ^vk92OzDa%+i}PXF!) zsPR_O^EI{v-?$+pjf%Xk%f$ce)#O~U2Trfe%5NI{;;_0F zxa4g`!H7`%aQ~ox4xR53zyER)=lzfCBDE>7Fvxb;^ z5J^tf;Ed<^@W6Rtoor%~vjTSk4zsq-=oeO|S7Wilbz>~8EU6zon ztyvS0#I)gu5_E`?M2pCpo#ylOjG7U^?>CWi)aW>bMd)@Wb}7C_8*)u(DI>&wKpHyU z>5(G-zDA>CSEDe-{v`_!ybCGdospYb6X*EefCA8e&8%N-K?ZcaK66fi8{Q@3_}zex zs!)aUKNyole{n~w&5_%JKaP)tItu9WZ9I&W7RlKpVCE_)dFHD0y&kD+hY2<&SAD8* zLWw+P-g_XfQ7!GD3OxoAfrdKbT0MjAD4F@(TK@!7v@@GpItpsvY+E4;VY$y9m$Ol1=H1?31+x&MqqqI)IyK)iSwmPP#&{-fW ze@t(g^OLUW{aZpJ=~y8vTx=@!w&4S2a(B^wQBX6DXKT+i&@ap^jSe%ZbsKiBW$k#h z=K~f-ci#T%z@f64FSm{*rOiHqj9MS+1+5(%+i#ZvgSkCKY{p-(yQ{!TYYrfZOJ6X& zq1U-n@m?6daks{If(oax+V2BjLVi`r^0%=>cRXYwU(~|bbHSYh1g+DaKFo;XqB!r< z;^>SZl9FaEa06I~Ws;z7s(5sbt&ASJog-T}V-Aq%uA1iN+T-fgUxoe-Y*ddp4i}sL zI3ujUbqvE{iXH_4wW;yC@}J#b|0!U;Ym(Ru zDj&&82L3?Q+-}B1;PMN`$)5%a9m(C7V=6hq9(=jYpJLs2uG9nFCsCDXtJmRi)rT