From 315b6635f5f2359f4dd4fe67505a06bb2ac70725 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Sat, 19 Jul 2025 11:26:31 -0400 Subject: [PATCH 01/16] refactor: move to RSC --- packages/cli/package.json | 5 +- packages/cli/src/account.ts | 2 +- .../src/commands/provision/durable-objects.ts | 16 +- packages/cli/src/commands/provision/index.ts | 16 +- packages/cli/src/commands/types.ts | 26 +- packages/cli/src/config.ts | 39 - packages/cli/src/prompts.ts | 279 +-- packages/core/package.json | 29 +- packages/core/src/actor.tsx | 66 + packages/core/src/client.tsx | 134 +- packages/core/src/durable-object.ts | 156 -- packages/core/src/hono.ts | 16 +- packages/core/src/index.tsx | 36 +- packages/core/src/internal-context.ts | 8 + packages/core/src/internal.ts | 7 - packages/core/src/modules.d.ts | 40 +- packages/core/src/route-module.ts | 73 - packages/core/src/router.ts | 12 + packages/core/src/server-entry.tsx | 31 - packages/core/src/server.tsx | 322 +-- packages/core/src/ssr.tsx | 72 + packages/core/src/websocket.ts | 29 - packages/core/src/workflows.ts | 54 - packages/core/tsconfig.json | 17 +- packages/vite/package.json | 15 +- packages/vite/src/assets.ts | 102 - packages/vite/src/config.ts | 42 + .../vite/src/entrypoints/entry.browser.tsx | 4 + packages/vite/src/entrypoints/entry.ssr.tsx | 2 + packages/vite/src/index.ts | 181 +- packages/vite/src/plugins/agents.ts | 88 - packages/vite/src/plugins/build.ts | 118 -- packages/vite/src/plugins/config.ts | 54 + packages/vite/src/plugins/decorator.ts | 43 - packages/vite/src/plugins/dev-manifest.ts | 25 - packages/vite/src/plugins/durable-objects.ts | 287 --- packages/vite/src/plugins/entrypoints.ts | 42 - packages/vite/src/plugins/hmr.ts | 60 - packages/vite/src/plugins/internal.ts | 51 - packages/vite/src/plugins/isolation.ts | 4 +- .../vite/src/plugins/remove-data-stubs.ts | 123 -- packages/vite/src/plugins/route-reload.ts | 33 - packages/vite/src/plugins/routes.ts | 56 + packages/vite/src/plugins/server-bundle.ts | 77 - packages/vite/src/plugins/worker-stub.ts | 28 - packages/vite/src/routes.ts | 185 -- packages/vite/src/routing/fs-routes.ts | 56 + packages/vite/src/routing/index.ts | 6 + packages/vite/src/vite-exec.ts | 49 + pnpm-lock.yaml | 1724 +++++++---------- 50 files changed, 1679 insertions(+), 3261 deletions(-) delete mode 100644 packages/cli/src/config.ts create mode 100644 packages/core/src/actor.tsx delete mode 100644 packages/core/src/durable-object.ts create mode 100644 packages/core/src/internal-context.ts delete mode 100644 packages/core/src/internal.ts delete mode 100644 packages/core/src/route-module.ts create mode 100644 packages/core/src/router.ts delete mode 100644 packages/core/src/server-entry.tsx create mode 100644 packages/core/src/ssr.tsx delete mode 100644 packages/core/src/websocket.ts delete mode 100644 packages/core/src/workflows.ts delete mode 100644 packages/vite/src/assets.ts create mode 100644 packages/vite/src/config.ts create mode 100644 packages/vite/src/entrypoints/entry.browser.tsx create mode 100644 packages/vite/src/entrypoints/entry.ssr.tsx delete mode 100644 packages/vite/src/plugins/agents.ts delete mode 100644 packages/vite/src/plugins/build.ts create mode 100644 packages/vite/src/plugins/config.ts delete mode 100644 packages/vite/src/plugins/decorator.ts delete mode 100644 packages/vite/src/plugins/dev-manifest.ts delete mode 100644 packages/vite/src/plugins/durable-objects.ts delete mode 100644 packages/vite/src/plugins/entrypoints.ts delete mode 100644 packages/vite/src/plugins/hmr.ts delete mode 100644 packages/vite/src/plugins/internal.ts delete mode 100644 packages/vite/src/plugins/remove-data-stubs.ts delete mode 100644 packages/vite/src/plugins/route-reload.ts create mode 100644 packages/vite/src/plugins/routes.ts delete mode 100644 packages/vite/src/plugins/server-bundle.ts delete mode 100644 packages/vite/src/plugins/worker-stub.ts delete mode 100644 packages/vite/src/routes.ts create mode 100644 packages/vite/src/routing/fs-routes.ts create mode 100644 packages/vite/src/routing/index.ts create mode 100644 packages/vite/src/vite-exec.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 3302673..d3480db 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@orange-js/cli", - "version": "0.2.1", + "version": "0.3.0", "description": "CLI for Orange.js projects", "bin": { "orange": "./dist/index.js" @@ -34,7 +34,6 @@ "@clack/core": "^0.4.1", "@clack/prompts": "^0.10.0", "@commander-js/extra-typings": "^13.1.0", - "@react-router/fs-routes": "^7.2.0", "chalk": "^5.4.1", "cloudflare": "^4.2.0", "commander": "^11.1.0", @@ -52,4 +51,4 @@ "vite": "6.2", "wrangler": "^4.7.2" } -} \ No newline at end of file +} diff --git a/packages/cli/src/account.ts b/packages/cli/src/account.ts index a51623a..ab16625 100644 --- a/packages/cli/src/account.ts +++ b/packages/cli/src/account.ts @@ -27,7 +27,7 @@ export async function promptForAccount(client: Cloudflare): Promise { writeFileSync( "node_modules/.cache/orange/orange-account.json", JSON.stringify({ accountId: account }, null, 2), - "utf8" + "utf8", ); return account; diff --git a/packages/cli/src/commands/provision/durable-objects.ts b/packages/cli/src/commands/provision/durable-objects.ts index 3d4413c..d180a5c 100644 --- a/packages/cli/src/commands/provision/durable-objects.ts +++ b/packages/cli/src/commands/provision/durable-objects.ts @@ -1,3 +1,4 @@ +// @ts-nocheck - TODO(zebp): re-enable this once RSC is more mature import * as fs from "node:fs/promises"; import { flatRoutes } from "@react-router/fs-routes"; import { loadRoutes } from "@orange-js/vite/routes"; @@ -43,7 +44,7 @@ function durableObjectInCode(contents: string): string | undefined { } export async function provisionDurableObjects() { - const config = await resolveConfig() + const config = await resolveConfig(); const wranglerConfig = readWranglerConfig({}); const durableObjects = await findDurableObjects(config); @@ -85,12 +86,9 @@ export async function provisionDurableObjects() { ], }); - await loader( - generateWranglerTypes(), - { - start: "Generating Wrangler types...", - success: () => "Wrangler types generated", - error: "Failed to generate Wrangler types", - }, - ); + await loader(generateWranglerTypes(), { + start: "Generating Wrangler types...", + success: () => "Wrangler types generated", + error: "Failed to generate Wrangler types", + }); } diff --git a/packages/cli/src/commands/provision/index.ts b/packages/cli/src/commands/provision/index.ts index 2a94e2d..bb1ff32 100644 --- a/packages/cli/src/commands/provision/index.ts +++ b/packages/cli/src/commands/provision/index.ts @@ -8,13 +8,13 @@ import { readAccountId } from "../../wrangler.js"; import { provisionPostgres } from "./postgres.js"; import { provisionSqlite } from "./sqlite.js"; import { provisionBucket } from "./object-storage.js"; -import { provisionDurableObjects } from "./durable-objects.js"; +// import { provisionDurableObjects } from "./durable-objects.js"; import { provisionKv } from "./kv.js"; export function provisionCommand(client: Cloudflare) { return createCommand("provision") .description("Provision Cloudflare resources for your project") - .option("-d, --durable-objects", "Provision Durable Objects") + // .option("-d, --durable-objects", "Provision Durable Objects") .option("-k, --kv", "Provision Key-Value Store") .option("-s, --sqlite", "Provision SQLite Database") .option("-D, --d1", "Provision D1 Database") @@ -34,13 +34,13 @@ export function provisionCommand(client: Cloudflare) { } else if (selectedResource === "kv") { await provisionKv(client, accountId); } else if (selectedResource === "durable-objects") { - await provisionDurableObjects(); + // await provisionDurableObjects(); } }); } async function determineResource(options: { - durableObjects?: true; + // durableObjects?: true; kv?: true; sqlite?: true; d1?: true; @@ -48,9 +48,9 @@ async function determineResource(options: { r2?: true; postgres?: true; }): Promise { - if (options.durableObjects) { - return "durable-objects"; - } + // if (options.durableObjects) { + // return "durable-objects"; + // } if (options.kv) { return "kv"; @@ -73,7 +73,7 @@ async function determineResource(options: { { title: "Key-Value Store", value: "kv" }, { title: "SQLite Database", value: "sqlite" }, { title: "Postgres Database", value: "postgres" }, - { title: "Durable Objects", value: "durable-objects" }, + // { title: "Durable Objects", value: "durable-objects" }, ]; const selectedResource = await select({ diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts index d64cbfc..f98dc01 100644 --- a/packages/cli/src/commands/types.ts +++ b/packages/cli/src/commands/types.ts @@ -1,12 +1,10 @@ import dedent from "dedent"; import chalk from "chalk"; import { createCommand } from "@commander-js/extra-typings"; -import { flatRoutes } from "@react-router/fs-routes"; -import { loadRoutes } from "@orange-js/vite/routes"; +import { ResolvedConfig, resolveConfig } from "@orange-js/vite/config"; import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; -import { Config, resolveConfig } from "../config.js"; import { exec } from "../exec.js"; import { step } from "../prompts.js"; @@ -28,21 +26,16 @@ export async function generateWranglerTypes() { await exec("wrangler", ["types"]); } -async function generateRouteTypes(config: Config) { - globalThis.__reactRouterAppDirectory = "app"; - const routes = await flatRoutes(); - const { manifest } = loadRoutes( - routes, - config.apiRoutePatterns ?? ["api*.{ts,js}"], - ); +async function generateRouteTypes(config: ResolvedConfig) { + const routes = config.routes; - for (const route of Object.values(manifest)) { + for (const route of routes) { step(`Generating route types for ${chalk.whiteBright(route.file)}`, true); const newPath = route.file.replace(".tsx", ".ts").replace("app", ".types"); const slashes = newPath.split("/").length - 1; const importPrefix = "../".repeat(slashes); - const params = paramsForPath(route.path ?? ""); + const params = paramsForPath(route.pattern); const paramsLiteral = `{ ${params .map((param) => `"${param}": string`) .join(", ")} }`; @@ -54,14 +47,11 @@ async function generateRouteTypes(config: Config) { import type * as T from "@orange-js/orange/route-module" type Module = typeof import("${importPrefix}${route.file - .replace(".tsx", "") - .replace(".jsx", "")}") + .replace(".tsx", "") + .replace(".jsx", "")}") export type Info = { parents: [], - id: "${route.id}" - file: "${route.file}" - path: "${route.path}" params: ${paramsLiteral} & { [key: string]: string | undefined } module: Module loaderData: T.CreateLoaderData @@ -92,7 +82,7 @@ async function generateRouteTypes(config: Config) { `, { encoding: "utf-8", - }, + } ); } } diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts deleted file mode 100644 index 5cf1f83..0000000 --- a/packages/cli/src/config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { cwd } from "node:process"; -import { tsImport } from "tsx/esm/api"; -import { join } from "node:path"; -import { UserConfig } from "vite"; - -export type Config = { - /** - * Glob patterns for API routes. - * @default ["api*.{ts,js}"] - */ - apiRoutePatterns?: string[]; -}; - -export async function resolveConfig() { - const path = join(cwd(), "vite.config.ts"); - const viteConfig: { default: UserConfig } = await tsImport( - path, - import.meta.url, - ); - const orangeSettingsPlugin = viteConfig.default.plugins - ?.flat() - .find( - (p) => - p !== null && - typeof p === "object" && - "name" in p && - p.name === "orange:settings", - ); - - if (!orangeSettingsPlugin) { - throw new Error("orange:settings plugin not found"); - } - - if (!("orangeOptions" in orangeSettingsPlugin)) { - throw new Error("orange:settings plugin does not have orangeOptions"); - } - - return orangeSettingsPlugin.orangeOptions as Config; -} diff --git a/packages/cli/src/prompts.ts b/packages/cli/src/prompts.ts index 6efe5bc..cc33dad 100644 --- a/packages/cli/src/prompts.ts +++ b/packages/cli/src/prompts.ts @@ -24,31 +24,31 @@ export const orange = (text: string) => c.rgb(249, 115, 22)(text); const unicode = isUnicodeSupported(); const s = (c: string, fallback: string) => (unicode ? c : fallback); -const S_STEP_ACTIVE = s('◆', '*'); -const S_STEP_CANCEL = s('■', 'x'); -const S_STEP_ERROR = s('▲', 'x'); -const S_STEP_SUBMIT = s('◇', 'o'); - -const S_BAR_START = s('┌', 'T'); -const S_BAR = s('│', '|'); -const S_BAR_END = s('└', '—'); - -const S_RADIO_ACTIVE = s('●', '>'); -const S_RADIO_INACTIVE = s('○', ' '); -const S_CHECKBOX_ACTIVE = s('◻', '[•]'); -const S_CHECKBOX_SELECTED = s('◼', '[+]'); -const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); -const S_PASSWORD_MASK = s('▪', '•'); - -const S_BAR_H = s('─', '-'); -const S_CORNER_TOP_RIGHT = s('╮', '+'); -const S_CONNECT_LEFT = s('├', '+'); -const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); - -const S_INFO = s('●', '•'); -const S_SUCCESS = s('◆', '*'); -const S_WARN = s('▲', '!'); -const S_ERROR = s('■', 'x'); +const S_STEP_ACTIVE = s("◆", "*"); +const S_STEP_CANCEL = s("■", "x"); +const S_STEP_ERROR = s("▲", "x"); +const S_STEP_SUBMIT = s("◇", "o"); + +const S_BAR_START = s("┌", "T"); +const S_BAR = s("│", "|"); +const S_BAR_END = s("└", "—"); + +const S_RADIO_ACTIVE = s("●", ">"); +const S_RADIO_INACTIVE = s("○", " "); +const S_CHECKBOX_ACTIVE = s("◻", "[•]"); +const S_CHECKBOX_SELECTED = s("◼", "[+]"); +const S_CHECKBOX_INACTIVE = s("◻", "[ ]"); +const S_PASSWORD_MASK = s("▪", "•"); + +const S_BAR_H = s("─", "-"); +const S_CORNER_TOP_RIGHT = s("╮", "+"); +const S_CONNECT_LEFT = s("├", "+"); +const S_CORNER_BOTTOM_RIGHT = s("╯", "+"); + +const S_INFO = s("●", "•"); +const S_SUCCESS = s("◆", "*"); +const S_WARN = s("▲", "!"); +const S_ERROR = s("■", "x"); const symbol = (state: State) => { switch (state) { @@ -472,123 +472,128 @@ export function spinner({ }; } - export interface MultiSelectOptions extends CommonOptions { - message: string; - options: Option[]; - initialValues?: Value[]; - maxItems?: number; - required?: boolean; - cursorAt?: Value; + message: string; + options: Option[]; + initialValues?: Value[]; + maxItems?: number; + required?: boolean; + cursorAt?: Value; } export const multiselect = (opts: MultiSelectOptions) => { - const opt = ( - option: Option, - state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' - ) => { - const label = option.label ?? String(option.value); - if (state === 'active') { - return `${orange(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? c.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'selected') { - return `${orange(S_CHECKBOX_SELECTED)} ${c.dim(label)} ${ - option.hint ? c.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'cancelled') { - return `${c.strikethrough(c.dim(label))}`; - } - if (state === 'active-selected') { - return `${orange(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? c.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'submitted') { - return `${c.dim(label)}`; - } - return `${c.dim(S_CHECKBOX_INACTIVE)} ${c.dim(label)}`; - }; - - return new MultiSelectPrompt({ - options: opts.options, - input: opts.input, - output: opts.output, - initialValues: opts.initialValues, - required: opts.required ?? true, - cursorAt: opts.cursorAt, - validate(selected: Value[]) { - if (this.required && selected.length === 0) - return `Please select at least one option.\n${c.reset( - c.dim( - `Press ${c.gray(c.bgWhite(c.inverse(' space ')))} to select, ${c.gray( - c.bgWhite(c.inverse(' enter ')) - )} to submit` - ) - )}`; - }, - render() { - const active = this.state === 'active' || this.state === 'initial'; - const title = `${c.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const opt = ( + option: Option, + state: + | "inactive" + | "active" + | "selected" + | "active-selected" + | "submitted" + | "cancelled", + ) => { + const label = option.label ?? String(option.value); + if (state === "active") { + return `${orange(S_CHECKBOX_ACTIVE)} ${label} ${ + option.hint ? c.dim(`(${option.hint})`) : "" + }`; + } + if (state === "selected") { + return `${orange(S_CHECKBOX_SELECTED)} ${c.dim(label)} ${ + option.hint ? c.dim(`(${option.hint})`) : "" + }`; + } + if (state === "cancelled") { + return `${c.strikethrough(c.dim(label))}`; + } + if (state === "active-selected") { + return `${orange(S_CHECKBOX_SELECTED)} ${label} ${ + option.hint ? c.dim(`(${option.hint})`) : "" + }`; + } + if (state === "submitted") { + return `${c.dim(label)}`; + } + return `${c.dim(S_CHECKBOX_INACTIVE)} ${c.dim(label)}`; + }; + + return new MultiSelectPrompt({ + options: opts.options, + input: opts.input, + output: opts.output, + initialValues: opts.initialValues, + required: opts.required ?? true, + cursorAt: opts.cursorAt, + validate(selected: Value[]) { + if (this.required && selected.length === 0) + return `Please select at least one option.\n${c.reset( + c.dim( + `Press ${c.gray(c.bgWhite(c.inverse(" space ")))} to select, ${c.gray( + c.bgWhite(c.inverse(" enter ")), + )} to submit`, + ), + )}`; + }, + render() { + const active = this.state === "active" || this.state === "initial"; + const title = `${c.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const controls = `${active ? orange(S_BAR) : c.gray(S_BAR)} ${c.dim("(space to select, enter to submit)")}\n`; - const styleOption = (option: Option, active: boolean) => { - const selected = this.value.includes(option.value); - if (active && selected) { - return opt(option, 'active-selected'); - } - if (selected) { - return opt(option, 'selected'); - } - return opt(option, active ? 'active' : 'inactive'); - }; - - switch (this.state) { - case 'submit': { - return `${title}${controls}${c.gray(S_BAR)} ${ - this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'submitted')) - .join(c.dim(', ')) || c.dim('none') - }`; - } - case 'cancel': { - const label = this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'cancelled')) - .join(c.dim(', ')); - return `${title}${controls}${c.gray(S_BAR)} ${ - label.trim() ? `${label}\n${c.gray(S_BAR)}` : '' - }`; - } - case 'error': { - const footer = this.error - .split('\n') - .map((ln, i) => - i === 0 ? `${c.yellow(S_BAR_END)} ${c.yellow(ln)}` : ` ${ln}` - ) - .join('\n'); - return `${title + controls + c.yellow(S_BAR)} ${limitOptions({ - output: opts.output, - options: this.options, - cursor: this.cursor, - maxItems: opts.maxItems, - style: styleOption, - }).join(`\n${c.yellow(S_BAR)} `)}\n${footer}\n`; - } - default: { - return `${title}${controls}${orange(S_BAR)} ${limitOptions({ - output: opts.output, - options: this.options, - cursor: this.cursor, - maxItems: opts.maxItems, - style: styleOption, - }).join(`\n${orange(S_BAR)} `)}\n${orange(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; + const styleOption = (option: Option, active: boolean) => { + const selected = this.value.includes(option.value); + if (active && selected) { + return opt(option, "active-selected"); + } + if (selected) { + return opt(option, "selected"); + } + return opt(option, active ? "active" : "inactive"); + }; + + switch (this.state) { + case "submit": { + return `${title}${controls}${c.gray(S_BAR)} ${ + this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, "submitted")) + .join(c.dim(", ")) || c.dim("none") + }`; + } + case "cancel": { + const label = this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, "cancelled")) + .join(c.dim(", ")); + return `${title}${controls}${c.gray(S_BAR)} ${ + label.trim() ? `${label}\n${c.gray(S_BAR)}` : "" + }`; + } + case "error": { + const footer = this.error + .split("\n") + .map((ln, i) => + i === 0 ? `${c.yellow(S_BAR_END)} ${c.yellow(ln)}` : ` ${ln}`, + ) + .join("\n"); + return `${title + controls + c.yellow(S_BAR)} ${limitOptions({ + output: opts.output, + options: this.options, + cursor: this.cursor, + maxItems: opts.maxItems, + style: styleOption, + }).join(`\n${c.yellow(S_BAR)} `)}\n${footer}\n`; + } + default: { + return `${title}${controls}${orange(S_BAR)} ${limitOptions({ + output: opts.output, + options: this.options, + cursor: this.cursor, + maxItems: opts.maxItems, + style: styleOption, + }).join(`\n${orange(S_BAR)} `)}\n${orange(S_BAR_END)}\n`; + } + } + }, + }).prompt() as Promise; }; export async function loader( diff --git a/packages/core/package.json b/packages/core/package.json index ce8cf06..417222c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@orange-js/orange", - "version": "0.2.1", + "version": "0.3.0", "description": "", "keywords": [], "author": "zeb@zebulon.dev", @@ -21,39 +21,35 @@ "import": "./dist/client.js", "types": "./dist/client.d.ts" }, + "./ssr": { + "import": "./dist/ssr.js", + "types": "./dist/ssr.d.ts" + }, "./server": { "import": "./dist/server.js", "types": "./dist/server.d.ts" }, - "./server-entry": { - "import": "./dist/server-entry.js", - "types": "./dist/server-entry.d.ts" - }, "./modules": { "types": "./src/modules.d.ts" }, - "./workflows": { - "import": "./dist/workflows.js", - "types": "./dist/workflows.d.ts" - }, - "./route-module": { - "import": "./dist/route-module.js", - "types": "./dist/route-module.d.ts" - }, "./hono": { "import": "./dist/hono.js", "types": "./dist/hono.d.ts" } }, + "optionalDependencies": { + "@cloudflare/actors": "0.0.1-beta.1" + }, "peerDependencies": { "react": ">=19", "react-dom": ">=19" }, "devDependencies": { + "@cloudflare/actors": "0.0.1-beta.1", "@cloudflare/workers-types": "^4.20250413.0", "@types/node": "^22.13.0", - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "typescript": "^5.7.2" }, "files": [ @@ -62,7 +58,6 @@ "LICENSE" ], "dependencies": { - "hono": "^4.6.20", - "react-router": "=7.6.0" + "hono": "^4.6.20" } } diff --git a/packages/core/src/actor.tsx b/packages/core/src/actor.tsx new file mode 100644 index 0000000..bf5f1ba --- /dev/null +++ b/packages/core/src/actor.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import { Actor } from "@cloudflare/actors"; +import { RscPayload } from "./server.js"; +import * as rsc from "@vitejs/plugin-rsc/rsc"; +import { isValidElement } from "react"; + +type PropsFromDurableObject< + T extends ReactActor, + Env, + K extends keyof T, +> = T[K] extends (arg: infer Z) => React.ReactNode | Promise + ? Z + : never; + +export abstract class ReactActor extends Actor { + async rscStream( + name: string, + props: Record, + ): Promise { + const Component = (this[name as keyof this] as any).bind(this); + const rscStream = rsc.renderToReadableStream({ + // in this example, we always render the same `` + root: , + }); + return rscStream; + } + + static Component = Component; + + abstract Component( + props: Record, + ): React.ReactNode | Promise; +} + +async function Component, Env>( + props: { + durableObject: DurableObjectNamespace; + name: string; + } & PropsFromDurableObject, +) { + if ("children" in props) { + throw new Error("Children are not currently supported"); + } + + for (const key in props) { + const value = props[key as keyof typeof props]; + if (typeof value === "function") { + throw new Error("Functions are not currently supported"); + } + + if (isValidElement(value)) { + throw new Error("React components are not currently supported"); + } + } + + const stub = props.durableObject.get( + props.durableObject.idFromName(props.name), + ); + + const { durableObject, ...rest } = props; + + const rscStream = await stub.rscStream("Component", rest); + const payload = await rsc.createFromReadableStream(rscStream); + + return payload.root; +} diff --git a/packages/core/src/client.tsx b/packages/core/src/client.tsx index 2ecd376..fcddc8c 100644 --- a/packages/core/src/client.tsx +++ b/packages/core/src/client.tsx @@ -1,16 +1,128 @@ +// Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors +// Source: https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.browser.tsx + import React from "react"; +import * as ReactClient from "@vitejs/plugin-rsc/browser"; +import { getRscStreamFromHtml } from "@vitejs/plugin-rsc/rsc-html-stream/browser"; +import * as ReactDOMClient from "react-dom/client"; +import type { RscPayload } from "./server.js"; + +export async function main() { + // stash `setPayload` function to trigger re-rendering + // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) + let setPayload: (v: RscPayload) => void; + + // deserialize RSC stream back to React VDOM for CSR + const initialPayload = await ReactClient.createFromReadableStream( + // initial RSC stream is injected in SSR stream as + getRscStreamFromHtml() + ); + + // browser root component to (re-)render RSC payload as state + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload); + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)); + }, [setPayload_]); + + // re-fetch/render on client side navigation + React.useEffect(() => { + return listenNavigation(() => fetchRscPayload()); + }, []); + + return payload.root; + } + + // re-fetch RSC and trigger re-rendering + async function fetchRscPayload() { + const payload = await ReactClient.createFromFetch( + fetch(window.location.href) + ); + setPayload(payload); + } -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; - -export function hydrate() { - startTransition(() => { - hydrateRoot( - document, - - - , + // register a handler which will be internally called by React + // on server function request after hydration. + ReactClient.setServerCallback(async (id, args) => { + const url = new URL(window.location.href); + const temporaryReferences = ReactClient.createTemporaryReferenceSet(); + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: "POST", + body: await ReactClient.encodeReply(args, { temporaryReferences }), + headers: { + "x-rsc-action": id, + }, + }), + { temporaryReferences } ); + setPayload(payload); + return payload.returnValue; }); + + // hydration + const browserRoot = ( + + + + ); + ReactDOMClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }); + + // implement server HMR by trigering re-fetch/render of RSC upon server code change + if (import.meta.hot) { + import.meta.hot.on("rsc:update", () => { + fetchRscPayload(); + }); + } +} + +// a little helper to setup events interception for client side navigation +function listenNavigation(onNavigation: () => void) { + window.addEventListener("popstate", onNavigation); + + const oldPushState = window.history.pushState; + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args); + onNavigation(); + return res; + }; + + const oldReplaceState = window.history.replaceState; + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args); + onNavigation(); + return res; + }; + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest("a"); + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === "_self") && + link.origin === location.origin && + !link.hasAttribute("download") && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault(); + history.pushState(null, "", link.href); + } + } + document.addEventListener("click", onClick); + + return () => { + document.removeEventListener("click", onClick); + window.removeEventListener("popstate", onNavigation); + window.history.pushState = oldPushState; + window.history.replaceState = oldReplaceState; + }; } diff --git a/packages/core/src/durable-object.ts b/packages/core/src/durable-object.ts deleted file mode 100644 index 1b5df8a..0000000 --- a/packages/core/src/durable-object.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { DurableObject, RpcStub } from "cloudflare:workers"; -import { useLoaderData, type LoaderFunctionArgs } from "react-router"; -import { Context, ContextWithoutExecutionContext } from "./index.js"; - -export type IdentifierFunctionArgs = LoaderFunctionArgs; - -export type DurableLoaderFunctionArgs = { - request: Request; - params: Params; - context: ContextWithoutExecutionContext; -}; - -export type DurableActionFunctionArgs = { - request: Request; - params: Params; - context: ContextWithoutExecutionContext; -}; - -export type WebsocketConnectArgs = { - client: WebSocket; - server: WebSocket; - request: Request; - params: Params; - context: ContextWithoutExecutionContext; -}; - -export class RouteDurableObject extends DurableObject { - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - } - - override async fetch(request: Request): Promise { - if ( - this.webSocketConnect && - request.headers.get("Upgrade") === "websocket" - ) { - // @ts-ignore - const context = await globalThis.__orangeContextFn(this.env); - const pair = new WebSocketPair(); - const client = pair[0]; - const server = pair[1]; - const resp = await this.webSocketConnect({ - client, - server, - request, - context, - params: JSON.parse(request.headers.get("x-orange-params") ?? "{}"), - }); - return resp; - } - - return Response.json({ error: "Method not allowed" }, { status: 405 }); - } - - loader?(args: DurableLoaderFunctionArgs): Promise; - action?(args: DurableActionFunctionArgs): Promise; - webSocketConnect?(args: WebsocketConnectArgs): Promise; -} - -type Syncify = T extends Promise ? U : T; - -type SerializeLoaderFrom< - T extends RouteDurableObject, - Key extends keyof T = "loader" -> = Syncify< - ReturnType any ? T[Key] : never> ->; - -export function useDurableObject< - Obj extends RouteDurableObject ->(): SerializeLoaderFrom { - return useLoaderData() as SerializeLoaderFrom; -} - -function innerDataIn< - Obj extends RouteDurableObject, - Key extends keyof Obj, - Env ->( - durableObject: new (ctx: DurableObjectState, env: Env) => Obj, - method: Key, - nameGetter: - | string - | ((args: IdentifierFunctionArgs) => Promise | string) -): (args: IdentifierFunctionArgs) => Promise> { - return async (args): Promise> => { - // @ts-ignore - const namespace = args.context.cloudflare.env[ - durableObject.name - ] as DurableObjectNamespace; - const name = - typeof nameGetter === "function" - ? await nameGetter({ - ...args, - // @ts-ignore - request: args.request?.clone(), - }) - : nameGetter; - - if (name === undefined) { - throw new Error( - "DurableObject did not have a static name function specified" - ); - } - - const doID = namespace.idFromName(name); - const stub = namespace.get(doID); - - const ret = await (stub as any)[method]({ - ...args, - context: undefined, - }); - - if (ret instanceof Response) { - // @ts-ignore - return ret; - } - - if (ret instanceof RpcStub) { - throw new Error( - "`RpcStub`s cannot be used as loader or action data, wrap your return data in the `data` function to avoid this error." - ); - } - - // @ts-ignore - return ret as SerializeLoaderFrom; - }; -} - -export function loaderIn< - Obj extends RouteDurableObject, - Key extends keyof Obj, - Env ->( - durableObject: new (ctx: DurableObjectState, env: Env) => Obj, - method: Key, - nameGetter: - | string - | ((args: IdentifierFunctionArgs) => Promise | string) -): (args: IdentifierFunctionArgs) => Promise> { - return innerDataIn(durableObject, method, nameGetter); -} - -export function actionIn< - Obj extends RouteDurableObject, - Key extends keyof Obj, - Env ->( - durableObject: new (ctx: DurableObjectState, env: Env) => Obj, - method: Key, - nameGetter: - | string - | ((args: IdentifierFunctionArgs) => Promise | string) -): (args: IdentifierFunctionArgs) => Promise> { - return innerDataIn(durableObject, method, nameGetter); -} diff --git a/packages/core/src/hono.ts b/packages/core/src/hono.ts index c9576d6..e05221d 100644 --- a/packages/core/src/hono.ts +++ b/packages/core/src/hono.ts @@ -1,20 +1,20 @@ import { createMiddleware } from "hono/factory"; -import { app, ServerBuild } from "./server.js"; import { AsyncLocalStorage } from "node:async_hooks"; import { HonoBase } from "hono/hono-base"; +import { PropsWithChildren } from "react"; +import * as server from "./server.js"; const vars = new AsyncLocalStorage(); -export function handler(serverBuild: ServerBuild) { - const orangeApp = app(serverBuild); +export function handler( + layout: (props: PropsWithChildren) => React.ReactNode, + options?: server.AppOptions +) { + const orangeApp = server.app(layout, options); return createMiddleware(async (c) => { return vars.run(c.var, () => { - return orangeApp.fetch( - c.req.raw, - c.env, - c.executionCtx as ExecutionContext - ); + return orangeApp.fetch(c.req.raw); }); }); } diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index cea0306..488449d 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -1,41 +1,7 @@ -import * as React from "react"; - -export { useWebsocket } from "./websocket.js"; -export { - RouteDurableObject, - useDurableObject, - actionIn, - loaderIn, -} from "./durable-object.js"; -export type * from "./durable-object.js"; - -export * from "react-router"; - // @ts-ignore export type CloudflareEnv = Env; -import type * as rr from "react-router"; - -export type ActionFunctionArgs = rr.ActionFunctionArgs & { - env: CloudflareEnv; -}; - -export type LoaderFunctionArgs = rr.LoaderFunctionArgs & { - env: CloudflareEnv; -}; - // biome-ignore lint/complexity/noBannedTypes: export type ContextFrom {}> = Awaited>; -export interface Context { - cloudflare: { - env: CloudflareEnv; - ctx: ExecutionContext; - }; -} - -export type ContextWithoutExecutionContext = Omit & { - cloudflare: { - env: CloudflareEnv; - }; -}; +export interface Context {} diff --git a/packages/core/src/internal-context.ts b/packages/core/src/internal-context.ts new file mode 100644 index 0000000..3b61916 --- /dev/null +++ b/packages/core/src/internal-context.ts @@ -0,0 +1,8 @@ +import { AsyncLocalStorage } from "async_hooks"; + +export type InternalContext = { + request: Request; + params: Record; +}; + +export const internalContext = new AsyncLocalStorage(); diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts deleted file mode 100644 index 188491e..0000000 --- a/packages/core/src/internal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -export const _env = new AsyncLocalStorage(); - -export function env() { - return _env.getStore(); -} diff --git a/packages/core/src/modules.d.ts b/packages/core/src/modules.d.ts index 058b58c..aec3997 100644 --- a/packages/core/src/modules.d.ts +++ b/packages/core/src/modules.d.ts @@ -2,30 +2,20 @@ declare module "virtual:orange/entrypoints" {} declare module "virtual:orange/client-manifest" {} -declare module "virtual:orange/server-bundle" { - import type { ServerBuild } from "react-router"; +declare module "virtual:orange/routes" { + import type * as React from "react"; - export const assets: ServerBuild["assets"]; - export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; - export const basename: ServerBuild["basename"]; - export const entry: ServerBuild["entry"]; - export const future: ServerBuild["future"]; - export const isSpaMode: ServerBuild["isSpaMode"]; - export const publicPath: ServerBuild["publicPath"]; - export const routes: ServerBuild["routes"]; - export const apiRoutes: Record< - string, - { - default: { - fetch: ( - request: Request, - env: unknown, - ctx: ExecutionContext, - ) => Promise; - }; - } - >; - export const prerender: string[]; - export const ssr: true; - export const routeDiscovery: ServerBuild["routeDiscovery"]; + type ReactComponent = (props: { + request: Request; + params: Record; + }) => React.ReactNode | Promise; + + type Module = { + default: ReactComponent | typeof import("./actor.tsx").ReactActor; + }; + + export const routes: { + pattern: URLPattern; + module: Module; + }[]; } diff --git a/packages/core/src/route-module.ts b/packages/core/src/route-module.ts deleted file mode 100644 index 04ce5ce..0000000 --- a/packages/core/src/route-module.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { FC } from "react"; -import type { CloudflareEnv, Context } from "./index.js"; - -import type * as RR from "react-router/route-module"; - -type BaseServerArgs = { - params: T["params"]; - context: Context; - request: Request; - env: CloudflareEnv; -}; - -type BaseClientArgs = { - params: T["params"]; - request: Request; - env: CloudflareEnv; -}; - -type Func = (...args: any[]) => any; - -type RouteModule = { - meta?: Func; - links?: Func; - headers?: Func; - loader?: Func; - clientLoader?: Func; - action?: Func; - clientAction?: Func; - HydrateFallback?: unknown; - default?: unknown; - ErrorBoundary?: unknown; - [key: string]: unknown; -}; - -type RouteInfo = { - parents: RouteInfo[]; - module: RouteModule; - id: unknown; - file: string; - path: string; - params: unknown; - loaderData: unknown; - actionData: unknown; -}; - -export type { - CreateLoaderData, - CreateActionData, -} from "react-router/route-module"; - -export type LinkDescriptors = RR.LinkDescriptors; -export type LinksFunction = () => LinkDescriptors; - -export type MetaArgs = RR.CreateMetaArgs; -export type MetaDescriptors = RR.MetaDescriptors; - -export type HeadersArgs = RR.HeadersArgs; -export type HeadersFunction = (args: HeadersArgs) => Headers | HeadersInit; - -export type LoaderData = RR.CreateLoaderData; -export type LoaderArgs = BaseServerArgs; -export type ClientLoaderArgs = BaseClientArgs; - -export type ActionData = RR.CreateActionData; -export type ActionArgs = BaseServerArgs; -export type ClientActionArgs = BaseClientArgs; - -export type Component = FC>; -export type ComponentProps = RR.CreateComponentProps; -export type ErrorBoundaryProps = - RR.CreateErrorBoundaryProps; -export type HydrateFallbackProps = - RR.CreateHydrateFallbackProps; diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts new file mode 100644 index 0000000..7869c58 --- /dev/null +++ b/packages/core/src/router.ts @@ -0,0 +1,12 @@ +export type Route = { + pattern: URLPattern; +}; + +export function router( + routes: T[] +): (request: Request) => T | undefined { + return (request: Request) => { + const url = new URL(request.url); + return routes.find((route) => route.pattern.test(url)); + }; +} diff --git a/packages/core/src/server-entry.tsx b/packages/core/src/server-entry.tsx deleted file mode 100644 index 7ac2b3d..0000000 --- a/packages/core/src/server-entry.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -// @ts-ignore -import { renderToReadableStream } from "react-dom/server.edge"; -import { type EntryContext, ServerRouter } from "react-router"; - -export default async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - routerContext: EntryContext, -) { - let status = responseStatusCode; - - const body = await renderToReadableStream( - , - { - signal: request.signal, - onError(error: unknown) { - // Log streaming rendering errors from inside the shell - console.error(error); - status = 500; - }, - }, - ); - - responseHeaders.set("Content-Type", "text/html"); - return new Response(body, { - headers: responseHeaders, - status, - }); -} diff --git a/packages/core/src/server.tsx b/packages/core/src/server.tsx index 48b155e..1f13ab7 100644 --- a/packages/core/src/server.tsx +++ b/packages/core/src/server.tsx @@ -1,48 +1,23 @@ +// Adapted from: +// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.rsc.tsx + +import * as React from "react"; +import * as ReactServer from "@vitejs/plugin-rsc/rsc"; +import type { ReactFormState } from "react-dom/client"; +import type { ErrorInfo } from "react"; +import { router } from "./router.js"; + +import { ReactComponent, routes } from "virtual:orange/routes"; +import { ReactActor } from "./actor.js"; import { env } from "cloudflare:workers"; -import { - createRequestHandler, - createStaticHandler, - type ServerBuild as RRServerBuild, - type RouteObject, -} from "react-router"; - -export interface ServerBuild extends RRServerBuild { - apiRoutes: Record< - string, - { - default: { - fetch: ( - request: Request, - env: unknown, - ctx: ExecutionContext - ) => Promise; - }; - } - >; -} +import { CloudflareEnv } from "./index.js"; +import { internalContext } from "./internal-context.js"; -// @ts-ignore -import { _env } from "./internal.js"; -import { Hono } from "hono"; -import { CloudflareEnv, Context } from "./index.js"; -import { createMiddleware } from "hono/factory"; -import { AsyncLocalStorage } from "async_hooks"; - -const contextStorage = new AsyncLocalStorage(); - -function isProbablyHono(obj: object) { - const honoKeys = [ - "routes", - "_basePath", - "route", - "mount", - "errorHandler", - "all", - "get", - "post", - ]; - - return honoKeys.every((key) => key in obj); +export interface Context { + cloudflare: { + env: CloudflareEnv; + ctx: ExecutionContext; + }; } export type AppOptions = { @@ -51,129 +26,178 @@ export type AppOptions = { ) => Omit | Promise>; }; -export function app(serverBuild: ServerBuild, options?: AppOptions) { - const contextFn = options?.context ?? ((env) => ({})); - // @ts-ignore - globalThis.__orangeContextFn = contextFn; - const globalMiddleware: Array< - ( - request: Request, - env: CloudflareEnv - ) => Promise - // @ts-ignore - > = globalThis.middlewareStages ?? []; - - wrapLoadersAndActions(serverBuild); - const handler = createRequestHandler(serverBuild); - const routeObjects: RouteObject[] = Object.values(serverBuild.routes) - .filter((it) => it !== undefined) - .map((route) => ({ - id: route.id, - path: route.path, - index: route.index, - loader: route.module.loader, - caseSensitive: route.caseSensitive, - })); - - // This is a big ol' hack, but I think it's okay - const { queryRoute } = createStaticHandler(routeObjects); - - const fetch = async (request: Request, env: unknown) => { - return await _env.run(env, async () => { - const context = contextStorage.getStore(); - if (!context) { - throw new Error("No context found for request"); - } - - if (request.headers.get("upgrade") === "websocket") { - return await queryRoute(request, { requestContext: context }); - } - - // @ts-ignore - return await handler(request, context); - }); - }; - - const app = new Hono(); +export type RscPayload = { + root: React.ReactNode; + returnValue?: unknown; + formState?: ReactFormState; +}; - for (const middleware of globalMiddleware) { - app.use( - createMiddleware(async (c, next) => { - const response = await middleware(c.req.raw, c.env); - if (response !== null && response !== undefined) { - return response; - } +type Layout = (props: { children: React.ReactNode }) => React.ReactNode; - return await next(); - }) - ); +export async function request() { + const request = internalContext.getStore()?.request; + if (!request) { + throw new Error("Not within request context"); } - for (const [path, module] of Object.entries(serverBuild.apiRoutes)) { - const { default: handler } = module; - if (isProbablyHono(handler)) { - // @ts-ignore - app.route(`/${path}`, handler); + return request; +} + +async function handler( + request: Request, + Layout: Layout +): Promise { + const isAction = request.method === "POST"; + let returnValue: unknown | undefined; + let formState: ReactFormState | undefined; + let temporaryReferences: unknown | undefined; + + if (isAction) { + // x-rsc-action header exists when action is called via `ReactClient.setServerCallback`. + const actionId = request.headers.get("x-rsc-action"); + if (actionId) { + const contentType = request.headers.get("content-type"); + const body = contentType?.startsWith("multipart/form-data") + ? await request.formData() + : await request.text(); + temporaryReferences = ReactServer.createTemporaryReferenceSet(); + const args = await ReactServer.decodeReply(body, { temporaryReferences }); + const action = await ReactServer.loadServerAction(actionId); + returnValue = await action.apply(null, args); } else { - app.mount(`/${path}`, module.default.fetch, { - // By default Hono rewrites the path for mounted handlers, but we want to keep the - // route as-is for API handlers. - replaceRequest: (req) => req, - }); + // otherwise server function is called via `
` + // before hydration (e.g. when javascript is disabled). + // aka progressive enhancement. + const formData = await request.formData(); + const decodedAction = await ReactServer.decodeAction(formData); + const result = await decodedAction(); + formState = await ReactServer.decodeFormState(result, formData); } } - app.mount("/", fetch); - - return { - async fetch(request: Request, env: CloudflareEnv, ctx: ExecutionContext) { - const baseContext = { cloudflare: { env, ctx } }; - const context = { ...baseContext, ...(await contextFn(env)) }; - return await contextStorage.run(context, () => - app.fetch(request, env, ctx) - ); - }, - }; -} + const route = router(routes)(request); + if (!route) { + return undefined; + } -function wrapLoadersAndActions(build: ServerBuild) { - for (const route of Object.values(build.routes)) { - if (route === undefined) { - continue; - } + const match = route.pattern.exec(request.url); + const params = match?.pathname.groups ?? {}; - const module = { ...route.module }; - const { loader, action } = module; + return await internalContext.run({ request, params }, async () => { + const { default: maybeComponent } = route.module; + let Component: (props: { + request: Request; + params: Record; + }) => React.ReactNode | Promise; - if (loader) { - module.loader = (opts: object) => + if (isReactActor(maybeComponent)) { + const name = maybeComponent.nameFromRequest(request); + Component = () => ( // @ts-ignore - loader({ ...opts, env: _env.getStore() }); + + ); + } else { + Component = maybeComponent; } - if (action) { - module.action = (opts: object) => - // @ts-ignore - action({ ...opts, env: _env.getStore() }); - } + return await rscResponse({ + root: ( + + + + ), + request, + returnValue, + formState, + }); + }); +} + +type RscResponseOptions = { + root: React.ReactNode; + request: Request; + returnValue?: unknown; + formState?: ReactFormState; +}; - route.module = module; +async function rscResponse({ + root, + request, + returnValue, + formState, +}: RscResponseOptions) { + const rscStream = ReactServer.renderToReadableStream( + { + // in this example, we always render the same `` + root, + returnValue, + formState, + }, + { + onError(error: unknown, errorInfo: ErrorInfo) { + console.error("Error during RSC streaming", error, errorInfo); + }, + } + ); + + const url = new URL(request.url); + const isRscRequest = + (!request.headers.get("accept")?.includes("text/html") && + !url.searchParams.has("__html")) || + url.searchParams.has("__rsc"); + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + "content-type": "text/x-component;charset=utf-8", + vary: "accept", + }, + }); } + + const { renderHTML } = await import.meta.viteRsc.loadModule< + typeof import("./ssr.js") + >("ssr", "index"); + + const htmlStream = await renderHTML(rscStream, { + formState, + // allow quick simulation of javscript disabled browser + debugNojs: url.searchParams.has("__nojs"), + onError(error: unknown, errorInfo: ErrorInfo) { + console.error("Error during RSC serialization", error, errorInfo); + }, + }); + + return new Response(htmlStream, { + headers: { + "Content-type": "text/html", + vary: "accept", + }, + }); } -/** - * Get the context for the current invocation. - * - * This function is useful for getting the context outside of a data loader or action. - * - * @returns The context for the current invocation. - */ -export async function context(): Promise { - const contextInAls = contextStorage.getStore(); - if (!contextInAls) { - // @ts-ignore - return await globalThis.__orangeContextFn(env); - } +import.meta.hot?.accept(); + +export function app(Layout: Layout, options?: AppOptions) { + return { + async fetch(request: Request) { + try { + return ( + (await handler(request, Layout)) ?? + new Response("Not found", { status: 404 }) + ); + } catch (error) { + return new Response("Error", { status: 500 }); + } + }, + }; +} - return contextInAls; +function isReactActor( + maybeComponent: ReactComponent | typeof ReactActor +): maybeComponent is typeof ReactActor { + return ( + typeof maybeComponent === "function" && + Object.getPrototypeOf(maybeComponent) === ReactActor + ); } diff --git a/packages/core/src/ssr.tsx b/packages/core/src/ssr.tsx new file mode 100644 index 0000000..ae8d270 --- /dev/null +++ b/packages/core/src/ssr.tsx @@ -0,0 +1,72 @@ +// Adapted from: +// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.ssr.tsx + +import React, { ErrorInfo } from "react"; +import { injectRscStreamToHtml } from "@vitejs/plugin-rsc/rsc-html-stream/ssr"; +import type { ReactFormState } from "react-dom/client"; +import * as ReactClient from "@vitejs/plugin-rsc/ssr"; +import * as ReactDOMServer from "react-dom/server.edge"; +import type { RscPayload } from "./server.js"; + +export type RenderHTML = typeof renderHTML; + +export class SSRError extends Error { + errorInfo: ErrorInfo; + + constructor(message: string, errorInfo: ErrorInfo, cause?: unknown) { + super(message); + this.name = "SSRError"; + this.errorInfo = errorInfo; + this.cause = cause; + } +} + +export async function renderHTML( + rscStream: ReadableStream, + options?: { + formState?: ReactFormState; + nonce?: string; + debugNojs?: boolean; + onError?: (error: unknown, errorInfo: ErrorInfo) => void; + } +) { + // duplicate one RSC stream into two. + // - one for SSR (ReactClient.createFromReadableStream below) + // - another for browser hydration payload by injecting . + const [rscStream1, rscStream2] = rscStream.tee(); + + // deserialize RSC stream back to React VDOM + let payload: Promise; + function SsrRoot() { + // deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDomServer preinit/preloading to work + payload ??= ReactClient.createFromReadableStream(rscStream1); + const { root } = React.use(payload); + return root; + } + + // render html (traditional SSR) + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent("index"); + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent: options?.debugNojs + ? undefined + : bootstrapScriptContent, + nonce: options?.nonce, + onError: options?.onError, + // no types + ...{ formState: options?.formState }, + }); + + let responseStream: ReadableStream = htmlStream; + if (!options?.debugNojs) { + // initial RSC stream is injected in HTML stream as + responseStream = responseStream.pipeThrough( + injectRscStreamToHtml(rscStream2, { + nonce: options?.nonce, + }) + ); + } + + return responseStream; +} diff --git a/packages/core/src/websocket.ts b/packages/core/src/websocket.ts deleted file mode 100644 index 9dc8419..0000000 --- a/packages/core/src/websocket.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useLocation, useMatches } from "react-router"; -import { bail } from "./util.js"; -import { useEffect, useState } from "react"; - -type OnMessage = (event: MessageEvent) => void; - -export function useWebsocket(callback: OnMessage) { - const location = useLocation(); - const route = useMatches().at(-1) ?? bail("No route"); - - const [webSocket, setWebSocket] = useState(null); - - useEffect(() => { - const webSocket = new WebSocket( - `${location.pathname}?x-route-id=${route.id}`, - ); - setWebSocket(webSocket); - - webSocket.onmessage = callback; - - return () => { - webSocket.close(); - }; - }, [route.id, location.pathname, callback]); - - return (message: string | ArrayBuffer | ArrayBufferLike) => - webSocket?.send(message); -} -``; diff --git a/packages/core/src/workflows.ts b/packages/core/src/workflows.ts deleted file mode 100644 index 669b9b7..0000000 --- a/packages/core/src/workflows.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { WorkflowEntrypoint } from "cloudflare:workers"; -import { CloudflareEnv } from "./index.js"; - -// @ts-ignore -import { env } from "./internal.js"; - -export async function start< - Wrkflow extends WorkflowEntrypoint, - Params extends {}, ->( - workflow: new (ctx: ExecutionContext, env: CloudflareEnv) => Wrkflow, - id: string, - params: Params, -): Promise; -export async function start< - Wrkflow extends WorkflowEntrypoint, - Params extends {}, ->( - workflow: new (ctx: ExecutionContext, env: CloudflareEnv) => Wrkflow, - params: Params, -): Promise; -export async function start< - Wrkflow extends WorkflowEntrypoint, - Params extends {}, ->( - workflow: new (ctx: ExecutionContext, env: CloudflareEnv) => Wrkflow, - idOrParams: string | Params, - params?: Params, -): Promise { - const workflowClassName = workflow.name; - const e = env() as { [key: string]: Workflow }; - - if (typeof idOrParams === "string") { - return await e[workflowClassName].create({ - id: idOrParams, - params, - }); - } else { - return await e[workflowClassName].create({ - params: idOrParams, - }); - } -} - -export async function get< - Wrkflow extends WorkflowEntrypoint, ->( - workflow: new (ctx: ExecutionContext, env: CloudflareEnv) => Wrkflow, - id: string, -): Promise { - const workflowClassName = workflow.name; - const e = env() as { [key: string]: Workflow }; - return await e[workflowClassName].get(id); -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 1a76aa9..93f6381 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,7 +1,12 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["@cloudflare/workers-types", "@types/node"], - "outDir": "dist" - } -} \ No newline at end of file + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": [ + "vite/client", + "@vitejs/plugin-rsc/types", + "@cloudflare/workers-types", + "@types/node" + ], + "outDir": "dist" + } +} diff --git a/packages/vite/package.json b/packages/vite/package.json index b14ff92..7dca116 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,7 +1,7 @@ { "name": "@orange-js/vite", "type": "module", - "version": "0.2.1", + "version": "0.3.0", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -17,6 +17,10 @@ "./routes": { "import": "./dist/routes.js", "types": "./dist/routes.d.ts" + }, + "./config": { + "import": "./dist/config.js", + "types": "./dist/config.d.ts" } }, "keywords": [], @@ -33,16 +37,19 @@ "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", + "@babel/plugin-proposal-decorators": "^7.28.0", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "@cloudflare/vite-plugin": "^1.9.0", - "@react-router/dev": "^7.2.0", - "@react-router/fs-routes": "^7.2.0", "@swc-node/core": "^1.13.3", + "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-rsc": "^0.4.12", "dedent": "^1.5.3", "es-module-lexer": "^1.6.0", "minimatch": "^10.0.1", - "react-refresh": "^0.16.0" + "react-refresh": "^0.16.0", + "vite-node": "^3.2.4", + "vitest": "^3.2.4" }, "files": [ "dist", diff --git a/packages/vite/src/assets.ts b/packages/vite/src/assets.ts deleted file mode 100644 index cfb5d8f..0000000 --- a/packages/vite/src/assets.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Manifest } from "vite"; -import * as crypto from "node:crypto"; -import type { Context } from "./index.js"; -import type { RouteManifestEntry } from "./routes.js"; -import { mapObject, unreachable } from "./util.js"; -import { virtualInjectHmrRuntime } from "./plugins/hmr.js"; -import { browserManifestVirtualModule } from "./plugins/dev-manifest.js"; - -export function releaseAssets(ctx: Context) { - const routes = ctx.componentRoutes ?? unreachable(); - const manifest = ctx.clientManifest ?? unreachable(); - - const assetRoutes = mapObject(routes, (route) => - mapManifestRoute(route, manifest), - ); - - const entryChunk = - Object.values(manifest).find((it) => it.isEntry) ?? unreachable(); - const entry = { - module: `/${entryChunk.file}`, - imports: resolve(manifest, entryChunk.imports), - css: resolve(manifest, entryChunk.css), - }; - - const version = crypto - .createHash("sha256") - .update(JSON.stringify({ entry, routes })) - .digest("hex") - .slice(0, 8); - - return { - entry, - routes: assetRoutes, - url: `/assets/manifest-${version}.js`, - version, - }; -} - -function mapManifestRoute(route: RouteManifestEntry, manifest: Manifest) { - const chunk = manifest[route.file]; - - return { - id: route.id, - parentId: route.parentId, - path: route.path, - index: route.index ?? false, - caseSensitive: true, - hasLoader: route.hasLoader ?? false, - hasAction: route.hasAction ?? false, - hasClientLoader: route.hasClientLoader ?? false, - hasClientAction: route.hasClientAction ?? false, - hasErrorBoundary: route.hasErrorBoundary ?? false, - module: `/${chunk.file}`, - imports: resolve(manifest, chunk.imports), - css: resolve(manifest, chunk.css), - }; -} - -function resolve(manifest: Manifest, items: string[] | undefined): string[] { - const unresolved = items ?? []; - - return unresolved.map((it) => { - const chunk = manifest[it]; - if (!chunk) { - // throw new Error(`Could not find chunk "${it}" in manifest`); - return `/${it}`; - } - - return `/${chunk.file}`; - }); -} - -export function devAssets(ctx: Context) { - const routes = ctx.componentRoutes ?? unreachable(); - const assetRoutes = mapObject(routes, (route) => ({ - id: route.id, - parentId: route.parentId, - path: route.path, - index: route.index ?? false, - caseSensitive: true, - hasLoader: route.hasLoader ?? false, - hasAction: route.hasAction ?? false, - hasClientLoader: route.hasClientLoader ?? false, - hasClientAction: route.hasClientAction ?? false, - hasErrorBoundary: route.hasErrorBoundary ?? false, - module: `/${route.file}`, - imports: [], - })); - - return { - entry: { - module: "/app/entry.client.ts", - imports: [], - }, - hmr: { - runtime: virtualInjectHmrRuntime.url, - }, - routes: assetRoutes, - url: browserManifestVirtualModule.url, - version: "dev", - }; -} diff --git a/packages/vite/src/config.ts b/packages/vite/src/config.ts new file mode 100644 index 0000000..443a9f5 --- /dev/null +++ b/packages/vite/src/config.ts @@ -0,0 +1,42 @@ +import fs from "node:fs"; +import { Route } from "./routing/index.js"; +import { execVite } from "./vite-exec.js"; +import { fsRoutes } from "./routing/fs-routes.js"; + +export type Config = { + /** + * The routes in the application. + */ + routes?: Route[]; +}; + +export type ResolvedConfig = Required; + +let _configPromise: Promise | undefined; + +const defaultConfig: ResolvedConfig = { + routes: fsRoutes(), +}; + +async function resolveConfigImpl(): Promise { + for (const file of ["orange.config.ts", "orange.config.js"]) { + if (fs.existsSync(file)) { + const mod = await execVite(file); + const config = mod.default; + if (!config) { + console.error("orange.config.ts did not export a default config"); + process.exit(1); + } + return { ...defaultConfig, ...config }; + } + } + + return defaultConfig; +} + +export async function resolveConfig(): Promise { + if (!_configPromise) { + _configPromise = resolveConfigImpl(); + } + return _configPromise; +} diff --git a/packages/vite/src/entrypoints/entry.browser.tsx b/packages/vite/src/entrypoints/entry.browser.tsx new file mode 100644 index 0000000..91cd34f --- /dev/null +++ b/packages/vite/src/entrypoints/entry.browser.tsx @@ -0,0 +1,4 @@ +// @ts-nocheck +import { main } from "@orange-js/orange/client"; + +await main(); diff --git a/packages/vite/src/entrypoints/entry.ssr.tsx b/packages/vite/src/entrypoints/entry.ssr.tsx new file mode 100644 index 0000000..71ddd14 --- /dev/null +++ b/packages/vite/src/entrypoints/entry.ssr.tsx @@ -0,0 +1,2 @@ +// @ts-nocheck +export * from "@orange-js/orange/ssr"; diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index eac5414..c33cd56 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -1,126 +1,91 @@ -import type { Manifest, Plugin } from "vite"; +import type { PluginOption } from "vite"; + import { cloudflare } from "@cloudflare/vite-plugin"; -import { ApiRoute, loadRoutes, type RouteManifest } from "./routes.js"; -import { durableObjectRoutes } from "./plugins/durable-objects.js"; -import { workerStub } from "./plugins/worker-stub.js"; -import { clientBuilder, serverBuilder } from "./plugins/build.js"; -import { serverBundle } from "./plugins/server-bundle.js"; -import { hmr } from "./plugins/hmr.js"; -import { flatRoutes } from "@react-router/fs-routes"; +import rsc from "@vitejs/plugin-rsc"; +import react from "@vitejs/plugin-react"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { configPlugin } from "./plugins/config.js"; +import { routesPlugin } from "./plugins/routes.js"; import { isolation } from "./plugins/isolation.js"; -import { removeDataStubs } from "./plugins/remove-data-stubs.js"; -import { entrypoints } from "./plugins/entrypoints.js"; -import { internal } from "./plugins/internal.js"; -import { routeReload } from "./plugins/route-reload.js"; -import { devManifestPlugin } from "./plugins/dev-manifest.js"; -import { agentsMiddlewareInjector, agentsClientStub } from "./plugins/agents.js"; -import { decoratorPlugin } from "./plugins/decorator.js"; +import { Config, resolveConfig } from "./config.js"; -export type MiddlewareArgs = { - request: Request; - next: () => Promise; -}; +export * from "./routing/fs-routes.js"; -export type Context = { - componentRoutes: RouteManifest | undefined; - apiRoutes: ApiRoute[] | undefined; - clientManifest: Manifest | undefined; +export type OrangeRSCPluginOptions = { + cloudflare?: Parameters[0]; }; -const ctx: Context = { - componentRoutes: undefined, - apiRoutes: [], - clientManifest: undefined, -}; +export default function orange( + options: OrangeRSCPluginOptions +): PluginOption[] { + let _config: Config; -export type PluginConfig = { - cloudflare?: Parameters[0]; - /** - * Glob patterns for API routes. - * @default ["api*.{ts,js}"] - */ - apiRoutePatterns?: string[]; -}; + const config = () => _config; -export default function ({ - apiRoutePatterns = ["api*.{ts,js}"], - cloudflare: cloudflareCfg, -}: PluginConfig = {}): Plugin[] { return [ - cloudflare( - cloudflareCfg ?? { viteEnvironment: { name: "ssr" } }, - ) as unknown as Plugin, - { - name: "orange:app-builder", - config(config) { - return { - ...config, - builder: { - async buildApp(builder) { - const { client, ssr } = builder.environments; - await builder.build(client); - await builder.build(ssr); - } - } - } - }, - }, { name: "orange:settings", // @ts-ignore - this is a magic property used for the orange CLI orangeOptions: { - apiRoutePatterns, - cloudflare: cloudflareCfg, + cloudflare: options.cloudflare, }, - }, - { - name: "orange:route-plugin", - enforce: "pre", - async config(userConfig, env) { - globalThis.__reactRouterAppDirectory = "app"; - const routes = await flatRoutes(); - const { manifest, apiRoutes } = loadRoutes(routes, apiRoutePatterns); - ctx.componentRoutes = manifest; - ctx.apiRoutes = apiRoutes; - - if (env.mode === "production") { - return; - } - - return { - ...userConfig, - build: { - ...userConfig.build, - rollupOptions: { - ...userConfig.build?.rollupOptions, - external: ["cloudflare:workers"], - }, - }, - optimizeDeps: { - ...userConfig.optimizeDeps, - exclude: [ - "cloudflare:workers", - "cloudflare:env", - ...(userConfig.optimizeDeps?.exclude ?? []), - ], - } - }; + async config() { + _config = await resolveConfig(); }, }, - clientBuilder(ctx), - serverBuilder(ctx), - workerStub(), - durableObjectRoutes(ctx), - entrypoints(ctx), - serverBundle(ctx), - removeDataStubs(ctx), - routeReload(), - devManifestPlugin(ctx), - agentsMiddlewareInjector(ctx), - agentsClientStub(ctx), - decoratorPlugin(), - ...internal(), - ...isolation(), - ...hmr(), + isolation(), + configPlugin(), + react({ + babel: { + plugins: [ + ["@babel/plugin-proposal-decorators", { version: "2023-11" }], + ], + }, + }), + rsc({ + entries: { + client: entrypoint( + "entry.browser", + "node_modules/@orange-js/vite/dist/entrypoints/entry.browser.js" + ), + ssr: entrypoint( + "entry.ssr", + "node_modules/@orange-js/vite/dist/entrypoints/entry.ssr.js" + ), + // rsc: entrypoint( + // "entry.rsc", + // "node_modules/@orange-js/vite/dist/entrypoints/entry.rsc.js" + // ), + }, + serverHandler: false, + loadModuleDevProxy: true, + }), + cloudflare( + options.cloudflare ?? { + configPath: "./wrangler.jsonc", + viteEnvironment: { + name: "rsc", + }, + } + ), + routesPlugin(config), ]; } + +function entrypoint(name: string, fallback: string) { + for (const extension of ["tsx", "jsx"]) { + const entrypoint = path.join( + process.cwd(), + "src", + "entrypoints", + `${name}.${extension}` + ); + if (fs.existsSync(entrypoint)) { + return entrypoint; + } + } + + return fallback; +} diff --git a/packages/vite/src/plugins/agents.ts b/packages/vite/src/plugins/agents.ts deleted file mode 100644 index 9c2e5cf..0000000 --- a/packages/vite/src/plugins/agents.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Plugin } from "vite"; -import { Context } from "../index.js"; -import { unreachable } from "../util.js"; -import { resolve } from "node:path"; -import { VirtualModule } from "../virtual-module.js"; - -export function agentsMiddlewareInjector(ctx: Context): Plugin { - return { - name: "orange:agents-middleware-injector", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name !== "client"; - }, - transform(code, id) { - const routes = ctx.componentRoutes ?? unreachable(); - const routeFiles = Object.values(routes).map((route) => - resolve(route.file) - ); - if (!routeFiles.includes(id)) { - return; - } - - const className = agentInCode(code); - if (!className) { - return; - } - - const inject = ` - globalThis.middlewareStages ??= []; - globalThis.middlewareStages.push(async (request, env) => { - const agents = await import("agents"); - try { - const maybeResp = await agents.routeAgentRequest(request, env); - return maybeResp; - } catch (e) { - console.error(e); - return new Response("Internal Server Error", { status: 500 }); - } - }) - `; - return `${code}\n${inject}`; - }, - }; -} - -const agentsVmod = new VirtualModule("agents-stub"); - -export function agentsClientStub(_: Context): Plugin { - return { - name: "orange:agents-resolver", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name === "client"; - }, - resolveId(id) { - if (id === "agents") { - return agentsVmod.id; - } - }, - async load(id) { - if (agentsVmod.is(id)) { - return emptyExports([ - "Agent", - "StreamingResponse", - "getAgentByName", - "routeAgentEmail", - "routeAgentRequest", - "unstable_callable", - "unstable_context", - ]); - } - }, - }; -} - -function agentInCode(contents: string): string | undefined { - const matches = - /export\s+class\s+(\w+)\s+extends\s+(?:Agent|AIChatAgent)/.exec(contents); - if (!matches || !matches[1]) { - return undefined; - } - - return matches[1]; -} - -const emptyExports = (exports: string[]) => { - return exports.map((e) => `export const ${e} = undefined;`).join("\n"); -}; diff --git a/packages/vite/src/plugins/build.ts b/packages/vite/src/plugins/build.ts deleted file mode 100644 index 9360050..0000000 --- a/packages/vite/src/plugins/build.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { assert, unreachable } from "../util.js"; -import type { Context } from "../index.js"; -import type { Manifest, Plugin } from "vite"; -import { releaseAssets } from "../assets.js"; -import { writeFileSync } from "fs"; - -export function clientBuilder(ctx: Context): Plugin { - return { - name: "orange:client-builder", - enforce: "pre", - writeBundle(options, bundle) { - const manifestChunk = bundle[".vite/manifest.json"]; - assert("source" in manifestChunk, "missing manifest chunk"); - const clientManifest: Manifest = JSON.parse( - (manifestChunk?.source as string) ?? unreachable(), - ); - - ctx.clientManifest = clientManifest; - - const manifest = releaseAssets(ctx); - writeFileSync( - `${options.dir}/assets/manifest-${manifest.version}.js`, - `window.__reactRouterManifest=${JSON.stringify(manifest)};`, - ); - }, - applyToEnvironment(environment) { - return environment.name === "client"; - }, - configEnvironment(name, envConfig) { - if (name !== "client") return envConfig; - - const routes = ctx.componentRoutes ?? unreachable(); - return { - ...envConfig, - build: { - manifest: true, - outDir: "dist/client", - target: "es2022", - rollupOptions: { - input: [ - "app/entry.client.ts", - ...Object.values(routes).map((r) => r.file), - ], - preserveEntrySignatures: "exports-only", - }, - }, - optimizeDeps: { - entries: [ - "app/entry.client.ts", - ...Object.values(routes).map((r) => r.file), - ], - include: [ - "react", - "react/jsx-runtime", - "react/jsx-dev-runtime", - "react-dom", - "react-dom/client", - ], - }, - resolve: { - dedupe: ["react", "react-dom", "react-router", "@orange-js/orange"], - }, - }; - }, - }; -} - -export function serverBuilder(ctx: Context): Plugin { - return { - name: "orange:server-builder", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name !== "client"; - }, - configEnvironment(name, config, env) { - if (name === "client") return config; - - const routes = ctx.componentRoutes ?? unreachable(); - - return { - ...config, - build: { - ssr: true, - emitAssets: true, - outDir: `dist/${name}`, - write: true, - target: "es2022", - rollupOptions: { - input: "app/entry.server.ts", - external: ["cloudflare:workers", "node:async_hooks"], - }, - }, - optimizeDeps: { - entries: [ - "app/entry.server.ts", - ...Object.values(routes).map((r) => r.file), - ], - include: [ - "react", - "react/jsx-runtime", - "react/jsx-dev-runtime", - "react-dom", - "react-dom/server.edge", - ], - }, - resolve: { - dedupe: [ - "react", - "react-dom", - "react-router", - "react-dom/server.edge", - "@orange-js/orange", - ], - }, - }; - }, - }; -} diff --git a/packages/vite/src/plugins/config.ts b/packages/vite/src/plugins/config.ts new file mode 100644 index 0000000..e56e546 --- /dev/null +++ b/packages/vite/src/plugins/config.ts @@ -0,0 +1,54 @@ +import type { Plugin } from "vite"; + +export function configPlugin(): Plugin { + return { + name: "orange:config", + config: (config) => { + return { + ...config, + environments: { + rsc: { + build: { + rollupOptions: { + // ensure `default` export only in cloudflare entry output + preserveEntrySignatures: "exports-only", + }, + }, + optimizeDeps: { + exclude: ["virtual:orange/routes"], + }, + }, + ssr: { + keepProcessEnv: false, + build: { + // build `ssr` inside `rsc` directory so that + // wrangler can deploy self-contained `dist/rsc` + outDir: "./dist/rsc/ssr", + }, + optimizeDeps: { + exclude: [ + "virtual:orange/routes", + "cloudflare:workers", + "cloudflare:workerflows", + ], + }, + }, + client: { + build: { + rollupOptions: { + external: ["virtual:orange/routes"], + }, + }, + optimizeDeps: { + exclude: [ + "cloudflare:workers", + "virtual:orange/routes", + "cloudflare:workerflows", + ], + }, + }, + }, + }; + }, + }; +} diff --git a/packages/vite/src/plugins/decorator.ts b/packages/vite/src/plugins/decorator.ts deleted file mode 100644 index b03fd31..0000000 --- a/packages/vite/src/plugins/decorator.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Plugin } from "vite"; -import { ParserConfig, transform } from "@swc/core"; - -const extensionsToParse = [".js", ".jsx", ".ts", ".tsx"]; - -export function decoratorPlugin(): Plugin { - return { - name: "orange:decorator", - async transform(code, id) { - if (!extensionsToParse.some((ext) => id.endsWith(ext))) { - return; - } - - const isTs = id.endsWith(".ts") || id.endsWith(".tsx"); - - const parser: ParserConfig = isTs ? { - syntax: "typescript", - tsx: true, - decorators: true, - } as const : { - syntax: "ecmascript", - jsx: true, - decorators: true, - decoratorsBeforeExport: true, - } as const; - - const output = await transform(code, { - jsc: { - target: "esnext", - parser, - transform: { - decoratorVersion: "2022-03", - }, - }, - }); - - if (!output) { - return; - } - return { code: output.code, map: output.map }; - }, - }; -} diff --git a/packages/vite/src/plugins/dev-manifest.ts b/packages/vite/src/plugins/dev-manifest.ts deleted file mode 100644 index 2b320b9..0000000 --- a/packages/vite/src/plugins/dev-manifest.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Plugin } from "vite"; -import { VirtualModule } from "../virtual-module.js"; -import { Context } from "../index.js"; -import { devAssets } from "../assets.js"; - -export const browserManifestVirtualModule = new VirtualModule("dev-manifest"); - -export function devManifestPlugin(ctx: Context): Plugin { - return { - name: "orange:dev-manifest", - resolveId(source, importer, options) { - if (browserManifestVirtualModule.is(source)) { - return browserManifestVirtualModule.id; - } - }, - load(id) { - if (!browserManifestVirtualModule.is(id)) { - return; - } - - const manifest = devAssets(ctx); - return `window.__reactRouterManifest=${JSON.stringify(manifest)};`; - }, - }; -} diff --git a/packages/vite/src/plugins/durable-objects.ts b/packages/vite/src/plugins/durable-objects.ts deleted file mode 100644 index 24d42a7..0000000 --- a/packages/vite/src/plugins/durable-objects.ts +++ /dev/null @@ -1,287 +0,0 @@ -import type { Plugin } from "vite"; -import { resolve } from "node:path"; - -import type { Context } from "../index.js"; -import { unreachable } from "../util.js"; -import { parse } from "@babel/parser"; -import _traverse from "@babel/traverse"; -import _generate from "@babel/generator"; -import { - arrowFunctionExpression, - assignmentExpression, - awaitExpression, - binaryExpression, - blockStatement, - callExpression, - ClassMethod, - expressionStatement, - Identifier, - identifier, - ifStatement, - isClassDeclaration, - isIdentifier, - isTSParameterProperty, - memberExpression, - objectExpression, - objectProperty, - Pattern, - RestElement, - returnStatement, - spreadElement, - variableDeclaration, - variableDeclarator, -} from "@babel/types"; -const traverse = _traverse.default; -const generate = _generate.default; - -export function durableObjectRoutes(ctx: Context): Plugin { - return { - name: "orange:durable-object-routes", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name !== "client"; - }, - async transform(code, id) { - // Only process JS/TS files - if (!/\.(t|j)sx?$/.test(id)) { - return; - } - - const routes = ctx.componentRoutes ?? unreachable(); - const routeFiles = Object.values(routes).map((route) => - resolve(route.file), - ); - if (!routeFiles.includes(id)) { - return; - } - - const className = durableObjectInCode(code); - if (!className) { - return; - } - - const parsed = parse(code, { - sourceType: "module", - plugins: ["typescript", "jsx", ["decorators", { - decoratorsBeforeExport: true, - allowCallParenthesized: true, - }]], - }); - - let wrapped = false; - - traverse(parsed, { - ExportNamedDeclaration(path) { - const node = path.node; - if (isClassDeclaration(node.declaration)) { - const classDeclaration = node.declaration; - - if ( - isIdentifier(classDeclaration.superClass) && - classDeclaration.superClass.name === "RouteDurableObject" - ) { - traverse( - classDeclaration, - { - ClassMethod(path) { - if (updateDataMethod(path)) { - wrapped = true; - } - }, - }, - path.scope, - path, - ); - } - } - - path.stop(); - }, - }); - - const output = generate(parsed); - return { - code: dataFunctionsSuffix(output.code, className), - map: output.map, - }; - }, - }; -} - -const nonDataMethods = [ - "fetch", - "webSocketConnect", - "webSocketMessage", - "webSocketClose", - "alarm", -]; - -function updateDataMethod(path: _traverse.NodePath): boolean { - const { node } = path; - - if (!isIdentifier(node.key)) { - return false; - } - - if (nonDataMethods.includes(node.key.name)) { - return false; - } - - const params = node.params.filter( - (it): it is Identifier | RestElement | Pattern => - !isTSParameterProperty(it), - ); - const callToBody = callExpression( - arrowFunctionExpression(params, node.body, node.async), - [identifier("opts")], - ); - - node.params = [identifier("opts")]; - node.body = blockStatement([ - expressionStatement( - assignmentExpression( - "=", - memberExpression(identifier("opts"), identifier("context")), - objectExpression([ - spreadElement( - awaitExpression( - callExpression( - memberExpression( - identifier("globalThis"), - identifier("__orangeContextFn"), - ), - [memberExpression(identifier("this"), identifier("env"))], - ), - ), - ), - objectProperty( - identifier("cloudflare"), - objectExpression([ - objectProperty( - identifier("env"), - memberExpression(identifier("this"), identifier("env")), - ), - ]), - ), - ]), - ), - ), - variableDeclaration("const", [ - variableDeclarator( - identifier("ret"), - node.async ? awaitExpression(callToBody) : callToBody, - ), - ]), - ifStatement( - binaryExpression("instanceof", identifier("ret"), identifier("Response")), - blockStatement([returnStatement(identifier("ret"))]), - ), - ifStatement( - binaryExpression("===", identifier("ret"), identifier("undefined")), - blockStatement([returnStatement(identifier("ret"))]), - ), - ifStatement( - callExpression( - memberExpression( - callExpression( - memberExpression( - identifier("Object"), - identifier("getPrototypeOf"), - ), - [identifier("ret")], - ), - identifier("isPrototypeOf"), - ), - [objectExpression([])], - ), - blockStatement([returnStatement(identifier("ret"))]), - ), - returnStatement( - callExpression( - memberExpression(identifier("JSON"), identifier("parse")), - [ - callExpression( - memberExpression(identifier("JSON"), identifier("stringify")), - [identifier("ret")], - ), - ], - ), - ), - ]); - - return true; -} - -const dataFunctionsSuffix = (code: string, className: string) => `${code}\n -export async function loader(args) { - const env = args.context?.cloudflare.env as unknown as Env; - if (!env) { - throw new Error("No env found in context"); - } - - const namespace = env.${className}; - const name = typeof ${className}.id === "string" ? ${className}.id : ${className}.id(args); - if (name === undefined) { - throw new Error("DurableObject did not have a static id function specified"); - } - const doID = namespace.idFromName(name); - const stub = namespace.get(doID); - - if (args.request.headers.get("Upgrade") === "websocket") { - const modifiedRequest = new Request(args.request, { - headers: new Headers({ - ...Object.fromEntries(args.request.headers), - "x-orange-params": JSON.stringify(args.params), - }) - }); - - return await stub.fetch(modifiedRequest); - } - - delete args.context; - delete args.env; - - return await (stub as any).loader(args); -} - -export async function action(args) { - const env = args.context?.cloudflare.env as unknown as Env; - if (!env) { - throw new Error("No env found in context"); - } - - const namespace = env.${className}; - const name = typeof ${className}.id === "string" ? ${className}.id : ${className}.id(args); - if (name === undefined) { - throw new Error("DurableObject did not have a static id function specified"); - } - const doID = namespace.idFromName(name); - const stub = namespace.get(doID); - - if (args.request.headers.get("Upgrade") === "websocket") { - const modifiedRequest = new Request(args.request, { - headers: new Headers({ - ...Object.fromEntries(args.request.headers), - "x-orange-params": JSON.stringify(args.params), - }) - }); - - return await stub.fetch(modifiedRequest); - } - - delete args.context; - delete args.env; - - return await (stub as any).action(args); -}`; - -function durableObjectInCode(contents: string): string | undefined { - const matches = /export\s+class\s+(\w+)\s+extends\s+RouteDurableObject/.exec( - contents, - ); - if (!matches || !matches[1]) { - return undefined; - } - - return matches[1]; -} diff --git a/packages/vite/src/plugins/entrypoints.ts b/packages/vite/src/plugins/entrypoints.ts deleted file mode 100644 index 9dd18a2..0000000 --- a/packages/vite/src/plugins/entrypoints.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Context } from "../index.js"; -import { unreachable } from "../util.js"; -import { VirtualModule } from "../virtual-module.js"; -import { Plugin } from "vite"; - -const ENTRYPOINTS_VIRTUAL_MODULE = new VirtualModule("entrypoints"); - -export function entrypoints(ctx: Context): Plugin { - return { - name: "orange:entrypoints-virtual-module", - enforce: "pre", - resolveId(id) { - if (ENTRYPOINTS_VIRTUAL_MODULE.is(id)) { - return id; - } - }, - load(id) { - if (ENTRYPOINTS_VIRTUAL_MODULE.is(id)) { - const routes = ctx.componentRoutes ?? unreachable(); - let body = ""; - - for (const route of Object.values(routes)) { - // Only process JS/TS files - if (!/\.(t|j)sx?$/.test(route.file)) { - continue; - } - - const exportedClasses = route.exportedClasses ?? []; - if (exportedClasses.length === 0) { - continue; - } - - body += `export { ${exportedClasses.join(", ")} } from "/${ - route.file - }";\n`; - } - - return body; - } - }, - }; -} diff --git a/packages/vite/src/plugins/hmr.ts b/packages/vite/src/plugins/hmr.ts deleted file mode 100644 index 1f13c52..0000000 --- a/packages/vite/src/plugins/hmr.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Plugin } from "vite"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { VirtualModule } from "../virtual-module.js"; - -export const virtualInjectHmrRuntime = new VirtualModule("inject-hmr-runtime"); -const virtualHmrRuntime = new VirtualModule("hmr-runtime"); - -export function hmr(): Plugin[] { - return [ - { - name: "react-router:inject-hmr-runtime", - enforce: "pre", - resolveId(id) { - if (virtualInjectHmrRuntime.is(id)) { - return virtualInjectHmrRuntime.id; - } - }, - async load(id) { - if (!virtualInjectHmrRuntime.is(id)) return; - - return [ - `import RefreshRuntime from "${virtualHmrRuntime.raw}"`, - "RefreshRuntime.injectIntoGlobalHook(window)", - "window.$RefreshReg$ = () => {}", - "window.$RefreshSig$ = () => (type) => type", - "window.__vite_plugin_react_preamble_installed__ = true", - ].join("\n"); - }, - }, - { - name: "react-router:hmr-runtime", - enforce: "pre", - resolveId(id) { - if (virtualHmrRuntime.is(id)) { - return virtualHmrRuntime.id; - } - }, - async load(id) { - if (!virtualHmrRuntime.is(id)) return; - - const reactRefreshDir = path.dirname( - import.meta - .resolve("react-refresh/package.json") - .replace("file://", ""), - ); - const reactRefreshRuntimePath = path.join( - reactRefreshDir, - "cjs/react-refresh-runtime.development.js", - ); - - return [ - "const exports = {}", - await fs.readFile(reactRefreshRuntimePath, "utf8"), - "export default exports", - ].join("\n"); - }, - }, - ]; -} diff --git a/packages/vite/src/plugins/internal.ts b/packages/vite/src/plugins/internal.ts deleted file mode 100644 index 20473c3..0000000 --- a/packages/vite/src/plugins/internal.ts +++ /dev/null @@ -1,51 +0,0 @@ -// TODO: burn this file with fire -import { Plugin } from "vite"; -import { VirtualModule } from "../virtual-module.js"; - -const EMPTY = new VirtualModule("empty"); -const INTERNAL = new VirtualModule("internal"); - -const emptyExports = (exports: string[]) => { - return exports.map((e) => `export const ${e} = undefined;`).join("\n"); -}; - -export function internal(): Plugin[] { - return [ - { - name: "orange:shimmed-workflows", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name === "client"; - }, - resolveId(id) { - if (id === "@orange-js/orange/workflows") { - return EMPTY.id; - } else if (id === "@orange-js/orange/hono") { - return EMPTY.id; - } - }, - load(id) { - if (EMPTY.is(id)) { - return emptyExports(["start", "get", "variables"]); - } - }, - }, - { - name: "orange:shimmed-internal", - enforce: "pre", - applyToEnvironment(environment) { - return environment.name === "client"; - }, - resolveId(id) { - if (id === "./internal.js") { - return INTERNAL.id; - } - }, - load(id) { - if (INTERNAL.is(id)) { - return emptyExports(["env", "contextFn"]); - } - }, - }, - ]; -} diff --git a/packages/vite/src/plugins/isolation.ts b/packages/vite/src/plugins/isolation.ts index 54f3136..59e268f 100644 --- a/packages/vite/src/plugins/isolation.ts +++ b/packages/vite/src/plugins/isolation.ts @@ -51,14 +51,14 @@ export function isolation(): Plugin[] { } const inAppDir = (importPath: string) => - path.resolve(importPath).startsWith(path.resolve("./app")); + path.resolve(importPath).startsWith(path.resolve("./src")); const emptyExports = (exports: string[]) => { return exports .map((e) => e === "default" ? "export default undefined;" - : `export const ${e} = undefined;`, + : `export const ${e} = undefined;` ) .join("\n"); }; diff --git a/packages/vite/src/plugins/remove-data-stubs.ts b/packages/vite/src/plugins/remove-data-stubs.ts deleted file mode 100644 index 523ebff..0000000 --- a/packages/vite/src/plugins/remove-data-stubs.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as path from "node:path"; -import { Plugin } from "vite"; -import { Context } from "../index.js"; -import { unreachable } from "../util.js"; -import { parse } from "@babel/parser"; -import _traverse from "@babel/traverse"; -import { - isClassDeclaration, - isIdentifier, - isVariableDeclaration, - exportNamedDeclaration, - classDeclaration, - classBody, - isFunctionDeclaration, -} from "@babel/types"; -import _generate from "@babel/generator"; -const traverse = _traverse.default; -const generate = _generate.default; - -const namesToStrip = ["action", "loader"]; -const baseClassesToStrip = [ - "WorkerEntrypoint", - "WorkflowEntrypoint", - "RouteDurableObject", - "DurableObject", - "Agent", - "AIChatAgent", - "McpAgent" -]; -const allNamesToStrip = [...namesToStrip, ...baseClassesToStrip]; -const extensionsToParse = [".js", ".jsx", ".ts", ".tsx"]; - -export function removeDataStubs(ctx: Context): Plugin { - return { - name: "orange:remove-data-stubs", - applyToEnvironment(environment) { - return environment.name === "client"; - }, - async transform(code, id) { - const routes = ctx.componentRoutes ?? unreachable(); - const isRouteModule = Object.values(routes) - .map((route) => path.resolve(route.file)) - .some((path) => path === id); - - const hasExtension = extensionsToParse.some((ext) => id.endsWith(ext)); - if (!isRouteModule || !hasExtension) return; - - // Optimization: if the code doesn't contain any of the names we're looking for, skip parsing - if (!allNamesToStrip.some((name) => code.includes(name))) { - return; - } - - const parsed = parse(code, { - sourceType: "module", - plugins: ["typescript", "jsx", ["decorators", { - decoratorsBeforeExport: true, - allowCallParenthesized: true, - }]], - }); - - let stripped = false; - - traverse(parsed, { - ExportNamedDeclaration(path) { - const node = path.node; - - if (isVariableDeclaration(node.declaration)) { - const declarations = node.declaration.declarations; - for (let i = 0; i < declarations.length; i++) { - const declaration = declarations[i]; - - if ( - isIdentifier(declaration.id) && - namesToStrip.includes(declaration.id.name) - ) { - stripped = true; - declarations.splice(i, 1); - } - } - - if (declarations.length === 0) { - path.remove(); - } - } else if (isClassDeclaration(node.declaration)) { - const { superClass } = node.declaration; - - if ( - superClass && - isIdentifier(superClass) && - baseClassesToStrip.includes(superClass.name) - ) { - stripped = true; - - // TODO: remove the class declaration entirely without imports complaining - path.replaceWith( - exportNamedDeclaration( - classDeclaration( - node.declaration.id, - null, - classBody([]), - null, - ), - ), - ); - } - } else if (isFunctionDeclaration(node.declaration)) { - const { id } = node.declaration; - - if (id && isIdentifier(id) && namesToStrip.includes(id.name)) { - stripped = true; - path.remove(); - } - } - }, - }); - - if (stripped) { - const output = generate(parsed); - return output.code; - } - }, - }; -} diff --git a/packages/vite/src/plugins/route-reload.ts b/packages/vite/src/plugins/route-reload.ts deleted file mode 100644 index 2c3ef2d..0000000 --- a/packages/vite/src/plugins/route-reload.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Plugin, ViteDevServer } from "vite"; -import * as path from "node:path"; - -// Prevent double reload for file renames -let reloadCount = 0; - -async function forceReload(server: ViteDevServer, reloadId: number) { - if (reloadId !== reloadCount) { - return; - } - - // TODO: This is a hack to force a full reload - await server.restart(); - server.ws.send({ type: "full-reload" }); -} - -export function routeReload(): Plugin { - return { - name: "orange:reload-routes", - async configureServer(server) { - const routeDir = path.resolve("./app/routes"); - const onFileChange = async (filePath: string) => { - if (filePath.startsWith(routeDir)) { - const reloadId = ++reloadCount; - setTimeout(() => forceReload(server, reloadId), 20); - } - }; - - server.watcher.on("add", onFileChange); - server.watcher.on("unlink", onFileChange); - }, - }; -} diff --git a/packages/vite/src/plugins/routes.ts b/packages/vite/src/plugins/routes.ts new file mode 100644 index 0000000..16a7c0a --- /dev/null +++ b/packages/vite/src/plugins/routes.ts @@ -0,0 +1,56 @@ +import type { Plugin } from "vite"; +import { VirtualModule } from "../virtual-module.js"; +import * as path from "node:path"; +import { fsRoutes } from "../routing/fs-routes.js"; +import { Route } from "../routing/index.js"; +import { Config } from "../config.js"; + +const vmod = new VirtualModule("routes"); + +let _routes: Route[] | undefined; + +export function routesPlugin(config: () => Config): Plugin { + return { + name: "orange:routes", + enforce: "pre", + applyToEnvironment(environment) { + return environment.name === "rsc"; + }, + resolveId(source) { + if (source === "virtual:orange/routes") { + return vmod.id; + } + }, + async load(id) { + const routes = _routes ?? config().routes ?? fsRoutes(); + _routes = routes; + + if (id === vmod.id) { + const ids = Object.fromEntries( + routes.map((route) => [ + route.pattern, + `route_${Math.random().toString(36).substring(2, 15)}`, + ]) + ); + const imports = routes.map((route) => { + return `import * as ${ids[route.pattern]} from "${path.resolve( + route.file + )}";`; + }); + + const routeDeclarations = routes.map( + (route) => + `{ pattern: new URLPattern({ pathname: "${ + route.pattern + }" }), module: ${ids[route.pattern]} }` + ); + + return { + code: `${imports.join("\n")} + export const routes = [${routeDeclarations.join(",\n")}]; + `, + }; + } + }, + }; +} diff --git a/packages/vite/src/plugins/server-bundle.ts b/packages/vite/src/plugins/server-bundle.ts deleted file mode 100644 index a99707c..0000000 --- a/packages/vite/src/plugins/server-bundle.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { type Plugin } from "vite"; -import type { Context } from "../index.js"; -import { VirtualModule } from "../virtual-module.js"; -import { unreachable } from "../util.js"; -import dedent from "dedent"; -import { devAssets, releaseAssets } from "../assets.js"; - -const routesVirtualId = new VirtualModule("server-bundle"); - -/* -This plugin is responsible for generating a `ServerBuild` object from React-Router -so that we can re-use their SSR modules. -*/ -export function serverBundle(ctx: Context): Plugin { - return { - name: "orange:server-bundle", - enforce: "pre", - resolveId(id) { - if (routesVirtualId.is(id)) { - return routesVirtualId.id; - } - }, - async load(id) { - if (!routesVirtualId.is(id)) { - return; - } - - const componentRoutes = ctx.componentRoutes ?? unreachable(); - const componentRouteImports = Object.values(componentRoutes).map( - (route, index) => - `import * as componentRouteModule${index} from "/${route.file}";`, - ); - const componentRouteLiterals = Object.values(componentRoutes).map( - (route, index) => `"${route.id}": { - id: ${JSON.stringify(route.id)}, - parentId: ${JSON.stringify(route.parentId)}, - path: ${JSON.stringify(route.path)}, - index: ${JSON.stringify(route.index)}, - caseSensitive: true, - module: componentRouteModule${index}, - }`, - ); - - const apiRoutes = ctx.apiRoutes ?? unreachable(); - const apiRouteImports = apiRoutes.map( - (route, index) => - `import * as apiRouteModule${index} from "/${route.file}";`, - ); - const apiRouteLiterals = Object.values(apiRoutes).map( - (route, index) => - `"${route.path.replaceAll("$", ":")}": apiRouteModule${index}`, - ); - - const assets = this.environment.mode === "build" ? releaseAssets(ctx) : devAssets(ctx); - - return dedent` - import * as serverModule from "@orange-js/orange/server-entry"; - - ${componentRouteImports.join("\n")} - ${apiRouteImports.join("\n")} - - export const prerender = []; - export const entry = { module: serverModule }; - export const future = { unstable_optimizeDeps: false }; - export const basename = "/"; - export const publicPath = "/"; - export const isSpaMode = false; - export const ssr = true; - export const assetsBuildDirectory = "dist/client"; - export const assets = ${JSON.stringify(assets, null, 2)}; - export const routes = {${componentRouteLiterals.join(",")}}; - export const routeDiscovery = { mode: "lazy", manifestPath: "/__manifest" }; - export const apiRoutes = {${apiRouteLiterals.join(",")}}; - `; - }, - }; -} diff --git a/packages/vite/src/plugins/worker-stub.ts b/packages/vite/src/plugins/worker-stub.ts deleted file mode 100644 index 228ce8f..0000000 --- a/packages/vite/src/plugins/worker-stub.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Plugin } from "vite"; -import { VirtualModule } from "../virtual-module.js"; - -const workersVmod = new VirtualModule("workers-stub"); - -// Replaces the `cloudflare:workers` import with an empty module for the client. -export function workerStub(): Plugin { - return { - name: "orange:worker-stub", - applyToEnvironment(environment) { - return environment.name === "client"; - }, - resolveId(id) { - if (id === "cloudflare:workers") { - return workersVmod.id; - } - }, - load(id) { - if (workersVmod.is(id)) { - return ` - export class DurableObject {}; - export class RpcStub {}; - export class WorkflowEntrypoint {}; - `; - } - }, - }; -} diff --git a/packages/vite/src/routes.ts b/packages/vite/src/routes.ts deleted file mode 100644 index 3ad4de5..0000000 --- a/packages/vite/src/routes.ts +++ /dev/null @@ -1,185 +0,0 @@ -import fs from "node:fs"; -import { flatRoutes } from "@react-router/fs-routes"; -import { unreachable, isEcmaLike } from "./util.js"; -import { minimatch } from "minimatch"; -import { parse } from "@babel/parser"; -import _traverse from "@babel/traverse"; -import { isIdentifier } from "@babel/types"; -const traverse = _traverse.default; - -// TOOD: use AST for this, this is a hack -function loadRoute(file: string) { - if (!isEcmaLike(file)) { - return { - hasLoader: false, - hasAction: false, - hasClientLoader: false, - hasClientAction: false, - } - } - - const contents = fs.readFileSync(file, "utf-8"); - const ast = parse(contents, { - sourceType: "module", - plugins: ["typescript", "jsx", "decorators"], - }); - - const routeInfo = { - hasLoader: false, - hasAction: false, - hasClientLoader: false, - hasClientAction: false, - exportedClasses: [] as string[], - }; - - traverse(ast, { - ExportNamedDeclaration(path) { - const node = path.node; - const declaration = node.declaration; - - if (declaration === undefined || declaration === null) { - return; - } - - if (declaration.type === "ClassDeclaration" && declaration.id) { - routeInfo.exportedClasses.push(declaration.id.name); - - const members: Record = {}; - - traverse(declaration, { - ClassMethod(path) { - const node = path.node; - if (node.kind === "method" && isIdentifier(node.key)) { - members[node.key.name] = node.static; - } - }, - ClassProperty(path) { - const node = path.node; - if (isIdentifier(node.key)) { - members[node.key.name] = node.static; - } - }, - }, path.scope); - - // Ensure that the loader and action exist and there is a static id method or property - routeInfo.hasLoader ||= "loader" in members && members.id; - routeInfo.hasAction ||= "action" in members && members.id; - } else if ((declaration.type === "FunctionDeclaration" || declaration.type === "DeclareVariable") && declaration.id) { - routeInfo.hasLoader ||= declaration.id.name === "loader"; - routeInfo.hasAction ||= declaration.id.name === "action"; - routeInfo.hasClientLoader ||= declaration.id.name === "clientLoader"; - routeInfo.hasClientAction ||= declaration.id.name === "clientAction"; - } - }, - }); - - return routeInfo; -} - -export interface RouteManifestEntry { - /** - * The path this route uses to match on the URL pathname. - */ - path?: string; - /** - * The unique id for this route, named like its `file` but without the - * extension. So `app/routes/gists/$username.tsx` will have an `id` of - * `routes/gists/$username`. - */ - id: string; - - /** - * The unique `id` for this route's parent route, if there is one. - */ - parentId?: string; - - /** - * The path to the entry point for this route, relative to - * `config.appDirectory`. - */ - file: string; - index?: boolean; - hasAction?: boolean; - hasLoader?: boolean; - hasClientAction?: boolean; - hasClientLoader?: boolean; - hasErrorBoundary?: boolean; - exportedClasses?: string[]; -} - -export interface RouteManifest { - [routeId: string]: RouteManifestEntry; -} - -type RouteConfigEntry = Awaited>[number]; - -export type ApiRoute = { - file: string; - path: string; -}; - -type LoadedRoutes = { - manifest: RouteManifest; - apiRoutes: ApiRoute[]; -}; - -export function loadRoutes( - routes: RouteConfigEntry[], - apiRoutePatterns: string[] -): LoadedRoutes { - const root: RouteManifestEntry = { - id: "root", - file: "app/root.tsx", - path: "", - ...loadRoute("app/root.tsx"), - }; - const manifest: RouteManifest = { root }; - - const recurse = (route: RouteConfigEntry, parentId?: string) => { - if (route.id === undefined) unreachable(); - - manifest[route.id] = { - id: route.id, - parentId, - ...route, - file: `app/${route.file}`, - ...loadRoute(`app/${route.file}`), - }; - - if (route.children) { - route.children.forEach((child) => recurse(child, route.id)); - } - }; - - const topLevelApiRoutes = routes.filter((it) => - apiRoutePatterns.some((pattern) => - minimatch(it.file.replace("routes/", ""), pattern) - ) - ); - const apiRoutes = topLevelApiRoutes.flatMap(collectApiRoutes); - - routes - .filter((it) => topLevelApiRoutes.every((api) => it.file !== api.file)) - .forEach((route) => recurse(route, "root")); - - return { manifest, apiRoutes }; -} - -function collectApiRoutes(route: RouteConfigEntry): ApiRoute[] { - const results: ApiRoute[] = []; - - if (route.path) { - results.push({ - file: `app/${route.file}`, - path: route.path, - }); - } - - if (route.children) { - route.children.forEach((child) => { - results.push(...collectApiRoutes(child)); - }); - } - - return results; -} diff --git a/packages/vite/src/routing/fs-routes.ts b/packages/vite/src/routing/fs-routes.ts new file mode 100644 index 0000000..1c7d32f --- /dev/null +++ b/packages/vite/src/routing/fs-routes.ts @@ -0,0 +1,56 @@ +import { readdirSync } from "node:fs"; +import * as path from "node:path"; +import { isEcmaLike } from "../util.js"; +import type { Route } from "./index.js"; + +export function fsRoutes(): Route[] { + const routesDir = path.resolve(process.cwd(), "app", "routes"); + const routes = walkDir(routesDir).map((route) => + route.replace(`${routesDir}/`, "") + ); + + return routes + .filter((route) => isEcmaLike(route)) + .map((route) => { + const pattern = fileNameToPattern(route); + + return { + pattern, + file: path.resolve(routesDir, route), + }; + }); +} + +function fileNameToPattern(fileName: string) { + let pattern = + "/" + fileName.replace(/\.(t|j)sx?$/, "").replace(/\/index$/, ""); + + if (pattern === "/index") { + pattern = "/"; + } + + if (pattern.endsWith("/index")) { + pattern = pattern.slice(0, -6); + } + + return pattern.replace("$", ":"); +} + +function walkDir(dir: string) { + let files: string[] = []; + + const dirents = readdirSync(dir, { withFileTypes: true }); + + for (const dirent of dirents) { + const fullPath = path.join(dir, dirent.name); + if (dirent.isSymbolicLink()) continue; + + if (dirent.isDirectory()) { + files = files.concat(walkDir(fullPath)); + } else if (dirent.isFile()) { + files.push(fullPath); + } + } + + return files; +} diff --git a/packages/vite/src/routing/index.ts b/packages/vite/src/routing/index.ts new file mode 100644 index 0000000..a25d7b9 --- /dev/null +++ b/packages/vite/src/routing/index.ts @@ -0,0 +1,6 @@ +export type Route = { + // The pattern is the pathname of the route, should follow URLPattern syntax + pattern: string; + // The file is the path to the file that contains the route + file: string; +}; diff --git a/packages/vite/src/vite-exec.ts b/packages/vite/src/vite-exec.ts new file mode 100644 index 0000000..d09c0c2 --- /dev/null +++ b/packages/vite/src/vite-exec.ts @@ -0,0 +1,49 @@ +import { createServer, version as viteVersion } from "vite"; +import { ViteNodeRunner } from "vite-node/client"; +import { ViteNodeServer } from "vite-node/server"; +import { installSourcemapsSupport } from "vite-node/source-map"; + +export async function execVite(file: string) { + const server = await createServer({ + server: { + preTransformRequests: false, + hmr: false, + watch: null, + }, + ssr: { + external: [], + }, + optimizeDeps: { + noDiscovery: true, + }, + css: { + postcss: {}, + }, + configFile: false, + envFile: false, + plugins: [], + }); + + // @ts-ignore + const node = new ViteNodeServer(server); + + installSourcemapsSupport({ + getSourceMap: (source) => node.getSourceMap(source), + }); + + const runner = new ViteNodeRunner({ + root: server.config.root, + base: server.config.base, + fetchModule(id) { + return node.fetchModule(id); + }, + resolveId(id, importer) { + return node.resolveId(id, importer); + }, + }); + + const result = await runner.executeFile(file); + await server.close(); + + return result; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0efd8f0..973c4ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,9 +87,6 @@ importers: '@orange-js/vite': specifier: workspace:* version: link:../vite - '@react-router/fs-routes': - specifier: ^7.2.0 - version: 7.2.0(@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.9.1(@cloudflare/workers-types@4.20250413.0)))(typescript@5.7.3) chalk: specifier: ^5.4.1 version: 5.4.1 @@ -154,9 +151,6 @@ importers: react-dom: specifier: '>=19' version: 19.0.0(react@19.0.0) - react-router: - specifier: '=7.6.0' - version: 7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) devDependencies: '@cloudflare/workers-types': specifier: ^4.20250413.0 @@ -165,14 +159,18 @@ importers: specifier: ^22.13.0 version: 22.13.0 '@types/react': - specifier: ^19.0.2 - version: 19.0.8 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': - specifier: ^19.0.2 - version: 19.0.3(@types/react@19.0.8) + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) typescript: specifier: ^5.7.2 version: 5.7.3 + optionalDependencies: + '@cloudflare/actors': + specifier: 0.0.1-beta.1 + version: 0.0.1-beta.1 packages/create-orange: dependencies: @@ -219,6 +217,9 @@ importers: '@babel/parser': specifier: ^7.26.7 version: 7.26.7 + '@babel/plugin-proposal-decorators': + specifier: ^7.28.0 + version: 7.28.0(@babel/core@7.28.0) '@babel/traverse': specifier: ^7.26.7 version: 7.26.7 @@ -228,15 +229,15 @@ importers: '@cloudflare/vite-plugin': specifier: ^1.9.0 version: 1.9.0(rollup@4.40.1)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0) - '@react-router/dev': - specifier: ^7.2.0 - version: 7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.23.0) - '@react-router/fs-routes': - specifier: ^7.2.0 - version: 7.2.0(@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.23.0))(typescript@5.7.3) '@swc-node/core': specifier: ^1.13.3 version: 1.13.3(@swc/core@1.11.20)(@swc/types@0.1.21) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + '@vitejs/plugin-rsc': + specifier: ^0.4.12 + version: 0.4.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) dedent: specifier: ^1.5.3 version: 1.5.3 @@ -249,6 +250,12 @@ importers: react-refresh: specifier: ^0.16.0 version: 0.16.0 + vite-node: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) devDependencies: '@swc/core': specifier: ^1.11.20 @@ -279,82 +286,98 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.5': - resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.10': - resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} '@babel/generator@7.26.5': resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.0': - resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.25.9': - resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.27.0': - resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-member-expression-to-functions@7.25.9': - resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.25.9': - resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - '@babel/helper-replace-supers@7.26.5': - resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-skip-transparent-expression-wrappers@7.25.9': - resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} '@babel/parser@7.26.7': @@ -362,43 +385,31 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-decorators@7.25.9': - resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.25.9': - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.25.9': - resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.26.3': - resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.27.0': - resolution: {integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.26.0': - resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -407,24 +418,24 @@ packages: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} '@babel/traverse@7.26.7': resolution: {integrity: sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.0': - resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} '@babel/types@7.26.7': resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} engines: {node: '>=6.9.0'} '@biomejs/biome@2.0.0-beta.1': @@ -486,6 +497,10 @@ packages: '@clack/prompts@0.10.1': resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==} + '@cloudflare/actors@0.0.1-beta.1': + resolution: {integrity: sha512-dG8zSZM0z5p47bCcEG66Mw98nF5dIqRRawd8gXrkPoVnZ8w3uBB14MCDyneEPqYrFHjsS9ANBrc3UUeb4ChxwQ==} + engines: {node: '>=18.0.0'} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -1413,14 +1428,13 @@ packages: '@types/node': optional: true - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1439,73 +1453,25 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@mjackson/node-fetch-server@0.2.0': - resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} - '@mjackson/node-fetch-server@0.6.1': resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} - '@npmcli/git@4.1.0': - resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@npmcli/package-json@4.0.1': - resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@npmcli/promise-spawn@6.0.2': - resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@mjackson/node-fetch-server@0.7.0': + resolution: {integrity: sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==} '@playwright/test@1.53.2': resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==} engines: {node: '>=18'} hasBin: true - '@react-router/dev@7.2.0': - resolution: {integrity: sha512-GzSNGeWuhx6sMsnidCQAlCAephibUMC61xIAdsc6hBXWCJe/T9wUrvtnh2Xbcpr7BRZJtJN4UhI472ZURA6m9w==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@react-router/serve': ^7.2.0 - react-router: ^7.2.0 - typescript: ^5.1.0 - vite: ^5.1.0 || ^6.0.0 - wrangler: ^3.28.2 - peerDependenciesMeta: - '@react-router/serve': - optional: true - typescript: - optional: true - wrangler: - optional: true - - '@react-router/fs-routes@7.2.0': - resolution: {integrity: sha512-vbXNCJ6Rd9ZhMpYzacz2bZjDi4UTgOtIktdcHG0oFUFS51DWvfzFo0h/7iyqT3r1QFKQ3wg+KhX5PEiksipeaQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@react-router/dev': ^7.2.0 - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - - '@react-router/node@7.2.0': - resolution: {integrity: sha512-CqBHLwvvV4BB8htmaSwT+SOwX9B4RVOIiEdTlaIp12sNVCGSYDIEGbv3T4Wxeq8p5ynNfhNcdBeXtZ6ZPWVozA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react-router: 7.2.0 - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} '@rollup/plugin-replace@6.0.2': resolution: {integrity: sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==} @@ -1892,15 +1858,27 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@types/babel__generator@7.6.8': resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/command-exists@1.2.3': resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1922,17 +1900,62 @@ packages: '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} - '@types/react-dom@19.0.3': - resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} + '@types/react-dom@19.1.6': + resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: '@types/react': ^19.0.0 '@types/react@19.0.8': resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==} + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@types/which-pm-runs@1.0.2': resolution: {integrity: sha512-M0ZefeDApctHbjqtATOiixiwafG7pXD3exxnjku4XmX9+2DmONGghv5Z8Pnm0lNLBZKvDQyuG+4pLkH2UkP5gg==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitejs/plugin-rsc@0.4.12': + resolution: {integrity: sha512-sEr+l2gLAdCYf77eNO69Ef+bXpVB/rzvPUDl+qJT/3GQNzuwo1zPvKfFxnfNKSep2x0EzXr2t5BLnKvxj8Rjkg==} + peerDependencies: + react: '*' + react-dom: '*' + vite: '*' + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1958,30 +1981,20 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - babel-dead-code-elimination@1.0.8: - resolution: {integrity: sha512-og6HQERk0Cmm+nTT4Od2wbPtgABXFMPaHACjbKLulZIFMkYyXZLkUGuAxdgpMJBrxyt/XFpSz++lNzjbcMnPkQ==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1991,17 +2004,11 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - browserify-zlib@0.1.4: - resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2014,8 +2021,12 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001696: - resolution: {integrity: sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==} + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} @@ -2024,9 +2035,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} @@ -2071,13 +2082,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cron-schedule@5.0.4: + resolution: {integrity: sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==} engines: {node: '>=18'} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2097,6 +2105,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -2105,6 +2122,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + default-browser-id@5.0.0: resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} engines: {node: '>=18'} @@ -2140,31 +2161,16 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexify@3.7.1: - resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.5.90: - resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==} + electron-to-chromium@1.5.187: + resolution: {integrity: sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.18.2: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} - err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2176,6 +2182,9 @@ packages: es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2211,6 +2220,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -2223,6 +2235,10 @@ packages: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.4: resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} @@ -2246,10 +2262,6 @@ packages: picomatch: optional: true - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: '>=14'} - form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} @@ -2261,10 +2273,6 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2307,10 +2315,6 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2325,10 +2329,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gunzip-maybe@1.4.2: - resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} - hasBin: true - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2345,10 +2345,6 @@ packages: resolution: {integrity: sha512-5qfNQeaIptMaJKyoJ6N/q4gIq0DBp2FCRaLNuUI3LlJKL4S37DY/rLL1uAxA4wrPB39tJ3s+f7kgI79O4ScSug==} engines: {node: '>=16.9.0'} - hosted-git-info@6.1.3: - resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2360,19 +2356,9 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-deflate@1.0.0: - resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2382,15 +2368,14 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-gzip@1.0.0: - resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} - engines: {node: '>=0.10.0'} - is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} hasBin: true + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2403,15 +2388,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -2419,28 +2398,19 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true - json-parse-even-better-errors@3.0.2: - resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -2509,19 +2479,12 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2563,10 +2526,6 @@ packages: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2601,6 +2560,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2618,26 +2582,6 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - normalize-package-data@5.0.0: - resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-install-checks@6.3.0: - resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-normalize-package-bin@3.0.1: - resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-package-arg@10.1.0: - resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-pick-manifest@8.0.2: - resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -2645,9 +2589,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2664,39 +2605,26 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - peek-stream@1.1.3: - resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + periscopic@4.0.2: + resolution: {integrity: sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.2: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} @@ -2719,80 +2647,29 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - - promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - - pump@2.0.1: - resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} - - pumpify@1.5.1: - resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} - react-dom@19.0.0: resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: react: ^19.0.0 - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - react-refresh@0.16.0: resolution: {integrity: sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==} engines: {node: '>=0.10.0'} - react-router@7.6.0: - resolution: {integrity: sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readdirp@4.1.1: - resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} - engines: {node: '>= 14.18.0'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - rollup@4.34.0: resolution: {integrity: sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2807,9 +2684,6 @@ packages: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2825,9 +2699,6 @@ packages: engines: {node: '>=10'} hasBin: true - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} - sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2840,6 +2711,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2857,61 +2731,38 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.21: - resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - - stream-slice@0.1.2: - resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + tailwindcss@4.1.11: resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} @@ -2923,8 +2774,11 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} @@ -2934,6 +2788,18 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -2965,6 +2831,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turbo-stream@3.1.0: + resolution: {integrity: sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -2998,46 +2867,20 @@ packages: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} - undici@6.21.1: - resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} - engines: {node: '>=18.17'} - unenv@2.0.0-rc.15: resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} unenv@2.0.0-rc.17: resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - update-browserslist-db@1.1.2: - resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - valibot@0.41.0: - resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - - validate-npm-package-name@5.0.1: - resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - vite-node@3.0.0-beta.2: - resolution: {integrity: sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -3089,46 +2932,6 @@ packages: yaml: optional: true - vite@6.2.7: - resolution: {integrity: sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@7.0.1: resolution: {integrity: sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3209,28 +3012,64 @@ packages: yaml: optional: true - 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==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true - which@2.0.2: + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + 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==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - which@3.0.1: - resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} hasBin: true workerd@1.20250408.0: @@ -3267,17 +3106,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -3298,10 +3126,6 @@ packages: resolution: {integrity: sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw==} engines: {node: '>= 4.0'} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3316,6 +3140,9 @@ packages: youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} @@ -3332,20 +3159,26 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.26.5': {} + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} - '@babel/core@7.26.10': + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 convert-source-map: 2.0.0 debug: 4.4.0 gensync: 1.0.0-beta.2 @@ -3362,147 +3195,132 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/generator@7.27.0': + '@babel/generator@7.28.0': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.25.9': + '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 - '@babel/helper-compilation-targets@7.26.5': + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-member-expression-to-functions': 7.25.9 - '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-member-expression-to-functions@7.25.9': + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.25.9': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.25.9': + '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 - '@babel/helper-plugin-utils@7.26.5': {} + '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-member-expression-to-functions': 7.25.9 - '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.0': + '@babel/helpers@7.27.6': dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 '@babel/parser@7.26.7': dependencies: '@babel/types': 7.26.7 - '@babel/parser@7.27.0': + '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.26.10)': + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-typescript@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 '@babel/template@7.25.9': dependencies: @@ -3510,11 +3328,11 @@ snapshots: '@babel/parser': 7.26.7 '@babel/types': 7.26.7 - '@babel/template@7.27.0': + '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 '@babel/traverse@7.26.7': dependencies: @@ -3528,15 +3346,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.27.0': + '@babel/traverse@7.28.0': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 debug: 4.4.0 - globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3545,10 +3363,10 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.27.0': + '@babel/types@7.28.1': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 '@biomejs/biome@2.0.0-beta.1': optionalDependencies: @@ -3596,6 +3414,12 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@cloudflare/actors@0.0.1-beta.1': + optionalDependencies: + cron-schedule: 5.0.4 + nanoid: 5.1.5 + optional: true + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -4166,19 +3990,15 @@ snapshots: optionalDependencies: '@types/node': 22.13.0 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -4196,178 +4016,25 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.9': + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@mjackson/node-fetch-server@0.2.0': {} - - '@mjackson/node-fetch-server@0.6.1': {} - - '@npmcli/git@4.1.0': - dependencies: - '@npmcli/promise-spawn': 6.0.2 - lru-cache: 7.18.3 - npm-pick-manifest: 8.0.2 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.7.0 - which: 3.0.1 - transitivePeerDependencies: - - bluebird - - '@npmcli/package-json@4.0.1': + '@jridgewell/trace-mapping@0.3.9': dependencies: - '@npmcli/git': 4.1.0 - glob: 10.4.5 - hosted-git-info: 6.1.3 - json-parse-even-better-errors: 3.0.2 - normalize-package-data: 5.0.0 - proc-log: 3.0.0 - semver: 7.7.0 - transitivePeerDependencies: - - bluebird + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 - '@npmcli/promise-spawn@6.0.2': - dependencies: - which: 3.0.1 + '@mjackson/node-fetch-server@0.6.1': {} - '@pkgjs/parseargs@0.11.0': - optional: true + '@mjackson/node-fetch-server@0.7.0': {} '@playwright/test@1.53.2': dependencies: playwright: 1.53.2 - '@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.9.1(@cloudflare/workers-types@4.20250413.0))': - dependencies: - '@babel/core': 7.26.10 - '@babel/generator': 7.26.5 - '@babel/parser': 7.26.7 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/preset-typescript': 7.26.0(@babel/core@7.26.10) - '@babel/traverse': 7.26.7 - '@babel/types': 7.26.7 - '@npmcli/package-json': 4.0.1 - '@react-router/node': 7.2.0(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3) - arg: 5.0.2 - babel-dead-code-elimination: 1.0.8 - chokidar: 4.0.3 - dedent: 1.5.3 - es-module-lexer: 1.6.0 - exit-hook: 2.2.1 - fs-extra: 10.1.0 - gunzip-maybe: 1.4.2 - jsesc: 3.0.2 - lodash: 4.17.21 - pathe: 1.1.2 - picocolors: 1.1.1 - picomatch: 2.3.1 - prettier: 2.8.8 - react-refresh: 0.14.2 - react-router: 7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - semver: 7.7.0 - set-cookie-parser: 2.7.1 - valibot: 0.41.0(typescript@5.7.3) - vite: 6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - vite-node: 3.0.0-beta.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - optionalDependencies: - typescript: 5.7.3 - wrangler: 4.9.1(@cloudflare/workers-types@4.20250413.0) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - bluebird - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - '@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.23.0)': - dependencies: - '@babel/core': 7.26.10 - '@babel/generator': 7.26.5 - '@babel/parser': 7.26.7 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/preset-typescript': 7.26.0(@babel/core@7.26.10) - '@babel/traverse': 7.26.7 - '@babel/types': 7.26.7 - '@npmcli/package-json': 4.0.1 - '@react-router/node': 7.2.0(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3) - arg: 5.0.2 - babel-dead-code-elimination: 1.0.8 - chokidar: 4.0.3 - dedent: 1.5.3 - es-module-lexer: 1.6.0 - exit-hook: 2.2.1 - fs-extra: 10.1.0 - gunzip-maybe: 1.4.2 - jsesc: 3.0.2 - lodash: 4.17.21 - pathe: 1.1.2 - picocolors: 1.1.1 - picomatch: 2.3.1 - prettier: 2.8.8 - react-refresh: 0.14.2 - react-router: 7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - semver: 7.7.0 - set-cookie-parser: 2.7.1 - valibot: 0.41.0(typescript@5.7.3) - vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - vite-node: 3.0.0-beta.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - optionalDependencies: - typescript: 5.7.3 - wrangler: 4.23.0(@cloudflare/workers-types@4.20250413.0) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - bluebird - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - '@react-router/fs-routes@7.2.0(@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.9.1(@cloudflare/workers-types@4.20250413.0)))(typescript@5.7.3)': - dependencies: - '@react-router/dev': 7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.9.1(@cloudflare/workers-types@4.20250413.0)) - minimatch: 9.0.5 - optionalDependencies: - typescript: 5.7.3 - - '@react-router/fs-routes@7.2.0(@react-router/dev@7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.23.0))(typescript@5.7.3)': - dependencies: - '@react-router/dev': 7.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tsx@4.19.2)(typescript@5.7.3)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(wrangler@4.23.0) - minimatch: 9.0.5 - optionalDependencies: - typescript: 5.7.3 - - '@react-router/node@7.2.0(react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)': - dependencies: - '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - source-map-support: 0.5.21 - stream-slice: 0.1.2 - undici: 6.21.1 - optionalDependencies: - typescript: 5.7.3 + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/plugin-replace@6.0.2(rollup@4.40.1)': dependencies: @@ -4629,16 +4296,35 @@ snapshots: tailwindcss: 4.1.11 vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + '@types/babel__generator@7.6.8': dependencies: '@babel/types': 7.26.7 + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + '@types/babel__traverse@7.20.6': dependencies: '@babel/types': 7.26.7 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/command-exists@1.2.3': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.6': {} '@types/estree@1.0.7': {} @@ -4665,16 +4351,87 @@ snapshots: '@types/node': 22.13.0 kleur: 3.0.3 - '@types/react-dom@19.0.3(@types/react@19.0.8)': + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: - '@types/react': 19.0.8 + '@types/react': 19.1.8 '@types/react@19.0.8': dependencies: csstype: 3.1.3 + '@types/react@19.1.8': + dependencies: + csstype: 3.1.3 + '@types/which-pm-runs@1.0.2': {} + '@vitejs/plugin-react@4.7.0(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-rsc@0.4.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + dependencies: + '@mjackson/node-fetch-server': 0.7.0 + es-module-lexer: 1.7.0 + estree-walker: 3.0.3 + magic-string: 0.30.17 + periscopic: 4.0.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + turbo-stream: 3.1.0 + vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vitefu: 1.1.1(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -4693,30 +4450,17 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} - - arg@5.0.2: {} - as-table@1.0.55: dependencies: printable-characters: 1.0.42 - asynckit@0.4.0: {} + assertion-error@2.0.1: {} - babel-dead-code-elimination@1.0.8: - dependencies: - '@babel/core': 7.26.10 - '@babel/parser': 7.26.7 - '@babel/traverse': 7.26.7 - '@babel/types': 7.26.7 - transitivePeerDependencies: - - supports-color + asynckit@0.4.0: {} balanced-match@1.0.2: {} @@ -4726,18 +4470,12 @@ snapshots: dependencies: balanced-match: 1.0.2 - browserify-zlib@0.1.4: - dependencies: - pako: 0.2.9 - - browserslist@4.24.4: + browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001696 - electron-to-chromium: 1.5.90 + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.187 node-releases: 2.0.19 - update-browserslist-db: 1.1.2(browserslist@4.24.4) - - buffer-from@1.1.2: {} + update-browserslist-db: 1.1.3(browserslist@4.25.1) bundle-name@4.1.0: dependencies: @@ -4750,15 +4488,21 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - caniuse-lite@1.0.30001696: {} + caniuse-lite@1.0.30001727: {} + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 chalk@5.4.1: {} chardet@0.7.0: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.1 + check-error@2.1.1: {} chownr@3.0.0: {} @@ -4804,9 +4548,8 @@ snapshots: cookie@0.7.2: {} - cookie@1.0.2: {} - - core-util-is@1.0.3: {} + cron-schedule@5.0.4: + optional: true cross-spawn@7.0.6: dependencies: @@ -4822,8 +4565,14 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + dedent@1.5.3: {} + deep-eql@5.0.2: {} + default-browser-id@5.0.0: {} default-browser@5.2.1: @@ -4851,38 +4600,23 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexify@3.7.1: - dependencies: - end-of-stream: 1.4.4 - inherits: 2.0.4 - readable-stream: 2.3.8 - stream-shift: 1.0.3 - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.5.90: {} + electron-to-chromium@1.5.187: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - enhanced-resolve@5.18.2: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 - err-code@2.0.3: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} es-module-lexer@1.6.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5009,6 +4743,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + event-target-shim@5.0.1: {} execa@5.1.1: @@ -5025,6 +4763,8 @@ snapshots: exit-hook@2.2.1: {} + expect-type@1.2.2: {} + exsolve@1.0.4: {} external-editor@3.1.0: @@ -5041,11 +4781,6 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - form-data-encoder@1.7.2: {} form-data@4.0.2: @@ -5060,12 +4795,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - fsevents@2.3.2: optional: true @@ -5109,15 +4838,6 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.4.5: - dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globals@11.12.0: {} globrex@0.1.2: {} @@ -5126,15 +4846,6 @@ snapshots: graceful-fs@4.2.11: {} - gunzip-maybe@1.4.2: - dependencies: - browserify-zlib: 0.1.4 - is-deflate: 1.0.0 - is-gzip: 1.0.0 - peek-stream: 1.1.3 - pumpify: 1.5.1 - through2: 2.0.5 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -5147,10 +4858,6 @@ snapshots: hono@4.6.20: {} - hosted-git-info@6.1.3: - dependencies: - lru-cache: 7.18.3 - human-signals@2.1.0: {} humanize-ms@1.2.1: @@ -5161,26 +4868,20 @@ snapshots: dependencies: safer-buffer: 2.1.2 - inherits@2.0.4: {} - is-arrayish@0.3.2: {} - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-deflate@1.0.0: {} - is-docker@3.0.0: {} is-fullwidth-code-point@3.0.0: {} - is-gzip@1.0.0: {} - is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.7 + is-stream@2.0.1: {} is-unicode-supported@2.1.0: {} @@ -5189,34 +4890,18 @@ snapshots: dependencies: is-inside-container: 1.0.0 - isarray@1.0.0: {} - isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jiti@2.4.2: {} js-tokens@4.0.0: {} - jsesc@3.0.2: {} + js-tokens@9.0.1: {} jsesc@3.1.0: {} - json-parse-even-better-errors@3.0.2: {} - json5@2.2.3: {} - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - kleur@3.0.3: {} lightningcss-darwin-arm64@1.30.1: @@ -5264,16 +4949,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 - lodash@4.17.21: {} - - lru-cache@10.4.3: {} + loupe@3.1.4: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -5331,10 +5012,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - minipass@7.1.2: {} minizlib@3.0.2: @@ -5353,6 +5030,9 @@ snapshots: nanoid@3.3.8: {} + nanoid@5.1.5: + optional: true + node-domexception@1.0.0: {} node-fetch@2.7.0: @@ -5361,43 +5041,12 @@ snapshots: node-releases@2.0.19: {} - normalize-package-data@5.0.0: - dependencies: - hosted-git-info: 6.1.3 - is-core-module: 2.16.1 - semver: 7.7.0 - validate-npm-package-license: 3.0.4 - - npm-install-checks@6.3.0: - dependencies: - semver: 7.7.0 - - npm-normalize-package-bin@3.0.1: {} - - npm-package-arg@10.1.0: - dependencies: - hosted-git-info: 6.1.3 - proc-log: 3.0.0 - semver: 7.7.0 - validate-npm-package-name: 5.0.1 - - npm-pick-manifest@8.0.2: - dependencies: - npm-install-checks: 6.3.0 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 10.1.0 - semver: 7.7.0 - npm-run-path@4.0.1: dependencies: path-key: 3.1.1 ohash@2.0.11: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -5415,33 +5064,22 @@ snapshots: os-tmpdir@1.0.2: {} - package-json-from-dist@1.0.1: {} - - pako@0.2.9: {} - path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-to-regexp@6.3.0: {} - pathe@1.1.2: {} - pathe@2.0.3: {} - peek-stream@1.1.3: + pathval@2.0.1: {} + + periscopic@4.0.2: dependencies: - buffer-from: 1.1.2 - duplexify: 3.7.1 - through2: 2.0.5 + '@types/estree': 1.0.7 + is-reference: 3.0.3 + zimmerframe: 1.1.2 picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.2: {} playwright-core@1.53.2: {} @@ -5464,67 +5102,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@2.8.8: {} - printable-characters@1.0.42: {} - proc-log@3.0.0: {} - - process-nextick-args@2.0.1: {} - - promise-inflight@1.0.1: {} - - promise-retry@2.0.1: - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - - pump@2.0.1: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - - pumpify@1.5.1: - dependencies: - duplexify: 3.7.1 - inherits: 2.0.4 - pump: 2.0.1 - react-dom@19.0.0(react@19.0.0): dependencies: react: 19.0.0 scheduler: 0.25.0 - react-refresh@0.14.2: {} - react-refresh@0.16.0: {} - react-router@7.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): - dependencies: - cookie: 1.0.2 - react: 19.0.0 - set-cookie-parser: 2.7.1 - optionalDependencies: - react-dom: 19.0.0(react@19.0.0) + react-refresh@0.17.0: {} react@19.0.0: {} - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readdirp@4.1.1: {} - resolve-pkg-maps@1.0.0: {} - retry@0.12.0: {} - rollup@4.34.0: dependencies: '@types/estree': 1.0.6 @@ -5578,8 +5170,6 @@ snapshots: run-applescript@7.0.0: {} - safe-buffer@5.1.2: {} - safer-buffer@2.1.2: {} scheduler@0.25.0: {} @@ -5588,8 +5178,6 @@ snapshots: semver@7.7.0: {} - set-cookie-parser@2.7.1: {} - sharp@0.33.5: dependencies: color: 4.2.3 @@ -5622,6 +5210,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -5634,37 +5224,18 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map@0.6.1: {} - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.21 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.21 - - spdx-license-ids@3.0.21: {} + stackback@0.0.2: {} stacktracey@2.1.8: dependencies: as-table: 1.0.55 get-source: 2.0.12 - stoppable@1.1.0: {} - - stream-shift@1.0.3: {} + std-env@3.9.0: {} - stream-slice@0.1.2: {} + stoppable@1.1.0: {} string-width@4.2.3: dependencies: @@ -5672,26 +5243,16 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 - strip-final-newline@2.0.0: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + tailwindcss@4.1.11: {} tapable@2.2.2: {} @@ -5705,10 +5266,9 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - through2@2.0.5: - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} tinyglobby@0.2.13: dependencies: @@ -5720,6 +5280,12 @@ snapshots: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -5744,6 +5310,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + turbo-stream@3.1.0: {} + type-fest@0.21.3: {} typescript@5.7.3: {} @@ -5764,8 +5332,6 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - undici@6.21.1: {} - unenv@2.0.0-rc.15: dependencies: defu: 6.1.4 @@ -5782,34 +5348,19 @@ snapshots: pathe: 2.0.3 ufo: 1.6.1 - universalify@2.0.1: {} - - update-browserslist-db@1.1.2(browserslist@4.24.4): + update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: - browserslist: 4.24.4 + browserslist: 4.25.1 escalade: 3.2.0 picocolors: 1.1.1 - util-deprecate@1.0.2: {} - - valibot@0.41.0(typescript@5.7.3): - optionalDependencies: - typescript: 5.7.3 - - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - - validate-npm-package-name@5.0.1: {} - - vite-node@3.0.0-beta.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite-node@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 1.1.2 - vite: 6.2.7(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) transitivePeerDependencies: - '@types/node' - jiti @@ -5847,11 +5398,14 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 - vite@6.2.7(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: - esbuild: 0.25.4 + esbuild: 0.25.0 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 postcss: 8.5.6 rollup: 4.40.1 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 22.13.0 fsevents: 2.3.3 @@ -5859,9 +5413,9 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 - vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: - esbuild: 0.25.0 + esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.6 @@ -5889,6 +5443,51 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 + vitefu@1.1.1(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): + optionalDependencies: + vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + + vitest@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite-node: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.13.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} @@ -5904,9 +5503,10 @@ snapshots: dependencies: isexe: 2.0.0 - which@3.0.1: + why-is-node-running@2.3.0: dependencies: - isexe: 2.0.0 + siginfo: 2.0.0 + stackback: 0.0.2 workerd@1.20250408.0: optionalDependencies: @@ -5965,20 +5565,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - wrappy@1.0.2: {} - ws@8.18.0: {} xdg-app-paths@8.3.0: @@ -5993,8 +5579,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - xtend@4.0.2: {} - yallist@3.1.1: {} yallist@5.0.0: {} @@ -6007,4 +5591,6 @@ snapshots: mustache: 4.2.0 stacktracey: 2.1.8 + zimmerframe@1.1.2: {} + zod@3.22.3: {} From eee925ecf3d485291e0310a4a1f168ad16b458a3 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Mon, 21 Jul 2025 19:18:59 -0400 Subject: [PATCH 02/16] chore: make vite plugin options optional --- packages/vite/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index c33cd56..d2e3791 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -18,7 +18,7 @@ export type OrangeRSCPluginOptions = { }; export default function orange( - options: OrangeRSCPluginOptions + options: OrangeRSCPluginOptions = {} ): PluginOption[] { let _config: Config; From eaff624b3d2b0cae0addb1b64807205074f4116f Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Mon, 21 Jul 2025 19:22:05 -0400 Subject: [PATCH 03/16] format --- e2e/templates/basic/vite.config.ts | 4 +- packages/cli/src/commands/provision/index.ts | 50 ++++++++++---------- packages/cli/src/commands/types.ts | 6 +-- packages/core/src/client.tsx | 6 +-- packages/core/src/hono.ts | 4 +- packages/core/src/router.ts | 2 +- packages/core/src/server.tsx | 8 ++-- packages/core/src/ssr.tsx | 4 +- packages/vite/src/index.ts | 10 ++-- packages/vite/src/plugins/isolation.ts | 2 +- packages/vite/src/plugins/routes.ts | 6 +-- packages/vite/src/routing/fs-routes.ts | 2 +- 12 files changed, 53 insertions(+), 51 deletions(-) diff --git a/e2e/templates/basic/vite.config.ts b/e2e/templates/basic/vite.config.ts index bb591c7..b6a7af2 100644 --- a/e2e/templates/basic/vite.config.ts +++ b/e2e/templates/basic/vite.config.ts @@ -1,11 +1,11 @@ import orange from "@orange-js/vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import tailwindcss from '@tailwindcss/vite' +import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [orange(), tsconfigPaths(), tailwindcss()], build: { minify: true, - } + }, }); diff --git a/packages/cli/src/commands/provision/index.ts b/packages/cli/src/commands/provision/index.ts index bb1ff32..a35bf13 100644 --- a/packages/cli/src/commands/provision/index.ts +++ b/packages/cli/src/commands/provision/index.ts @@ -12,31 +12,33 @@ import { provisionBucket } from "./object-storage.js"; import { provisionKv } from "./kv.js"; export function provisionCommand(client: Cloudflare) { - return createCommand("provision") - .description("Provision Cloudflare resources for your project") - // .option("-d, --durable-objects", "Provision Durable Objects") - .option("-k, --kv", "Provision Key-Value Store") - .option("-s, --sqlite", "Provision SQLite Database") - .option("-D, --d1", "Provision D1 Database") - .option("-b, --bucket", "Provision Object Storage Bucket") - .option("-R, --r2", "Provision R2 Bucket") - .option("-p, --postgres", "Provision Postgres Database") - .action(async (options) => { - const accountId = await readAccountId(client); - const selectedResource = await determineResource(options); + return ( + createCommand("provision") + .description("Provision Cloudflare resources for your project") + // .option("-d, --durable-objects", "Provision Durable Objects") + .option("-k, --kv", "Provision Key-Value Store") + .option("-s, --sqlite", "Provision SQLite Database") + .option("-D, --d1", "Provision D1 Database") + .option("-b, --bucket", "Provision Object Storage Bucket") + .option("-R, --r2", "Provision R2 Bucket") + .option("-p, --postgres", "Provision Postgres Database") + .action(async (options) => { + const accountId = await readAccountId(client); + const selectedResource = await determineResource(options); - if (selectedResource === "postgres") { - await provisionPostgres(client, accountId); - } else if (selectedResource === "sqlite") { - await provisionSqlite(client, accountId); - } else if (selectedResource === "bucket") { - await provisionBucket(client, accountId); - } else if (selectedResource === "kv") { - await provisionKv(client, accountId); - } else if (selectedResource === "durable-objects") { - // await provisionDurableObjects(); - } - }); + if (selectedResource === "postgres") { + await provisionPostgres(client, accountId); + } else if (selectedResource === "sqlite") { + await provisionSqlite(client, accountId); + } else if (selectedResource === "bucket") { + await provisionBucket(client, accountId); + } else if (selectedResource === "kv") { + await provisionKv(client, accountId); + } else if (selectedResource === "durable-objects") { + // await provisionDurableObjects(); + } + }) + ); } async function determineResource(options: { diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts index f98dc01..fa77b51 100644 --- a/packages/cli/src/commands/types.ts +++ b/packages/cli/src/commands/types.ts @@ -47,8 +47,8 @@ async function generateRouteTypes(config: ResolvedConfig) { import type * as T from "@orange-js/orange/route-module" type Module = typeof import("${importPrefix}${route.file - .replace(".tsx", "") - .replace(".jsx", "")}") + .replace(".tsx", "") + .replace(".jsx", "")}") export type Info = { parents: [], @@ -82,7 +82,7 @@ async function generateRouteTypes(config: ResolvedConfig) { `, { encoding: "utf-8", - } + }, ); } } diff --git a/packages/core/src/client.tsx b/packages/core/src/client.tsx index fcddc8c..c4b8647 100644 --- a/packages/core/src/client.tsx +++ b/packages/core/src/client.tsx @@ -15,7 +15,7 @@ export async function main() { // deserialize RSC stream back to React VDOM for CSR const initialPayload = await ReactClient.createFromReadableStream( // initial RSC stream is injected in SSR stream as - getRscStreamFromHtml() + getRscStreamFromHtml(), ); // browser root component to (re-)render RSC payload as state @@ -37,7 +37,7 @@ export async function main() { // re-fetch RSC and trigger re-rendering async function fetchRscPayload() { const payload = await ReactClient.createFromFetch( - fetch(window.location.href) + fetch(window.location.href), ); setPayload(payload); } @@ -55,7 +55,7 @@ export async function main() { "x-rsc-action": id, }, }), - { temporaryReferences } + { temporaryReferences }, ); setPayload(payload); return payload.returnValue; diff --git a/packages/core/src/hono.ts b/packages/core/src/hono.ts index e05221d..4c93581 100644 --- a/packages/core/src/hono.ts +++ b/packages/core/src/hono.ts @@ -8,7 +8,7 @@ const vars = new AsyncLocalStorage(); export function handler( layout: (props: PropsWithChildren) => React.ReactNode, - options?: server.AppOptions + options?: server.AppOptions, ) { const orangeApp = server.app(layout, options); @@ -26,7 +26,7 @@ type ExtractEnv = T extends HonoBase : never; export function variables< - App extends HonoBase + App extends HonoBase, >(): ExtractEnv { const state = vars.getStore(); if (!state) { diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index 7869c58..d18ff90 100644 --- a/packages/core/src/router.ts +++ b/packages/core/src/router.ts @@ -3,7 +3,7 @@ export type Route = { }; export function router( - routes: T[] + routes: T[], ): (request: Request) => T | undefined { return (request: Request) => { const url = new URL(request.url); diff --git a/packages/core/src/server.tsx b/packages/core/src/server.tsx index 1f13ab7..040e4ae 100644 --- a/packages/core/src/server.tsx +++ b/packages/core/src/server.tsx @@ -22,7 +22,7 @@ export interface Context { export type AppOptions = { context?: ( - env: CloudflareEnv + env: CloudflareEnv, ) => Omit | Promise>; }; @@ -45,7 +45,7 @@ export async function request() { async function handler( request: Request, - Layout: Layout + Layout: Layout, ): Promise { const isAction = request.method === "POST"; let returnValue: unknown | undefined; @@ -137,7 +137,7 @@ async function rscResponse({ onError(error: unknown, errorInfo: ErrorInfo) { console.error("Error during RSC streaming", error, errorInfo); }, - } + }, ); const url = new URL(request.url); @@ -194,7 +194,7 @@ export function app(Layout: Layout, options?: AppOptions) { } function isReactActor( - maybeComponent: ReactComponent | typeof ReactActor + maybeComponent: ReactComponent | typeof ReactActor, ): maybeComponent is typeof ReactActor { return ( typeof maybeComponent === "function" && diff --git a/packages/core/src/ssr.tsx b/packages/core/src/ssr.tsx index ae8d270..a6a9444 100644 --- a/packages/core/src/ssr.tsx +++ b/packages/core/src/ssr.tsx @@ -28,7 +28,7 @@ export async function renderHTML( nonce?: string; debugNojs?: boolean; onError?: (error: unknown, errorInfo: ErrorInfo) => void; - } + }, ) { // duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) @@ -64,7 +64,7 @@ export async function renderHTML( responseStream = responseStream.pipeThrough( injectRscStreamToHtml(rscStream2, { nonce: options?.nonce, - }) + }), ); } diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index d2e3791..1b27434 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -18,7 +18,7 @@ export type OrangeRSCPluginOptions = { }; export default function orange( - options: OrangeRSCPluginOptions = {} + options: OrangeRSCPluginOptions = {}, ): PluginOption[] { let _config: Config; @@ -48,11 +48,11 @@ export default function orange( entries: { client: entrypoint( "entry.browser", - "node_modules/@orange-js/vite/dist/entrypoints/entry.browser.js" + "node_modules/@orange-js/vite/dist/entrypoints/entry.browser.js", ), ssr: entrypoint( "entry.ssr", - "node_modules/@orange-js/vite/dist/entrypoints/entry.ssr.js" + "node_modules/@orange-js/vite/dist/entrypoints/entry.ssr.js", ), // rsc: entrypoint( // "entry.rsc", @@ -68,7 +68,7 @@ export default function orange( viteEnvironment: { name: "rsc", }, - } + }, ), routesPlugin(config), ]; @@ -80,7 +80,7 @@ function entrypoint(name: string, fallback: string) { process.cwd(), "src", "entrypoints", - `${name}.${extension}` + `${name}.${extension}`, ); if (fs.existsSync(entrypoint)) { return entrypoint; diff --git a/packages/vite/src/plugins/isolation.ts b/packages/vite/src/plugins/isolation.ts index 59e268f..1634cd2 100644 --- a/packages/vite/src/plugins/isolation.ts +++ b/packages/vite/src/plugins/isolation.ts @@ -58,7 +58,7 @@ const emptyExports = (exports: string[]) => { .map((e) => e === "default" ? "export default undefined;" - : `export const ${e} = undefined;` + : `export const ${e} = undefined;`, ) .join("\n"); }; diff --git a/packages/vite/src/plugins/routes.ts b/packages/vite/src/plugins/routes.ts index 16a7c0a..857e51c 100644 --- a/packages/vite/src/plugins/routes.ts +++ b/packages/vite/src/plugins/routes.ts @@ -30,11 +30,11 @@ export function routesPlugin(config: () => Config): Plugin { routes.map((route) => [ route.pattern, `route_${Math.random().toString(36).substring(2, 15)}`, - ]) + ]), ); const imports = routes.map((route) => { return `import * as ${ids[route.pattern]} from "${path.resolve( - route.file + route.file, )}";`; }); @@ -42,7 +42,7 @@ export function routesPlugin(config: () => Config): Plugin { (route) => `{ pattern: new URLPattern({ pathname: "${ route.pattern - }" }), module: ${ids[route.pattern]} }` + }" }), module: ${ids[route.pattern]} }`, ); return { diff --git a/packages/vite/src/routing/fs-routes.ts b/packages/vite/src/routing/fs-routes.ts index 1c7d32f..bdf1292 100644 --- a/packages/vite/src/routing/fs-routes.ts +++ b/packages/vite/src/routing/fs-routes.ts @@ -6,7 +6,7 @@ import type { Route } from "./index.js"; export function fsRoutes(): Route[] { const routesDir = path.resolve(process.cwd(), "app", "routes"); const routes = walkDir(routesDir).map((route) => - route.replace(`${routesDir}/`, "") + route.replace(`${routesDir}/`, ""), ); return routes From 2070898e8e1d44a6c9dad94c4b38800d31279b9c Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Mon, 21 Jul 2025 19:28:05 -0400 Subject: [PATCH 04/16] fix: pass basic e2e tests --- e2e/templates/basic/app/entry.client.ts | 3 - e2e/templates/basic/app/entry.server.ts | 6 - e2e/templates/basic/app/entry.server.tsx | 17 ++ e2e/templates/basic/app/root.tsx | 71 -------- .../app/routes/{_index.tsx => index.tsx} | 4 +- e2e/templates/basic/package.json | 9 +- e2e/templates/basic/vite.config.ts | 1 + e2e/templates/basic/wrangler.jsonc | 2 +- packages/vite/package.json | 2 +- pnpm-lock.yaml | 164 +++++++----------- 10 files changed, 89 insertions(+), 190 deletions(-) delete mode 100644 e2e/templates/basic/app/entry.client.ts delete mode 100644 e2e/templates/basic/app/entry.server.ts create mode 100644 e2e/templates/basic/app/entry.server.tsx delete mode 100644 e2e/templates/basic/app/root.tsx rename e2e/templates/basic/app/routes/{_index.tsx => index.tsx} (64%) diff --git a/e2e/templates/basic/app/entry.client.ts b/e2e/templates/basic/app/entry.client.ts deleted file mode 100644 index ef54008..0000000 --- a/e2e/templates/basic/app/entry.client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { hydrate } from "@orange-js/orange/client"; - -hydrate(); \ No newline at end of file diff --git a/e2e/templates/basic/app/entry.server.ts b/e2e/templates/basic/app/entry.server.ts deleted file mode 100644 index f3f5711..0000000 --- a/e2e/templates/basic/app/entry.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { app } from "@orange-js/orange/server"; -import * as serverBuild from "virtual:orange/server-bundle"; - -export * from "virtual:orange/entrypoints"; - -export default app(serverBuild); \ No newline at end of file diff --git a/e2e/templates/basic/app/entry.server.tsx b/e2e/templates/basic/app/entry.server.tsx new file mode 100644 index 0000000..9cc89ce --- /dev/null +++ b/e2e/templates/basic/app/entry.server.tsx @@ -0,0 +1,17 @@ +import { app } from "@orange-js/orange/server"; +import rootStyles from "./root.css?inline"; + +export function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + ); +} + +export default app(Root); diff --git a/e2e/templates/basic/app/root.tsx b/e2e/templates/basic/app/root.tsx deleted file mode 100644 index bb711aa..0000000 --- a/e2e/templates/basic/app/root.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, - useRouteError, -} from "@orange-js/orange"; - -import rootStyles from "./root.css?inline"; - -export const links = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary() { - const error = useRouteError(); - - let status = 500; - let details = "Internal Server Error"; - let stack: string | undefined; - - if (isRouteErrorResponse(error)) { - status = error.status; - details = error.statusText; - } else if (import.meta.env.DEV && error && error instanceof Error) { - details = error.message; - stack = error.stack; - } - - return ( -
-

HTTP {status}

-

{details}

- {stack &&
{stack}
} -
- ); -} diff --git a/e2e/templates/basic/app/routes/_index.tsx b/e2e/templates/basic/app/routes/index.tsx similarity index 64% rename from e2e/templates/basic/app/routes/_index.tsx rename to e2e/templates/basic/app/routes/index.tsx index a9f5d04..840121f 100644 --- a/e2e/templates/basic/app/routes/_index.tsx +++ b/e2e/templates/basic/app/routes/index.tsx @@ -1,6 +1,4 @@ -import { Route } from ".types/routes/_index"; - -export default function Home({}: Route.ComponentProps) { +export default async function Home() { return (

Hello World 🍊

diff --git a/e2e/templates/basic/package.json b/e2e/templates/basic/package.json index 66948fa..bea1e16 100644 --- a/e2e/templates/basic/package.json +++ b/e2e/templates/basic/package.json @@ -18,10 +18,11 @@ "@cloudflare/workers-types": "^4.20250224.0", "@orange-js/cli": "workspace:*", "@orange-js/vite": "workspace:*", - "@types/react": "^19.0.2", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "esbuild": "^0.24.2", "typescript": "^5.8.2", - "vite": "^7.0.2", + "vite": "^7.0.5", "vite-tsconfig-paths": "^5.1.4", "wrangler": "^4.23.0" }, @@ -29,8 +30,8 @@ "@orange-js/orange": "workspace:", "@tailwindcss/vite": "^4.0.14", "hono": "^4.6.20", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "tailwindcss": "^4.0.14" } } diff --git a/e2e/templates/basic/vite.config.ts b/e2e/templates/basic/vite.config.ts index b6a7af2..17ebcb7 100644 --- a/e2e/templates/basic/vite.config.ts +++ b/e2e/templates/basic/vite.config.ts @@ -4,6 +4,7 @@ import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; export default defineConfig({ + // @ts-expect-error - vite type mismatch plugins: [orange(), tsconfigPaths(), tailwindcss()], build: { minify: true, diff --git a/e2e/templates/basic/wrangler.jsonc b/e2e/templates/basic/wrangler.jsonc index 4169eeb..c8b1602 100644 --- a/e2e/templates/basic/wrangler.jsonc +++ b/e2e/templates/basic/wrangler.jsonc @@ -1,7 +1,7 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "basic", - "main": "./app/entry.server.ts", + "main": "./app/entry.server.tsx", "compatibility_date": "2025-05-11", "compatibility_flags": ["nodejs_compat"], // Where the static asses built by Vite will be served out of. diff --git a/packages/vite/package.json b/packages/vite/package.json index 7dca116..d5756cd 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -32,7 +32,7 @@ "@types/babel__traverse": "^7.20.6", "@types/node": "^22.13.0", "typescript": "^5.7.2", - "vite": "^7.0.1" + "vite": "^7.0.5" }, "dependencies": { "@babel/generator": "^7.26.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 973c4ce..9675127 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,16 +31,16 @@ importers: version: link:../../../packages/core '@tailwindcss/vite': specifier: ^4.0.14 - version: 4.1.11(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) hono: specifier: ^4.6.20 version: 4.6.20 react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: ^19.1.0 + version: 19.1.0 react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) tailwindcss: specifier: ^4.0.14 version: 4.1.11 @@ -55,8 +55,11 @@ importers: specifier: workspace:* version: link:../../../packages/vite '@types/react': - specifier: ^19.0.2 - version: 19.0.8 + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) esbuild: specifier: ^0.24.2 version: 0.24.2 @@ -64,11 +67,11 @@ importers: specifier: ^5.8.2 version: 5.8.3 vite: - specifier: ^7.0.2 - version: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + specifier: ^7.0.5 + version: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) wrangler: specifier: ^4.23.0 version: 4.23.0(@cloudflare/workers-types@4.20250413.0) @@ -228,16 +231,16 @@ importers: version: 7.26.7 '@cloudflare/vite-plugin': specifier: ^1.9.0 - version: 1.9.0(rollup@4.40.1)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0) + version: 1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0) '@swc-node/core': specifier: ^1.13.3 version: 1.13.3(@swc/core@1.11.20)(@swc/types@0.1.21) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) '@vitejs/plugin-rsc': specifier: ^0.4.12 - version: 0.4.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) dedent: specifier: ^1.5.3 version: 1.5.3 @@ -273,8 +276,8 @@ importers: specifier: ^5.7.2 version: 5.7.3 vite: - specifier: ^7.0.1 - version: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + specifier: ^7.0.5 + version: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) packages: @@ -1905,9 +1908,6 @@ packages: peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.0.8': - resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==} - '@types/react@19.1.8': resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} @@ -2655,6 +2655,11 @@ packages: peerDependencies: react: ^19.0.0 + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + react-refresh@0.16.0: resolution: {integrity: sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==} engines: {node: '>=0.10.0'} @@ -2667,6 +2672,10 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2690,6 +2699,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2932,48 +2944,8 @@ packages: yaml: optional: true - vite@7.0.1: - resolution: {integrity: sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@7.0.2: - resolution: {integrity: sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==} + vite@7.0.5: + resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3436,7 +3408,7 @@ snapshots: optionalDependencies: workerd: 1.20250617.0 - '@cloudflare/vite-plugin@1.9.0(rollup@4.40.1)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0)': + '@cloudflare/vite-plugin@1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0)': dependencies: '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) '@mjackson/node-fetch-server': 0.6.1 @@ -3446,7 +3418,7 @@ snapshots: picocolors: 1.1.1 tinyglobby: 0.2.13 unenv: 2.0.0-rc.17 - vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) wrangler: 4.23.0(@cloudflare/workers-types@4.20250413.0) ws: 8.18.0 transitivePeerDependencies: @@ -4289,12 +4261,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@tailwindcss/vite@4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) '@types/babel__core@7.20.5': dependencies: @@ -4355,17 +4327,13 @@ snapshots: dependencies: '@types/react': 19.1.8 - '@types/react@19.0.8': - dependencies: - csstype: 3.1.3 - '@types/react@19.1.8': dependencies: csstype: 3.1.3 '@types/which-pm-runs@1.0.2': {} - '@vitejs/plugin-react@4.7.0(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitejs/plugin-react@4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -4373,22 +4341,22 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-rsc@0.4.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitejs/plugin-rsc@0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': dependencies: '@mjackson/node-fetch-server': 0.7.0 es-module-lexer: 1.7.0 estree-walker: 3.0.3 magic-string: 0.30.17 periscopic: 4.0.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) turbo-stream: 3.1.0 - vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - vitefu: 1.1.1(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vitefu: 1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) '@vitest/expect@3.2.4': dependencies: @@ -4398,13 +4366,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitest/mocker@3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -5109,12 +5077,19 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + react-refresh@0.16.0: {} react-refresh@0.17.0: {} react@19.0.0: {} + react@19.1.0: {} + resolve-pkg-maps@1.0.0: {} rollup@4.34.0: @@ -5174,6 +5149,8 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.26.0: {} + semver@6.3.1: {} semver@7.7.0: {} @@ -5360,7 +5337,7 @@ snapshots: debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) transitivePeerDependencies: - '@types/node' - jiti @@ -5375,13 +5352,13 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) transitivePeerDependencies: - supports-color - typescript @@ -5398,22 +5375,7 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 - vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): - dependencies: - esbuild: 0.25.0 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.6 - rollup: 4.40.1 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 22.13.0 - fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.30.1 - tsx: 4.19.2 - - vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.2) @@ -5428,7 +5390,7 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 - vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.2) @@ -5443,15 +5405,15 @@ snapshots: lightningcss: 1.30.1 tsx: 4.19.2 - vitefu@1.1.1(vite@7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): + vitefu@1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): optionalDependencies: - vite: 7.0.1(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) vitest@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + '@vitest/mocker': 3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -5469,7 +5431,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.2(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) vite-node: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) why-is-node-running: 2.3.0 optionalDependencies: From 46b24777c9df5870313ddc8837aee3a740c304e0 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Mon, 21 Jul 2025 19:57:18 -0400 Subject: [PATCH 05/16] chore: fix hono api tests --- e2e/durable-object.spec.ts | 6 +- e2e/fixture/index.ts | 2 +- e2e/hono-handler.spec.ts | 65 +++++++++---------- e2e/templates/basic/app/entry.server.tsx | 15 +---- e2e/templates/basic/app/root.tsx | 14 ++++ .../cli/src/commands/provision/postgres.ts | 34 +++++----- packages/cli/src/commands/provision/sqlite.ts | 28 ++++---- 7 files changed, 83 insertions(+), 81 deletions(-) create mode 100644 e2e/templates/basic/app/root.tsx diff --git a/e2e/durable-object.spec.ts b/e2e/durable-object.spec.ts index e444c32..1784a22 100644 --- a/e2e/durable-object.spec.ts +++ b/e2e/durable-object.spec.ts @@ -46,7 +46,7 @@ test.multi( }, { ...baseFiles, - "app/routes/_index.tsx": ` + "app/routes/index.tsx.tsx": ` ${mainRoute} export class Test extends RouteDurableObject { async loader() { @@ -70,7 +70,7 @@ test.multi( }, { ...baseFiles, - "app/routes/_index.tsx": ` + "app/routes/index.tsx.tsx": ` ${mainRoute} export class Test extends RouteDurableObject { async loader() { @@ -119,7 +119,7 @@ test.multi( }, { ...baseFiles, - "app/routes/_index.tsx": ` + "app/routes/index.tsx.tsx": ` ${mainRoute} export class Test extends RouteDurableObject { async loader() { diff --git a/e2e/fixture/index.ts b/e2e/fixture/index.ts index 5e4078f..982fbb3 100644 --- a/e2e/fixture/index.ts +++ b/e2e/fixture/index.ts @@ -227,7 +227,7 @@ export * from "@playwright/test"; const baseConfig = { name: "basic", - main: "./app/entry.server.ts", + main: "./app/entry.server.tsx", compatibility_date: "2025-05-11", compatibility_flags: ["nodejs_compat"], assets: { diff --git a/e2e/hono-handler.spec.ts b/e2e/hono-handler.spec.ts index e34a694..c98556d 100644 --- a/e2e/hono-handler.spec.ts +++ b/e2e/hono-handler.spec.ts @@ -7,15 +7,15 @@ test.multi( await expect(page.getByText("Hello World")).toBeVisible(); }, { - "app/routes/_index.tsx": ` - export default function Index() { + "app/routes/index.tsx.tsx": ` + export default async function Index() { return
Hello World
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -23,7 +23,7 @@ test.multi( app.get("/api/status", (c) => c.json({ status: "ok" })); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, @@ -37,15 +37,15 @@ test.multi( await expect(page.getByText('{"status":"ok"}')).toBeVisible(); }, { - "app/routes/_index.tsx": ` - export default function Index() { + "app/routes/index.tsx.tsx": ` + export default async function Index() { return
Hello World
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -53,7 +53,7 @@ test.multi( app.get("/api/status", (c) => c.json({ status: "ok" })); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, @@ -70,15 +70,15 @@ test.multi( await expect(page.getByText('{"version":"1.0.0"}')).toBeVisible(); }, { - "app/routes/_index.tsx": ` - export default function Index() { + "app/routes/index.tsx.tsx": ` + export default async function Index() { return
Hello World
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -87,7 +87,7 @@ test.multi( app.get("/api/version", (c) => c.json({ version: "1.0.0" })); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, @@ -112,15 +112,15 @@ test.multi( expect(response).toEqual({ echo: "test" }); }, { - "app/routes/_index.tsx": ` - export default function Index() { + "app/routes/index.tsx.tsx": ` + export default async function Index() { return
Hello World
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -131,7 +131,7 @@ test.multi( }); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, @@ -145,8 +145,8 @@ test.multi( await expect(page.getByText("About Page")).toBeVisible(); }, { - "app/routes/_index.tsx": ` - export default function Index() { + "app/routes/index.tsx.tsx": ` + export default async function Index() { return
Hello World
; } `, @@ -155,10 +155,10 @@ test.multi( return
About Page
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -166,7 +166,7 @@ test.multi( app.get("/api/info", (c) => c.json({ info: "available" })); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, @@ -182,21 +182,16 @@ test.multi( { "app/routes/variables.tsx": ` import { variables } from "@orange-js/orange/hono"; - import { useLoaderData } from "@orange-js/orange"; - export async function loader() { - return variables(); - } - - export default function Index() { - const { test } = useLoaderData(); + export default async function Index() { + const { test } = variables(); return
{JSON.stringify(test)}
; } `, - "app/entry.server.ts": ` + "app/entry.server.tsx": ` import { Hono } from "hono"; import { handler } from "@orange-js/orange/hono"; - import * as serverBuild from "virtual:orange/server-bundle"; + import { Root } from "./root"; const app = new Hono(); @@ -209,7 +204,7 @@ test.multi( }); // Use the handler to create middleware for React routing - app.use("*", handler(serverBuild)); + app.use("*", handler(Root)); export default app; `, diff --git a/e2e/templates/basic/app/entry.server.tsx b/e2e/templates/basic/app/entry.server.tsx index 9cc89ce..8cd96bb 100644 --- a/e2e/templates/basic/app/entry.server.tsx +++ b/e2e/templates/basic/app/entry.server.tsx @@ -1,17 +1,4 @@ import { app } from "@orange-js/orange/server"; -import rootStyles from "./root.css?inline"; - -export function Root({ children }: { children: React.ReactNode }) { - return ( - - - - - - - {children} - - ); -} +import { Root } from "./root"; export default app(Root); diff --git a/e2e/templates/basic/app/root.tsx b/e2e/templates/basic/app/root.tsx new file mode 100644 index 0000000..ab4d925 --- /dev/null +++ b/e2e/templates/basic/app/root.tsx @@ -0,0 +1,14 @@ +import rootStyles from "./root.css?inline"; + +export function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + ); +} diff --git a/packages/cli/src/commands/provision/postgres.ts b/packages/cli/src/commands/provision/postgres.ts index 5c83cd5..c346626 100644 --- a/packages/cli/src/commands/provision/postgres.ts +++ b/packages/cli/src/commands/provision/postgres.ts @@ -40,7 +40,7 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { start: "Fetching existing databases...", success: (value) => `Found ${value.result.length} existing databases`, error: "Failed to fetch existing databases", - }, + } ); const existingDatabases = hyperdriveDatbases.result.map((db) => db.name); @@ -132,7 +132,7 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { success: (value) => `Database ${databaseName} provisioned as ${orange(value.id)}`, error: "Failed to provision database", - }, + } ); patchConfig( @@ -146,7 +146,7 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { }, ], }, - true, + true ); const createClient = await confirm({ @@ -162,15 +162,15 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { connectFileTemplate( databaseName, 'import { Client } from "pg";', - `new Client(env.${camelCase(databaseName)}.connectionString)`, - ), + `new Client(env.${camelCase(databaseName)}.connectionString)` + ) ) .with("postgres", () => connectFileTemplate( databaseName, 'import postgres from "postgres";', - `postgres(env.${camelCase(databaseName)}.connectionString)`, - ), + `postgres(env.${camelCase(databaseName)}.connectionString)` + ) ) .with("drizzle-orm-pg", () => connectFileTemplate( @@ -179,8 +179,8 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { 'import { drizzle } from "drizzle-orm/node-postgres";', 'import * as schema from "./schema.server";', ], - `drizzle(env.${camelCase(databaseName)}.connectionString, { schema })`, - ), + `drizzle(env.${camelCase(databaseName)}.connectionString, { schema })` + ) ) .with("drizzle-orm-postgres", () => connectFileTemplate( @@ -189,8 +189,8 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { 'import { drizzle } from "drizzle-orm/postgres-js";', 'import * as schema from "./schema.server";', ], - `drizzle(env.${camelCase(databaseName)}.connectionString, { schema })`, - ), + `drizzle(env.${camelCase(databaseName)}.connectionString, { schema })` + ) ) .exhaustive(); @@ -209,15 +209,17 @@ export async function provisionPostgres(client: Cloudflare, accountId: string) { step( `Created connection to postgres accessible via \`${c.dim( - `env.${camelCase(databaseName)}`, - )}\` and \`${c.dim(`context.${camelCase(databaseName)}`)}\``, + `env.${camelCase(databaseName)}` + )}\` and \`${c.dim(`context.${camelCase(databaseName)}`)}\`` ); warn( dedent` - Add \`${orange("connect")}\` from \`${orange("app/database.context.ts")}\` to your entrypoint defined in \`${orange("app/entry.server.ts")}\` + Add \`${orange("connect")}\` from \`${orange( + "app/database.context.ts" + )}\` to your entrypoint defined in \`${orange("app/entry.server.tsx")}\` See more at ${orange("https://orange-js.dev/docs/context")} - `.trim(), + `.trim() ); } @@ -273,7 +275,7 @@ async function determineClient(): Promise< function connectFileTemplate( databaseName: string, imports: string | string[], - databaseExpr: string, + databaseExpr: string ) { return dedent` import { env } from "cloudflare:workers"; diff --git a/packages/cli/src/commands/provision/sqlite.ts b/packages/cli/src/commands/provision/sqlite.ts index 7015093..cfc1c9e 100644 --- a/packages/cli/src/commands/provision/sqlite.ts +++ b/packages/cli/src/commands/provision/sqlite.ts @@ -70,7 +70,7 @@ export async function provisionSqlite(client: Cloudflare, accountId: string) { start: "Creating database...", success: () => "Database created", error: "Failed to create database", - }, + } ); const extraDbConfig = isDrizzleInstalled() @@ -91,7 +91,7 @@ export async function provisionSqlite(client: Cloudflare, accountId: string) { }, ], }, - true, + true ); await loader(generateWranglerTypes(), { @@ -107,31 +107,35 @@ export async function provisionSqlite(client: Cloudflare, accountId: string) { 'import { drizzle } from "drizzle-orm/d1";', 'import * as schema from "./schema.server";', ], - `drizzle(env.${camelCase(sqliteName)}, { schema })`, + `drizzle(env.${camelCase(sqliteName)}, { schema })` ); writeFileSync(join(process.cwd(), "app/database.server.ts"), databaseFile); log( c.dim("You'll need to create a schema file in your app directory."), - c.dim("See more at https://orm.drizzle.team/docs/sql-schema-declaration"), + c.dim("See more at https://orm.drizzle.team/docs/sql-schema-declaration") ); step( - `Created SQLite database accessible via \`${c.dim(`context.${camelCase(sqliteName)}`)}\``, + `Created SQLite database accessible via \`${c.dim( + `context.${camelCase(sqliteName)}` + )}\`` ); warn( dedent` - Add \`${orange("database")}\` from \`${orange("app/database.context.ts")}\` to your entrypoint defined in \`${orange("app/entry.server.ts")}\` + Add \`${orange("database")}\` from \`${orange( + "app/database.context.ts" + )}\` to your entrypoint defined in \`${orange("app/entry.server.tsx")}\` See more at ${orange("https://orange-js.dev/docs/context")} - `.trim(), + `.trim() ); } else { step( `Created SQLite database accessible via \`${c.dim( - `env.${camelCase(sqliteName)}`, - )}\``, + `env.${camelCase(sqliteName)}` + )}\`` ); } } @@ -146,7 +150,7 @@ async function existingDatabaseNames(client: Cloudflare, accountId: string) { names.push( ...existingDatabases.result .map((db) => db.name?.toLowerCase()) - .filter((name) => name !== undefined), + .filter((name) => name !== undefined) ); while (existingDatabases.result.length === 100) { @@ -159,7 +163,7 @@ async function existingDatabaseNames(client: Cloudflare, accountId: string) { names.push( ...existingDatabases.result .map((db) => db.name?.toLowerCase()) - .filter((name) => name !== undefined), + .filter((name) => name !== undefined) ); } @@ -169,7 +173,7 @@ async function existingDatabaseNames(client: Cloudflare, accountId: string) { function databaseFileTemplate( databaseName: string, imports: string | string[], - databaseExpr: string, + databaseExpr: string ) { return dedent` import { env } from "cloudflare:workers"; From c0db68a735108accdcddc4f941aa40fc1cf4598f Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 18:41:48 -0400 Subject: [PATCH 06/16] feat: actors --- e2e/actors.spec.ts | 193 ++++++++++++++++++ e2e/api.spec.ts | 56 ----- e2e/durable-object.spec.ts | 139 ------------- e2e/fixture/index.ts | 12 +- e2e/hono-handler.spec.ts | 10 +- e2e/templates/basic/package.json | 1 + packages/actors/package.json | 33 +++ packages/actors/src/cl.tsx | 53 +++++ packages/actors/src/index.tsx | 115 +++++++++++ packages/actors/src/observed.tsx | 56 +++++ packages/actors/tsconfig.json | 12 ++ packages/core/src/actor.tsx | 68 +----- packages/core/src/client.tsx | 9 +- packages/core/src/server.tsx | 48 +++-- packages/vite/src/index.ts | 13 +- packages/vite/src/plugins/config.ts | 2 +- packages/vite/src/plugins/isolation.ts | 4 +- .../vite/src/plugins/preserve-class-names.ts | 78 +++++++ pnpm-lock.yaml | 161 +++++++++++---- 19 files changed, 722 insertions(+), 341 deletions(-) create mode 100644 e2e/actors.spec.ts delete mode 100644 e2e/api.spec.ts delete mode 100644 e2e/durable-object.spec.ts create mode 100644 packages/actors/package.json create mode 100644 packages/actors/src/cl.tsx create mode 100644 packages/actors/src/index.tsx create mode 100644 packages/actors/src/observed.tsx create mode 100644 packages/actors/tsconfig.json create mode 100644 packages/vite/src/plugins/preserve-class-names.ts diff --git a/e2e/actors.spec.ts b/e2e/actors.spec.ts new file mode 100644 index 0000000..782073c --- /dev/null +++ b/e2e/actors.spec.ts @@ -0,0 +1,193 @@ +import { test, expect, wranglerJson } from "./fixture/index"; + +const baseFiles = { + "wrangler.jsonc": wranglerJson({ + durable_objects: { + bindings: [ + { + name: "Test", + class_name: "Test", + }, + ], + }, + migrations: [ + { + tag: "v1", + new_sqlite_classes: ["Test"], + }, + ], + }), + "app/entry.server.tsx": ` + import { app } from "@orange-js/orange/server"; + import { Root } from "./root"; + + export { Test } from "./routes/index.tsx"; + + export default app(Root); + `, +}; + +test.multi( + "can call actor method", + async ({ page, port }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Hello from Actor")).toBeVisible(); + }, + { + ...baseFiles, + "app/routes/index.tsx": ` + import { Actor, getActor } from "@orange-js/actors"; + + export class Test extends Actor { + async load() { + return { + message: "Hello from Actor", + }; + } + } + + export default async function Home() { + const stub = getActor(Test, "foo"); + const { message } = await stub.load(); + return
{message}
; + } + `, + } +); + +test.multi( + "using actor component", + async ({ page, port }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Hello from Actor")).toBeVisible(); + await expect(page.getByText("id: foo")).toBeVisible(); + }, + { + ...baseFiles, + "app/routes/index.tsx": ` + import { Actor, getActor } from "@orange-js/actors"; + + export class Test extends Actor { + async Component() { + return ( +
+ Hello from Actor + id: {this.identifier} +
+ ); + } + } + + export default async function Home() { + return ; + } + `, + } +); + +test.multi( + "actor as component", + async ({ page, port }) => { + await page.goto(`http://localhost:${port}/?id=foo`); + await expect(page.getByText("Hello from Actor")).toBeVisible(); + await expect(page.getByText("id: foo")).toBeVisible(); + }, + { + ...baseFiles, + "app/routes/index.tsx": ` + import { Actor, getActor } from "@orange-js/actors"; + + export default class Test extends Actor { + static nameFromRequest(request: Request) { + const url = new URL(request.url); + return url.searchParams.get("id") ?? "default"; + } + + async Component() { + return ( +
+ Hello from Actor + id: {this.identifier} +
+ ); + } + } + `, + "app/entry.server.tsx": ` + import { app } from "@orange-js/orange/server"; + import { Root } from "./root"; + + export { default as Test } from "./routes/index.tsx"; + + export default app(Root); + `, + } +); + +test.multi( + "multiplayer", + async ({ page, port, browser }) => { + const secondTab = await browser.newPage(); + await secondTab.goto(`http://localhost:${port}/`); + await expect(secondTab.getByText("count: 0")).toBeVisible(); + + await page.goto(`http://localhost:${port}/`); + await expect(page.getByText("count: 0")).toBeVisible(); + + // Increment and ensure both tabs update + await page.click("button"); + + await expect(secondTab.getByText("count: 1")).toBeVisible({ + timeout: 1000, + }); + await expect(page.getByText("count: 1")).toBeVisible({ + timeout: 1000, + }); + }, + { + ...baseFiles, + "app/routes/index.tsx": ` + import { Actor, getActor, Observed, Persist } from "@orange-js/actors"; + + export class Test extends Actor { + @Persist + count = 0; + + async increment() { + this.count++; + } + + @Observed("count") + async Component() { + return ( +
+ count: {this.count} +
+ ); + } + } + + async function increment() { + "use server"; + Test.get("foo")!.increment(); + } + + export default function Home() { + return ( +
+ + +
+ ); + } + `, + "app/entry.server.tsx": ` + import { app } from "@orange-js/orange/server"; + import { Root } from "./root"; + + export { Test } from "./routes/index.tsx"; + + export default app(Root); + `, + } +); diff --git a/e2e/api.spec.ts b/e2e/api.spec.ts deleted file mode 100644 index b7ac3e7..0000000 --- a/e2e/api.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { test, expect } from "./fixture/index"; - -test.multi( - "api route", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}/api`); - await expect(page.getByText("Hello World")).toBeVisible(); - }, - { - // TODO(zebp): we don't support tsx files in api routes yet - "app/routes/api.ts": ` - export default { - async fetch(request: Request) { - return new Response("Hello World"); - } - } - `, - } -); - -test.multi( - "working to subpath works", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}/api/foo/bar/baz`); - await expect(page.getByText("Hello World")).toBeVisible(); - }, - { - // TODO(zebp): we don't support tsx files in api routes yet - "app/routes/api.ts": ` - export default { - async fetch(request: Request) { - return new Response("Hello World"); - } - } - `, - } -); - -test.multi( - "hono", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}/api`); - await expect(page.getByText("Hello World")).toBeVisible(); - }, - { - "app/routes/api.ts": ` - import { Hono } from "hono"; - - const app = new Hono(); - - app.get("/", (c) => c.text("Hello World")); - - export default app; - `, - } -); diff --git a/e2e/durable-object.spec.ts b/e2e/durable-object.spec.ts deleted file mode 100644 index 1784a22..0000000 --- a/e2e/durable-object.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { test, expect, wranglerJson } from "./fixture/index"; - -const baseFiles = { - "wrangler.jsonc": wranglerJson({ - durable_objects: { - bindings: [ - { - name: "Test", - class_name: "Test", - }, - ], - }, - migrations: [ - { - tag: "v1", - new_sqlite_classes: ["Test"], - }, - ], - }), -}; - -const mainRoute = ` -import { RouteDurableObject, useDurableObject, Form } from "@orange-js/orange"; - -export default function Home({}: Route.ComponentProps) { - const { message } = useDurableObject(); - - return ( -
-

{message}

- - - -
- ); -} -`; - -test.multi( - "loader", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}`); - await expect(page.getByText("Hello Durable Object")).toBeVisible(); - }, - { - ...baseFiles, - "app/routes/index.tsx.tsx": ` - ${mainRoute} - export class Test extends RouteDurableObject { - async loader() { - return { - message: "Hello Durable Object", - }; - } - - static async id() { - return "foo"; - } - }`, - } -); - -test.multi( - "loader with static id", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}`); - await expect(page.getByText("Hello Durable Object")).toBeVisible(); - }, - { - ...baseFiles, - "app/routes/index.tsx.tsx": ` - ${mainRoute} - export class Test extends RouteDurableObject { - async loader() { - return { - message: "Hello Durable Object", - }; - } - - static id = "foo"; - }`, - } -); - -test.multi( - "loader with dynamic id", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}/test/foo`); - await expect(page.getByText("Hello foo")).toBeVisible(); - }, - { - ...baseFiles, - "app/routes/test.$test.tsx": ` - ${mainRoute} - - export class Test extends RouteDurableObject { - async loader({ params }) { - return { - message: \`Hello \${params.test}\`, - }; - } - - static async id({ params }) { - return params.test; - } - }`, - } -); - -test.multi( - "action", - async ({ page, port }) => { - await page.goto(`http://localhost:${port}`); - await expect(page.getByText("Hello Durable Object")).toBeVisible(); - await page.click("#button"); - await expect(page.getByText("Updated")).toBeVisible(); - }, - { - ...baseFiles, - "app/routes/index.tsx.tsx": ` - ${mainRoute} - export class Test extends RouteDurableObject { - async loader() { - const message = await this.ctx.storage.get("message"); - return { - message: message ?? "Hello Durable Object", - }; - } - - async action() { - this.ctx.storage.put("message", "Updated"); - } - - static id = "foo"; - }`, - } -); diff --git a/e2e/fixture/index.ts b/e2e/fixture/index.ts index 982fbb3..d0614a4 100644 --- a/e2e/fixture/index.ts +++ b/e2e/fixture/index.ts @@ -2,7 +2,7 @@ import dedent from "dedent"; import getPort from "get-port"; import * as path from "node:path"; import * as fs from "node:fs/promises"; -import { test as base, Page } from "@playwright/test"; +import { test as base, Browser, Page } from "@playwright/test"; import { spawn } from "node:child_process"; import { Unstable_Config } from "wrangler"; import { stripVTControlCharacters } from "node:util"; @@ -209,17 +209,17 @@ test.multi = multitest; function multitest( title: string, - fn: (opts: { page: Page; port: number }) => Promise, + fn: (opts: { page: Page; port: number; browser: Browser }) => Promise, files: Files = {} ) { - test(`${title} dev`, async ({ page, dev }) => { + test(`${title} dev`, async ({ page, dev, browser }) => { const { port } = await dev(files); - await fn({ page, port }); + await fn({ page, port, browser }); }); - test(`${title} worker`, async ({ page, worker }) => { + test(`${title} worker`, async ({ page, worker, browser }) => { const { port } = await worker(files); - await fn({ page, port }); + await fn({ page, port, browser }); }); } diff --git a/e2e/hono-handler.spec.ts b/e2e/hono-handler.spec.ts index c98556d..b979248 100644 --- a/e2e/hono-handler.spec.ts +++ b/e2e/hono-handler.spec.ts @@ -7,7 +7,7 @@ test.multi( await expect(page.getByText("Hello World")).toBeVisible(); }, { - "app/routes/index.tsx.tsx": ` + "app/routes/index.tsx": ` export default async function Index() { return
Hello World
; } @@ -37,7 +37,7 @@ test.multi( await expect(page.getByText('{"status":"ok"}')).toBeVisible(); }, { - "app/routes/index.tsx.tsx": ` + "app/routes/index.tsx": ` export default async function Index() { return
Hello World
; } @@ -70,7 +70,7 @@ test.multi( await expect(page.getByText('{"version":"1.0.0"}')).toBeVisible(); }, { - "app/routes/index.tsx.tsx": ` + "app/routes/index.tsx": ` export default async function Index() { return
Hello World
; } @@ -112,7 +112,7 @@ test.multi( expect(response).toEqual({ echo: "test" }); }, { - "app/routes/index.tsx.tsx": ` + "app/routes/index.tsx": ` export default async function Index() { return
Hello World
; } @@ -145,7 +145,7 @@ test.multi( await expect(page.getByText("About Page")).toBeVisible(); }, { - "app/routes/index.tsx.tsx": ` + "app/routes/index.tsx": ` export default async function Index() { return
Hello World
; } diff --git a/e2e/templates/basic/package.json b/e2e/templates/basic/package.json index bea1e16..7abe4a8 100644 --- a/e2e/templates/basic/package.json +++ b/e2e/templates/basic/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@orange-js/orange": "workspace:", + "@orange-js/actors": "workspace:", "@tailwindcss/vite": "^4.0.14", "hono": "^4.6.20", "react": "^19.1.0", diff --git a/packages/actors/package.json b/packages/actors/package.json new file mode 100644 index 0000000..ee1e980 --- /dev/null +++ b/packages/actors/package.json @@ -0,0 +1,33 @@ +{ + "name": "@orange-js/actors", + "version": "0.3.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc -w" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@vitejs/plugin-rsc": "^0.4.12", + "react": "^19.1.0", + "typescript": "^5.7.2" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.13.1", + "peerDependencies": { + "@cloudflare/actors": "0.0.1-beta.1", + "@vitejs/plugin-rsc": "^0.4.12", + "react": "^19.1.0" + } +} diff --git a/packages/actors/src/cl.tsx b/packages/actors/src/cl.tsx new file mode 100644 index 0000000..c7f37c2 --- /dev/null +++ b/packages/actors/src/cl.tsx @@ -0,0 +1,53 @@ +"use client"; +import { useEffect, useState } from "react"; + +// TODO: remove this hack once dependency de-dupe works +function rsc() { + // @ts-ignore + return globalThis.rsc as typeof import("@vitejs/plugin-rsc/rsc"); +} + +// @ts-ignore +globalThis._dawgdhaigdauwgd = "use client"; + +type ClientComponentProps = { + children: React.ReactNode; + actorName: string; + id: string; +}; + +export function ClientComponent({ + children, + actorName, + id, +}: ClientComponentProps) { + const [component, setComponent] = useState(null); + + useEffect(() => { + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const ws = new WebSocket( + `${protocol}//${window.location.host}/${actorName}/${id}` + ); + + ws.addEventListener("message", async (event) => { + const data = event.data as Blob; + const bytes = await data.arrayBuffer(); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(bytes)); + controller.close(); + }, + }); + const created = await rsc().createFromReadableStream(stream); + setComponent((created as any).root); + }); + + return () => ws.close(); + }, []); + + if (!component) { + return children; + } + + return component; +} diff --git a/packages/actors/src/index.tsx b/packages/actors/src/index.tsx new file mode 100644 index 0000000..d2c48b6 --- /dev/null +++ b/packages/actors/src/index.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import { ActorState, Actor as CfActor, getActor } from "@cloudflare/actors"; +import { isValidElement } from "react"; +import { ClientComponent } from "./cl.js"; + +export * from "@cloudflare/actors"; + +export * from "./observed.js"; + +// TODO: remove this hack once dependency de-dupe works +function rsc() { + // @ts-ignore + return globalThis.rsc as typeof import("@vitejs/plugin-rsc/rsc"); +} + +type RSCPayload = { root: React.ReactNode }; + +export abstract class Actor extends CfActor { + abstract Component( + props: Record + ): React.ReactNode | Promise; + + async __rscStream( + name: string, + props: Record + ): Promise { + const Component = (this[name as keyof this] as any).bind(this); + const rscStream = rsc().renderToReadableStream({ + root: , + }); + return rscStream; + } + + static Component = Component; + + static { + Object.defineProperty(this, "__orangeIsActor", { + value: true, + enumerable: false, + }); + } +} + +async function InternalComponent, Env>( + props: { + actor: ActorConstructor; + name?: string; + } & PropsFromDurableObject +) { + if ("children" in props) { + throw new Error("Children are not currently supported"); + } + + for (const key in props) { + if (key === "actor") { + continue; + } + + const value = props[key as keyof typeof props]; + if (typeof value === "function") { + throw new Error("Functions are not currently supported"); + } + + if (isValidElement(value)) { + throw new Error("React components are not currently supported"); + } + } + + const stub = getActor(props.actor, props.name ?? "default"); + const { actor, ...rest } = props; + + // TODO: Remove this hack once the data-race in actors is fixed + await new Promise((resolve) => setTimeout(resolve, 25)); + + const rscStream = await stub.__rscStream("Component", rest); + const payload = await rsc().createFromReadableStream(rscStream); + + return payload.root; +} + +type PropsFromDurableObject< + T extends Actor, + Env, + K extends keyof T +> = T[K] extends (arg: infer Z) => React.ReactNode | Promise + ? Z + : never; + +export type ActorConstructor = Actor> = new ( + state: ActorState, + env: any +) => T; + +export { getActor } from "@cloudflare/actors"; + +async function Component, Env>( + props: { + actor: ActorConstructor; + name?: string; + observed?: boolean; + } & PropsFromDurableObject +) { + if (props.observed) { + return ( + + + + ); + } + + return await InternalComponent(props); +} diff --git a/packages/actors/src/observed.tsx b/packages/actors/src/observed.tsx new file mode 100644 index 0000000..43aa0fc --- /dev/null +++ b/packages/actors/src/observed.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Actor } from "./index.js"; +import { type JSX } from "react"; + +// TODO: remove this hack once dependency de-dupe works +function rsc() { + // @ts-ignore + return globalThis.rsc as typeof import("@vitejs/plugin-rsc/rsc"); +} + +type ClassMethodDecorator = ( + value: (...args: Args) => Return, + context: ClassMethodDecoratorContext +) => any; + +export function Observed( + ...names: string[] +): ClassMethodDecorator> { + return function (this: Actor, value, context) { + context.addInitializer(function () { + const self = this as Actor; + + self["onPersist"] = async () => { + const ret = await value.apply(this); + const stream = rsc().renderToReadableStream({ + root: ret, + }); + + const rscPayload = await new Response(stream).bytes(); + + // @ts-ignore + const websockets = self.ctx.getWebSockets(); + + // @ts-ignore + for (const ws of websockets) { + ws.send(rscPayload); + } + }; + + self["fetch"] = async (request: Request) => { + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // @ts-ignore + self.ctx.acceptWebSocket(server); + + return new Response(null, { + status: 101, + webSocket: client, + }); + }; + }); + + return value; + }; +} diff --git a/packages/actors/tsconfig.json b/packages/actors/tsconfig.json new file mode 100644 index 0000000..93f6381 --- /dev/null +++ b/packages/actors/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": [ + "vite/client", + "@vitejs/plugin-rsc/types", + "@cloudflare/workers-types", + "@types/node" + ], + "outDir": "dist" + } +} diff --git a/packages/core/src/actor.tsx b/packages/core/src/actor.tsx index bf5f1ba..c0689da 100644 --- a/packages/core/src/actor.tsx +++ b/packages/core/src/actor.tsx @@ -1,66 +1,6 @@ -import * as React from "react"; -import { Actor } from "@cloudflare/actors"; -import { RscPayload } from "./server.js"; -import * as rsc from "@vitejs/plugin-rsc/rsc"; -import { isValidElement } from "react"; +import type { Actor } from "@orange-js/actors"; -type PropsFromDurableObject< - T extends ReactActor, - Env, - K extends keyof T, -> = T[K] extends (arg: infer Z) => React.ReactNode | Promise - ? Z - : never; - -export abstract class ReactActor extends Actor { - async rscStream( - name: string, - props: Record, - ): Promise { - const Component = (this[name as keyof this] as any).bind(this); - const rscStream = rsc.renderToReadableStream({ - // in this example, we always render the same `` - root: , - }); - return rscStream; - } - - static Component = Component; - - abstract Component( - props: Record, - ): React.ReactNode | Promise; -} - -async function Component, Env>( - props: { - durableObject: DurableObjectNamespace; - name: string; - } & PropsFromDurableObject, -) { - if ("children" in props) { - throw new Error("Children are not currently supported"); - } - - for (const key in props) { - const value = props[key as keyof typeof props]; - if (typeof value === "function") { - throw new Error("Functions are not currently supported"); - } - - if (isValidElement(value)) { - throw new Error("React components are not currently supported"); - } - } - - const stub = props.durableObject.get( - props.durableObject.idFromName(props.name), - ); - - const { durableObject, ...rest } = props; - - const rscStream = await stub.rscStream("Component", rest); - const payload = await rsc.createFromReadableStream(rscStream); - - return payload.root; +export function isActor(value: unknown): value is Actor { + // @ts-ignore + return typeof value === "function" && value["__orangeIsActor"] === true; } diff --git a/packages/core/src/client.tsx b/packages/core/src/client.tsx index c4b8647..be14aee 100644 --- a/packages/core/src/client.tsx +++ b/packages/core/src/client.tsx @@ -7,6 +7,9 @@ import { getRscStreamFromHtml } from "@vitejs/plugin-rsc/rsc-html-stream/browser import * as ReactDOMClient from "react-dom/client"; import type { RscPayload } from "./server.js"; +// @ts-expect-error +globalThis.rsc = ReactClient; + export async function main() { // stash `setPayload` function to trigger re-rendering // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) @@ -15,7 +18,7 @@ export async function main() { // deserialize RSC stream back to React VDOM for CSR const initialPayload = await ReactClient.createFromReadableStream( // initial RSC stream is injected in SSR stream as - getRscStreamFromHtml(), + getRscStreamFromHtml() ); // browser root component to (re-)render RSC payload as state @@ -37,7 +40,7 @@ export async function main() { // re-fetch RSC and trigger re-rendering async function fetchRscPayload() { const payload = await ReactClient.createFromFetch( - fetch(window.location.href), + fetch(window.location.href) ); setPayload(payload); } @@ -55,7 +58,7 @@ export async function main() { "x-rsc-action": id, }, }), - { temporaryReferences }, + { temporaryReferences } ); setPayload(payload); return payload.returnValue; diff --git a/packages/core/src/server.tsx b/packages/core/src/server.tsx index 040e4ae..7915d1f 100644 --- a/packages/core/src/server.tsx +++ b/packages/core/src/server.tsx @@ -7,11 +7,14 @@ import type { ReactFormState } from "react-dom/client"; import type { ErrorInfo } from "react"; import { router } from "./router.js"; -import { ReactComponent, routes } from "virtual:orange/routes"; -import { ReactActor } from "./actor.js"; -import { env } from "cloudflare:workers"; +import { routes } from "virtual:orange/routes"; +import { isActor } from "./actor.js"; import { CloudflareEnv } from "./index.js"; import { internalContext } from "./internal-context.js"; +import { env } from "cloudflare:workers"; + +// @ts-expect-error +globalThis.rsc = ReactServer; export interface Context { cloudflare: { @@ -22,7 +25,7 @@ export interface Context { export type AppOptions = { context?: ( - env: CloudflareEnv, + env: CloudflareEnv ) => Omit | Promise>; }; @@ -45,7 +48,7 @@ export async function request() { async function handler( request: Request, - Layout: Layout, + Layout: Layout ): Promise { const isAction = request.method === "POST"; let returnValue: unknown | undefined; @@ -90,11 +93,12 @@ async function handler( params: Record; }) => React.ReactNode | Promise; - if (isReactActor(maybeComponent)) { + if (isActor(maybeComponent)) { + // @ts-ignore const name = maybeComponent.nameFromRequest(request); Component = () => ( // @ts-ignore - + ); } else { Component = maybeComponent; @@ -137,7 +141,7 @@ async function rscResponse({ onError(error: unknown, errorInfo: ErrorInfo) { console.error("Error during RSC streaming", error, errorInfo); }, - }, + } ); const url = new URL(request.url); @@ -178,10 +182,29 @@ async function rscResponse({ import.meta.hot?.accept(); +const wsPattern = new URLPattern({ + pathname: "/:actor/:id", +}); + export function app(Layout: Layout, options?: AppOptions) { return { async fetch(request: Request) { try { + if (request.headers.get("Upgrade") === "websocket") { + const match = wsPattern.exec(request.url); + if (!match) { + return new Response("Not found", { status: 404 }); + } + + const { actor, id } = match.pathname.groups; + const ns = (env as any)[actor] as DurableObjectNamespace; + const stubId = ns.idFromName(id); + const stub = ns.get(stubId); + await stub.setIdentifier(id); + // @ts-ignore + return await stub!.fetch(request); + } + return ( (await handler(request, Layout)) ?? new Response("Not found", { status: 404 }) @@ -192,12 +215,3 @@ export function app(Layout: Layout, options?: AppOptions) { }, }; } - -function isReactActor( - maybeComponent: ReactComponent | typeof ReactActor, -): maybeComponent is typeof ReactActor { - return ( - typeof maybeComponent === "function" && - Object.getPrototypeOf(maybeComponent) === ReactActor - ); -} diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 1b27434..b352efe 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -10,6 +10,7 @@ import { configPlugin } from "./plugins/config.js"; import { routesPlugin } from "./plugins/routes.js"; import { isolation } from "./plugins/isolation.js"; import { Config, resolveConfig } from "./config.js"; +import { preserveClassNames } from "./plugins/preserve-class-names.js"; export * from "./routing/fs-routes.js"; @@ -18,7 +19,7 @@ export type OrangeRSCPluginOptions = { }; export default function orange( - options: OrangeRSCPluginOptions = {}, + options: OrangeRSCPluginOptions = {} ): PluginOption[] { let _config: Config; @@ -48,11 +49,11 @@ export default function orange( entries: { client: entrypoint( "entry.browser", - "node_modules/@orange-js/vite/dist/entrypoints/entry.browser.js", + "node_modules/@orange-js/vite/dist/entrypoints/entry.browser.js" ), ssr: entrypoint( "entry.ssr", - "node_modules/@orange-js/vite/dist/entrypoints/entry.ssr.js", + "node_modules/@orange-js/vite/dist/entrypoints/entry.ssr.js" ), // rsc: entrypoint( // "entry.rsc", @@ -62,13 +63,15 @@ export default function orange( serverHandler: false, loadModuleDevProxy: true, }), + preserveClassNames(), + // @ts-ignore cloudflare( options.cloudflare ?? { configPath: "./wrangler.jsonc", viteEnvironment: { name: "rsc", }, - }, + } ), routesPlugin(config), ]; @@ -80,7 +83,7 @@ function entrypoint(name: string, fallback: string) { process.cwd(), "src", "entrypoints", - `${name}.${extension}`, + `${name}.${extension}` ); if (fs.existsSync(entrypoint)) { return entrypoint; diff --git a/packages/vite/src/plugins/config.ts b/packages/vite/src/plugins/config.ts index e56e546..fc72001 100644 --- a/packages/vite/src/plugins/config.ts +++ b/packages/vite/src/plugins/config.ts @@ -15,7 +15,7 @@ export function configPlugin(): Plugin { }, }, optimizeDeps: { - exclude: ["virtual:orange/routes"], + exclude: ["virtual:orange/routes", "@orange-js/actors"], }, }, ssr: { diff --git a/packages/vite/src/plugins/isolation.ts b/packages/vite/src/plugins/isolation.ts index 1634cd2..8911884 100644 --- a/packages/vite/src/plugins/isolation.ts +++ b/packages/vite/src/plugins/isolation.ts @@ -51,14 +51,14 @@ export function isolation(): Plugin[] { } const inAppDir = (importPath: string) => - path.resolve(importPath).startsWith(path.resolve("./src")); + path.resolve(importPath).startsWith(path.resolve("./app")); const emptyExports = (exports: string[]) => { return exports .map((e) => e === "default" ? "export default undefined;" - : `export const ${e} = undefined;`, + : `export const ${e} = undefined;` ) .join("\n"); }; diff --git a/packages/vite/src/plugins/preserve-class-names.ts b/packages/vite/src/plugins/preserve-class-names.ts new file mode 100644 index 0000000..36eac09 --- /dev/null +++ b/packages/vite/src/plugins/preserve-class-names.ts @@ -0,0 +1,78 @@ +import * as path from "node:path"; +import type { Plugin } from "vite"; +import { isEcmaLike } from "../util.js"; +import _parse from "@babel/parser"; +import _generate from "@babel/generator"; +import _traverse from "@babel/traverse"; +import { + callExpression, + expressionStatement, + identifier, + memberExpression, + objectExpression, + objectProperty, + staticBlock, + stringLiteral, + thisExpression, + booleanLiteral, +} from "@babel/types"; + +const parse = _parse.parse; +const generate = _generate.default; +const traverse = _traverse.default; + +export function preserveClassNames(): Plugin { + return { + name: "orange:preserve-class-names", + enforce: "pre", + transform(code, id) { + if (!isEcmaLike(id) || !inAppDir(id)) { + return; + } + + const ast = parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators"], + }); + + traverse(ast, { + ClassDeclaration(path) { + const { id } = path.node; + if (!id) return; + + path.node.body.body.unshift( + staticBlock([ + expressionStatement( + callExpression( + memberExpression( + identifier("Object"), + identifier("defineProperty") + ), + [ + thisExpression(), + stringLiteral("name"), + objectExpression([ + objectProperty( + stringLiteral("value"), + stringLiteral(id.name) + ), + objectProperty( + stringLiteral("enumerable"), + booleanLiteral(false) + ), + ]), + ] + ) + ), + ]) + ); + }, + }); + + return generate(ast).code; + }, + }; +} + +const inAppDir = (importPath: string) => + path.resolve(importPath).startsWith(path.resolve("./app")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9675127..a63192e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,15 @@ importers: e2e/templates/basic: dependencies: + '@orange-js/actors': + specifier: 'workspace:' + version: link:../../../packages/actors '@orange-js/orange': specifier: 'workspace:' version: link:../../../packages/core '@tailwindcss/vite': specifier: ^4.0.14 - version: 4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) hono: specifier: ^4.6.20 version: 4.6.20 @@ -68,14 +71,33 @@ importers: version: 5.8.3 vite: specifier: ^7.0.5 - version: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + version: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) wrangler: specifier: ^4.23.0 version: 4.23.0(@cloudflare/workers-types@4.20250413.0) + packages/actors: + dependencies: + '@cloudflare/actors': + specifier: 0.0.1-beta.1 + version: 0.0.1-beta.1 + devDependencies: + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@vitejs/plugin-rsc': + specifier: ^0.4.12 + version: 0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) + react: + specifier: ^19.1.0 + version: 19.1.0 + typescript: + specifier: ^5.7.2 + version: 5.8.3 + packages/cli: dependencies: '@clack/core': @@ -138,7 +160,7 @@ importers: version: 5.7.3 vite: specifier: '6.2' - version: 6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + version: 6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) wrangler: specifier: ^4.7.2 version: 4.9.1(@cloudflare/workers-types@4.20250413.0) @@ -231,16 +253,16 @@ importers: version: 7.26.7 '@cloudflare/vite-plugin': specifier: ^1.9.0 - version: 1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0) + version: 1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0) '@swc-node/core': specifier: ^1.13.3 version: 1.13.3(@swc/core@1.11.20)(@swc/types@0.1.21) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) '@vitejs/plugin-rsc': specifier: ^0.4.12 - version: 0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + version: 0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) dedent: specifier: ^1.5.3 version: 1.5.3 @@ -253,12 +275,15 @@ importers: react-refresh: specifier: ^0.16.0 version: 0.16.0 + terser: + specifier: ^5.43.1 + version: 5.43.1 vite-node: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) devDependencies: '@swc/core': specifier: ^1.11.20 @@ -277,7 +302,7 @@ importers: version: 5.7.3 vite: specifier: ^7.0.5 - version: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + version: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) packages: @@ -1450,6 +1475,9 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -2009,6 +2037,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2075,6 +2106,9 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2555,11 +2589,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@5.1.5: resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} @@ -2743,6 +2772,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -2786,6 +2818,11 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3390,7 +3427,6 @@ snapshots: optionalDependencies: cron-schedule: 5.0.4 nanoid: 5.1.5 - optional: true '@cloudflare/kv-asset-handler@0.4.0': dependencies: @@ -3408,7 +3444,7 @@ snapshots: optionalDependencies: workerd: 1.20250617.0 - '@cloudflare/vite-plugin@1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0)': + '@cloudflare/vite-plugin@1.9.0(rollup@4.40.1)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))(workerd@1.20250617.0)(wrangler@4.23.0)': dependencies: '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) '@mjackson/node-fetch-server': 0.6.1 @@ -3418,7 +3454,7 @@ snapshots: picocolors: 1.1.1 tinyglobby: 0.2.13 unenv: 2.0.0-rc.17 - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) wrangler: 4.23.0(@cloudflare/workers-types@4.20250413.0) ws: 8.18.0 transitivePeerDependencies: @@ -3981,6 +4017,11 @@ snapshots: '@jridgewell/set-array@1.2.1': {} + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': @@ -4261,12 +4302,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@tailwindcss/vite@4.1.11(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) '@types/babel__core@7.20.5': dependencies: @@ -4333,7 +4374,7 @@ snapshots: '@types/which-pm-runs@1.0.2': {} - '@vitejs/plugin-react@4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitejs/plugin-react@4.7.0(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -4341,11 +4382,24 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-rsc@0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitejs/plugin-rsc@0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))': + dependencies: + '@mjackson/node-fetch-server': 0.7.0 + es-module-lexer: 1.7.0 + estree-walker: 3.0.3 + magic-string: 0.30.17 + periscopic: 4.0.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + turbo-stream: 3.1.0 + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) + vitefu: 1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) + + '@vitejs/plugin-rsc@0.4.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))': dependencies: '@mjackson/node-fetch-server': 0.7.0 es-module-lexer: 1.7.0 @@ -4355,8 +4409,8 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) turbo-stream: 3.1.0 - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - vitefu: 1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) + vitefu: 1.1.1(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) '@vitest/expect@3.2.4': dependencies: @@ -4366,13 +4420,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2))': + '@vitest/mocker@3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -4445,6 +4499,8 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) + buffer-from@1.1.2: {} + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -4512,6 +4568,8 @@ snapshots: commander@11.1.0: {} + commander@2.20.3: {} + convert-source-map@2.0.0: {} cookie@0.7.2: {} @@ -4996,8 +5054,6 @@ snapshots: nanoid@3.3.11: {} - nanoid@3.3.8: {} - nanoid@5.1.5: optional: true @@ -5060,7 +5116,7 @@ snapshots: postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -5201,6 +5257,11 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} stackback@0.0.2: {} @@ -5243,6 +5304,13 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -5331,13 +5399,13 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - vite-node@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite-node@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) transitivePeerDependencies: - '@types/node' - jiti @@ -5352,18 +5420,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) transitivePeerDependencies: - supports-color - typescript - vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@6.2.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2): dependencies: esbuild: 0.25.0 postcss: 8.5.3 @@ -5373,9 +5441,10 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 + terser: 5.43.1 tsx: 4.19.2 - vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.2) @@ -5388,9 +5457,10 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 + terser: 5.43.1 tsx: 4.19.2 - vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.2) @@ -5403,17 +5473,22 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 + terser: 5.43.1 tsx: 4.19.2 - vitefu@1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)): + vitefu@1.1.1(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)): + optionalDependencies: + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) + + vitefu@1.1.1(vite@7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)): optionalDependencies: - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) - vitest@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2): + vitest@3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2)) + '@vitest/mocker': 3.2.4(vite@7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -5431,8 +5506,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) - vite-node: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.2) + vite: 7.0.5(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) + vite-node: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.0 From e9d539029cacd65f931b54718ec6f9a9ca56800d Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 19:36:50 -0400 Subject: [PATCH 07/16] fix lockfile --- pnpm-lock.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a63192e..f21005b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,9 +275,6 @@ importers: react-refresh: specifier: ^0.16.0 version: 0.16.0 - terser: - specifier: ^5.43.1 - version: 5.43.1 vite-node: specifier: ^3.2.4 version: 3.2.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.2) @@ -4021,6 +4018,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 + optional: true '@jridgewell/sourcemap-codec@1.5.0': {} @@ -4499,7 +4497,8 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) - buffer-from@1.1.2: {} + buffer-from@1.1.2: + optional: true bundle-name@4.1.0: dependencies: @@ -4568,7 +4567,8 @@ snapshots: commander@11.1.0: {} - commander@2.20.3: {} + commander@2.20.3: + optional: true convert-source-map@2.0.0: {} @@ -5261,6 +5261,7 @@ snapshots: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true source-map@0.6.1: {} @@ -5310,6 +5311,7 @@ snapshots: acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true tinybench@2.9.0: {} From e8a3f925fb35dca52dcb61c26853deae7122fdbf Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 19:45:43 -0400 Subject: [PATCH 08/16] build in order --- .github/workflows/e2e.yaml | 2 +- .github/workflows/prerelease.yaml | 2 +- .github/workflows/release.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index b6e0e0e..93641b3 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -19,7 +19,7 @@ jobs: node-version: "23.x" registry-url: "https://registry.npmjs.org" - run: pnpm install --frozen-lockfile - - run: pnpm -r build + - run: pnpm recursive run --sort --workspace-concurrency=1 build - name: Install Playwright run: pnpm exec playwright install diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index dc6e39d..d38ee16 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -22,7 +22,7 @@ jobs: node-version: "23.x" registry-url: "https://registry.npmjs.org" - run: pnpm install --frozen-lockfile - - run: pnpm -r build + - run: pnpm recursive run --sort --workspace-concurrency=1 build - name: Write dev versions env: PR_SHA: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ff5a25c..06be7cb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: node-version: "23.x" registry-url: "https://registry.npmjs.org" - run: pnpm install --frozen-lockfile - - run: pnpm -r build + - run: pnpm recursive run --sort --workspace-concurrency=1 build - name: Publish core run: pnpm publish From c7a9fc01ff78171e0a18a9a39fc7c09384373144 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 19:57:24 -0400 Subject: [PATCH 09/16] add actors to prerelease --- .github/workflows/prerelease.yaml | 6 ++++++ .github/workflows/release.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index d38ee16..41cb5ed 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -34,6 +34,12 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish actors + run: pnpm publish --no-git-checks --tag dev + working-directory: packages/actors + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish vite run: pnpm publish --no-git-checks --tag dev working-directory: packages/vite diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 06be7cb..2326356 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,6 +26,12 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish actors + run: pnpm publish + working-directory: packages/actors + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish vite run: pnpm publish working-directory: packages/vite From 434fbafe10b840140e54921c8658fd24f9f4a4bb Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 21:22:23 -0400 Subject: [PATCH 10/16] force access to be public in prerelease --- .github/workflows/prerelease.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index 41cb5ed..13c77ad 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -29,31 +29,31 @@ jobs: run: node scripts/version-for-prerelease.ts - name: Publish core - run: pnpm publish --no-git-checks --tag dev + run: pnpm publish --no-git-checks --tag dev --access public working-directory: packages/core env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish actors - run: pnpm publish --no-git-checks --tag dev + run: pnpm publish --no-git-checks --tag dev --access public working-directory: packages/actors env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish vite - run: pnpm publish --no-git-checks --tag dev + run: pnpm publish --no-git-checks --tag dev --access public working-directory: packages/vite env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish create-orange - run: pnpm publish --no-git-checks --tag dev + run: pnpm publish --no-git-checks --tag dev --access public working-directory: packages/create-orange env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish cli - run: pnpm publish --no-git-checks --tag dev + run: pnpm publish --no-git-checks --tag dev --access public working-directory: packages/cli env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 26c6e589810c2d46bfac873f60b0beb978d0d25c Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 23 Jul 2025 21:24:27 -0400 Subject: [PATCH 11/16] Update prerelease package manager comment --- .github/workflows/prerelease.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index 13c77ad..14847e0 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -69,10 +69,10 @@ jobs: Install the prerelease packages with: \`\`\`bash - npm install @orange-js/orange@0.0.0-${sha} && npm install -D @orange-js/vite@0.0.0-${sha} && npm install -D @orange-js/cli@0.0.0-${sha} - pnpm add @orange-js/orange@0.0.0-${sha} && pnpm add -D @orange-js/vite@0.0.0-${sha} && pnpm add -D @orange-js/cli@0.0.0-${sha} - yarn add @orange-js/orange@0.0.0-${sha} && yarn add -D @orange-js/vite@0.0.0-${sha} && yarn add -D @orange-js/cli@0.0.0-${sha} - bun add @orange-js/orange@0.0.0-${sha} && bun add -D @orange-js/vite@0.0.0-${sha} && bun add -D @orange-js/cli@0.0.0-${sha} + npm install @orange-js/orange@0.0.0-${sha} @orange-js/actors@0.0.0-${sha} && npm install -D @orange-js/vite@0.0.0-${sha} @orange-js/cli@0.0.0-${sha} + pnpm add @orange-js/orange@0.0.0-${sha} @orange-js/actors@0.0.0-${sha} && pnpm add -D @orange-js/vite@0.0.0-${sha} @orange-js/cli@0.0.0-${sha} + yarn add @orange-js/orange@0.0.0-${sha} @orange-js/actors@0.0.0-${sha} && yarn add -D @orange-js/vite@0.0.0-${sha} @orange-js/cli@0.0.0-${sha} + bun add @orange-js/orange@0.0.0-${sha} @orange-js/actors@0.0.0-${sha} && bun add -D @orange-js/vite@0.0.0-${sha} @orange-js/cli@0.0.0-${sha} \`\`\``; github.rest.issues.createComment({ From 1de67adba979509f5ee94c3bed4eb532d06f01de Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Thu, 24 Jul 2025 08:48:01 -0400 Subject: [PATCH 12/16] remove observed prop requirement --- e2e/actors.spec.ts | 2 +- packages/actors/src/index.tsx | 29 +++++++++++++++++++---------- packages/actors/src/observed.tsx | 5 +++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/e2e/actors.spec.ts b/e2e/actors.spec.ts index 782073c..62bdc40 100644 --- a/e2e/actors.spec.ts +++ b/e2e/actors.spec.ts @@ -175,7 +175,7 @@ test.multi( export default function Home() { return (
- +
); diff --git a/packages/actors/src/index.tsx b/packages/actors/src/index.tsx index d2c48b6..18c9536 100644 --- a/packages/actors/src/index.tsx +++ b/packages/actors/src/index.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { ActorState, Actor as CfActor, getActor } from "@cloudflare/actors"; import { isValidElement } from "react"; import { ClientComponent } from "./cl.js"; +import { observedSymbol } from "./observed.js"; export * from "@cloudflare/actors"; @@ -23,12 +24,14 @@ export abstract class Actor extends CfActor { async __rscStream( name: string, props: Record - ): Promise { + ): Promise<[ReadableStream, boolean]> { const Component = (this[name as keyof this] as any).bind(this); const rscStream = rsc().renderToReadableStream({ root: , }); - return rscStream; + // @ts-ignore + const observed = this[observedSymbol] ?? false; + return [rscStream, observed]; } static Component = Component; @@ -41,7 +44,7 @@ export abstract class Actor extends CfActor { } } -async function InternalComponent, Env>( +async function internalComponent, Env>( props: { actor: ActorConstructor; name?: string; @@ -73,9 +76,14 @@ async function InternalComponent, Env>( await new Promise((resolve) => setTimeout(resolve, 25)); const rscStream = await stub.__rscStream("Component", rest); - const payload = await rsc().createFromReadableStream(rscStream); - - return payload.root; + const payload = await rsc().createFromReadableStream( + rscStream[0] as ReadableStream + ); + + return { + root: payload.root, + isObserved: rscStream[1], + }; } type PropsFromDurableObject< @@ -97,19 +105,20 @@ async function Component, Env>( props: { actor: ActorConstructor; name?: string; - observed?: boolean; } & PropsFromDurableObject ) { - if (props.observed) { + const { root, isObserved } = await internalComponent(props); + + if (isObserved) { return ( - + {root} ); } - return await InternalComponent(props); + return root; } diff --git a/packages/actors/src/observed.tsx b/packages/actors/src/observed.tsx index 43aa0fc..ca9a339 100644 --- a/packages/actors/src/observed.tsx +++ b/packages/actors/src/observed.tsx @@ -13,6 +13,8 @@ type ClassMethodDecorator = ( context: ClassMethodDecoratorContext ) => any; +export const observedSymbol = Symbol("orange:observed"); + export function Observed( ...names: string[] ): ClassMethodDecorator> { @@ -20,6 +22,9 @@ export function Observed( context.addInitializer(function () { const self = this as Actor; + // @ts-ignore + self[observedSymbol] = true; + self["onPersist"] = async () => { const ret = await value.apply(this); const stream = rsc().renderToReadableStream({ From 6127e60a2492f35b0670603dcec4c0abd8ca1791 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Thu, 24 Jul 2025 21:10:00 -0400 Subject: [PATCH 13/16] refactor: make actor non-abstract --- packages/actors/src/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/actors/src/index.tsx b/packages/actors/src/index.tsx index 18c9536..7159487 100644 --- a/packages/actors/src/index.tsx +++ b/packages/actors/src/index.tsx @@ -16,10 +16,12 @@ function rsc() { type RSCPayload = { root: React.ReactNode }; -export abstract class Actor extends CfActor { - abstract Component( +export class Actor extends CfActor { + Component( props: Record - ): React.ReactNode | Promise; + ): React.ReactNode | Promise { + return null; + } async __rscStream( name: string, From 30d49d0cf378041b95c40f5fb44dbf510f88f55e Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Thu, 24 Jul 2025 21:16:40 -0400 Subject: [PATCH 14/16] set actor identifier before render --- packages/actors/src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/actors/src/index.tsx b/packages/actors/src/index.tsx index 7159487..315536b 100644 --- a/packages/actors/src/index.tsx +++ b/packages/actors/src/index.tsx @@ -75,7 +75,7 @@ async function internalComponent, Env>( const { actor, ...rest } = props; // TODO: Remove this hack once the data-race in actors is fixed - await new Promise((resolve) => setTimeout(resolve, 25)); + await stub.setIdentifier(props.name ?? "default"); const rscStream = await stub.__rscStream("Component", rest); const payload = await rsc().createFromReadableStream( From e5cc7d2561e84280f7d75612483898a8fd92a075 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Fri, 25 Jul 2025 18:45:51 -0400 Subject: [PATCH 15/16] feat: reload when routes are updated --- e2e/fixture/index.ts | 38 +++++++++++++++-- e2e/hmr.spec.ts | 20 +++++++++ packages/vite/src/config.ts | 7 +++- packages/vite/src/index.ts | 4 ++ packages/vite/src/plugins/route-reload.ts | 51 +++++++++++++++++++++++ packages/vite/src/plugins/routes.ts | 16 ++++--- 6 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 e2e/hmr.spec.ts create mode 100644 packages/vite/src/plugins/route-reload.ts diff --git a/e2e/fixture/index.ts b/e2e/fixture/index.ts index d0614a4..5512fae 100644 --- a/e2e/fixture/index.ts +++ b/e2e/fixture/index.ts @@ -125,9 +125,16 @@ export async function runCmd({ }; } -export type CreateServer = (files?: Files) => Promise<{ port: number }>; - -const orangeTest = base.extend<{ dev: CreateServer; worker: CreateServer }>({ +export type CreateServer = ( + files?: Files +) => Promise<{ port: number } & T>; + +const orangeTest = base.extend<{ + dev: CreateServer<{ + addFile: (filePath: string, contents: string) => Promise; + }>; + worker: CreateServer; +}>({ dev: async ({}, use, testInfo) => { const tasks = new DisposeScope(); @@ -148,7 +155,12 @@ const orangeTest = base.extend<{ dev: CreateServer; worker: CreateServer }>({ testInfo.attach("Vite dev server", { body: devServer.output() }) ); - return { port }; + return { + port, + addFile: async (filePath: string, contents: string) => { + fs.writeFile(path.join(fixtureDir, filePath), dedent(contents)); + }, + }; }); // TODO: prettier doesnt support using @@ -203,9 +215,27 @@ const orangeTest = base.extend<{ dev: CreateServer; worker: CreateServer }>({ export const test = orangeTest as typeof orangeTest & { multi: typeof multitest; + dev: typeof devtest; }; test.multi = multitest; +test.dev = devtest; + +function devtest( + title: string, + fn: (opts: { + page: Page; + port: number; + addFile: (filePath: string, contents: string) => Promise; + browser: Browser; + }) => Promise, + files: Files = {} +) { + test(`${title} dev`, async ({ page, dev, browser }) => { + const { port, addFile } = await dev(files); + await fn({ page, port, addFile, browser }); + }); +} function multitest( title: string, diff --git a/e2e/hmr.spec.ts b/e2e/hmr.spec.ts new file mode 100644 index 0000000..69ff594 --- /dev/null +++ b/e2e/hmr.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "./fixture/index"; + +test.dev("route reload", async ({ page, port, addFile }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Hello World")).toBeVisible(); + + await addFile( + "app/routes/bar.tsx", + ` + export default function Bar() { + return
Bar
; + } + ` + ); + + await page.waitForTimeout(2000); + + await page.goto(`http://localhost:${port}/bar`, { timeout: 10000 }); + await expect(page.getByText("Bar")).toBeVisible(); +}); diff --git a/packages/vite/src/config.ts b/packages/vite/src/config.ts index 443a9f5..0c08be8 100644 --- a/packages/vite/src/config.ts +++ b/packages/vite/src/config.ts @@ -14,7 +14,7 @@ export type ResolvedConfig = Required; let _configPromise: Promise | undefined; -const defaultConfig: ResolvedConfig = { +let defaultConfig: ResolvedConfig = { routes: fsRoutes(), }; @@ -40,3 +40,8 @@ export async function resolveConfig(): Promise { } return _configPromise; } + +export function resetConfig() { + defaultConfig = { routes: fsRoutes() }; + _configPromise = undefined; +} diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index b352efe..dadfaaa 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -11,6 +11,7 @@ import { routesPlugin } from "./plugins/routes.js"; import { isolation } from "./plugins/isolation.js"; import { Config, resolveConfig } from "./config.js"; import { preserveClassNames } from "./plugins/preserve-class-names.js"; +import { routeReload } from "./plugins/route-reload.js"; export * from "./routing/fs-routes.js"; @@ -74,6 +75,9 @@ export default function orange( } ), routesPlugin(config), + routeReload((newConfig) => { + _config = newConfig; + }), ]; } diff --git a/packages/vite/src/plugins/route-reload.ts b/packages/vite/src/plugins/route-reload.ts new file mode 100644 index 0000000..2d2e66f --- /dev/null +++ b/packages/vite/src/plugins/route-reload.ts @@ -0,0 +1,51 @@ +import { Plugin, ViteDevServer } from "vite"; +import * as path from "node:path"; +import { resetRoutes } from "./routes.js"; +import { resetConfig, resolveConfig, ResolvedConfig } from "../config.js"; + +// Prevent double reload for file renames +let reloadCount = 0; + +async function forceReload( + server: ViteDevServer, + reloadId: number, + updateConfig: (newConfig: ResolvedConfig) => void +) { + if (reloadId !== reloadCount) { + return; + } + + resetRoutes(); + resetConfig(); + updateConfig(await resolveConfig()); + + // TODO: This is a hack to force a full reload + await server.restart(); + server.ws.send({ type: "full-reload" }); +} + +const pathsToWatch = ["orange.config.ts", "orange.config.js", "app/routes"].map( + (file) => path.resolve(file) +); + +export function routeReload( + updateConfig: (newConfig: ResolvedConfig) => void +): Plugin { + return { + name: "orange:reload-routes", + async configureServer(server) { + const onFileChange = async (filePath: string) => { + if ( + pathsToWatch.includes(filePath) || + pathsToWatch.some((p) => filePath.startsWith(p)) + ) { + const reloadId = ++reloadCount; + setTimeout(() => forceReload(server, reloadId, updateConfig), 100); + } + }; + + server.watcher.on("add", onFileChange); + server.watcher.on("unlink", onFileChange); + }, + }; +} diff --git a/packages/vite/src/plugins/routes.ts b/packages/vite/src/plugins/routes.ts index 857e51c..762efa4 100644 --- a/packages/vite/src/plugins/routes.ts +++ b/packages/vite/src/plugins/routes.ts @@ -9,6 +9,10 @@ const vmod = new VirtualModule("routes"); let _routes: Route[] | undefined; +export function resetRoutes() { + _routes = undefined; +} + export function routesPlugin(config: () => Config): Plugin { return { name: "orange:routes", @@ -22,19 +26,19 @@ export function routesPlugin(config: () => Config): Plugin { } }, async load(id) { - const routes = _routes ?? config().routes ?? fsRoutes(); - _routes = routes; - if (id === vmod.id) { + const routes = _routes ?? config().routes ?? fsRoutes(); + _routes = routes; + const ids = Object.fromEntries( routes.map((route) => [ route.pattern, `route_${Math.random().toString(36).substring(2, 15)}`, - ]), + ]) ); const imports = routes.map((route) => { return `import * as ${ids[route.pattern]} from "${path.resolve( - route.file, + route.file )}";`; }); @@ -42,7 +46,7 @@ export function routesPlugin(config: () => Config): Plugin { (route) => `{ pattern: new URLPattern({ pathname: "${ route.pattern - }" }), module: ${ids[route.pattern]} }`, + }" }), module: ${ids[route.pattern]} }` ); return { From 26e8bf209d9e6df53254239d948915f1714f22a0 Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Sat, 26 Jul 2025 09:54:11 -0400 Subject: [PATCH 16/16] feat: default error boundary --- e2e/basic.spec.ts | 15 ++++ e2e/error-handling.spec.ts | 48 ++++++++++++ e2e/fixture/index.ts | 11 ++- packages/core/src/client.tsx | 5 +- packages/core/src/error-handling/browser.tsx | 78 ++++++++++++++++++++ packages/core/src/server.tsx | 45 +++++++++-- packages/core/src/ssr.tsx | 41 ++++++---- packages/vite/src/routing/fs-routes.ts | 7 +- 8 files changed, 225 insertions(+), 25 deletions(-) create mode 100644 e2e/error-handling.spec.ts create mode 100644 packages/core/src/error-handling/browser.tsx diff --git a/e2e/basic.spec.ts b/e2e/basic.spec.ts index 867d63b..24ac901 100644 --- a/e2e/basic.spec.ts +++ b/e2e/basic.spec.ts @@ -4,3 +4,18 @@ test.multi("hello world", async ({ page, port }) => { await page.goto(`http://localhost:${port}`); await expect(page.getByText("Hello World")).toBeVisible(); }); + +test.multi( + ".browser files arent treated as routes", + async ({ page, port }) => { + await page.goto(`http://localhost:${port}/index.browser`); + await expect(page.getByText("Not found")).toBeVisible(); + }, + { + "app/routes/index.browser.tsx": ` + export default function Index() { + return
Hello World
; + } + `, + } +); diff --git a/e2e/error-handling.spec.ts b/e2e/error-handling.spec.ts new file mode 100644 index 0000000..f605e32 --- /dev/null +++ b/e2e/error-handling.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from "./fixture/index"; + +test.multi( + "immediate error handling with default error handler", + async ({ page, port, isDev }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Something went wrong")).toBeVisible(); + if (isDev) { + await expect(page.getByText("Hello World")).toBeVisible(); + } + }, + { + "app/routes/index.tsx": ` + export default function Index() { + throw new Error("Hello World"); + } + `, + } +); + +test.multi( + "delayed error handling with default error handler", + async ({ page, port, isDev }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Something went wrong")).toBeVisible(); + if (isDev) { + await expect(page.getByText("Hello World")).toBeVisible(); + } + }, + { + "app/routes/index.tsx": ` + import { Suspense } from "react"; + + async function Err() { + await new Promise((resolve) => setTimeout(resolve, 1000)); + throw new Error("Hello World"); + } + + export default function Index() { + return ( + Loading...
}> + + + ); + } + `, + } +); diff --git a/e2e/fixture/index.ts b/e2e/fixture/index.ts index 5512fae..b271948 100644 --- a/e2e/fixture/index.ts +++ b/e2e/fixture/index.ts @@ -239,17 +239,22 @@ function devtest( function multitest( title: string, - fn: (opts: { page: Page; port: number; browser: Browser }) => Promise, + fn: (opts: { + page: Page; + port: number; + browser: Browser; + isDev: boolean; + }) => Promise, files: Files = {} ) { test(`${title} dev`, async ({ page, dev, browser }) => { const { port } = await dev(files); - await fn({ page, port, browser }); + await fn({ page, port, browser, isDev: true }); }); test(`${title} worker`, async ({ page, worker, browser }) => { const { port } = await worker(files); - await fn({ page, port, browser }); + await fn({ page, port, browser, isDev: false }); }); } diff --git a/packages/core/src/client.tsx b/packages/core/src/client.tsx index be14aee..8ffec3b 100644 --- a/packages/core/src/client.tsx +++ b/packages/core/src/client.tsx @@ -6,6 +6,7 @@ import * as ReactClient from "@vitejs/plugin-rsc/browser"; import { getRscStreamFromHtml } from "@vitejs/plugin-rsc/rsc-html-stream/browser"; import * as ReactDOMClient from "react-dom/client"; import type { RscPayload } from "./server.js"; +import { ErrorBoundary, ErrorFallback } from "./error-handling/browser.js"; // @ts-expect-error globalThis.rsc = ReactClient; @@ -67,7 +68,9 @@ export async function main() { // hydration const browserRoot = ( - + }> + + ); ReactDOMClient.hydrateRoot(document, browserRoot, { diff --git a/packages/core/src/error-handling/browser.tsx b/packages/core/src/error-handling/browser.tsx new file mode 100644 index 0000000..0eee285 --- /dev/null +++ b/packages/core/src/error-handling/browser.tsx @@ -0,0 +1,78 @@ +"use client"; + +import React from "react"; + +export class ErrorBoundary extends React.Component<{ + children: React.ReactNode; + fallback: (props: { error: Error | null }) => React.ReactNode; +}> { + state = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error) { + return { + hasError: true, + error: process.env.NODE_ENV === "development" ? error : null, + }; + } + + render() { + if (this.state.hasError) { + const Fallback = this.props.fallback; + return ; + } + + return this.props.children; + } +} + +export function ErrorFallback({ error }: { error: Error | null }) { + let content; + if (process.env.NODE_ENV === "development") { + if (error?.stack) { + content = error.stack.toString(); + } else if (error) { + content = error.toString(); + } else { + content = ""; + } + } + + return ( +
+ +

Something went wrong

+ {content && ( +
+          {content}
+        
+ )} +
+ ); +} diff --git a/packages/core/src/server.tsx b/packages/core/src/server.tsx index 7915d1f..96307c3 100644 --- a/packages/core/src/server.tsx +++ b/packages/core/src/server.tsx @@ -48,7 +48,8 @@ export async function request() { async function handler( request: Request, - Layout: Layout + Layout: Layout, + onError: (error: unknown, errorInfo: ErrorInfo) => void ): Promise { const isAction = request.method === "POST"; let returnValue: unknown | undefined; @@ -113,6 +114,7 @@ async function handler( request, returnValue, formState, + onError, }); }); } @@ -122,6 +124,7 @@ type RscResponseOptions = { request: Request; returnValue?: unknown; formState?: ReactFormState; + onError: (error: unknown, errorInfo: ErrorInfo) => void; }; async function rscResponse({ @@ -129,6 +132,7 @@ async function rscResponse({ request, returnValue, formState, + onError, }: RscResponseOptions) { const rscStream = ReactServer.renderToReadableStream( { @@ -138,9 +142,7 @@ async function rscResponse({ formState, }, { - onError(error: unknown, errorInfo: ErrorInfo) { - console.error("Error during RSC streaming", error, errorInfo); - }, + onError, } ); @@ -169,6 +171,7 @@ async function rscResponse({ debugNojs: url.searchParams.has("__nojs"), onError(error: unknown, errorInfo: ErrorInfo) { console.error("Error during RSC serialization", error, errorInfo); + onError(error, errorInfo); }, }); @@ -189,6 +192,9 @@ const wsPattern = new URLPattern({ export function app(Layout: Layout, options?: AppOptions) { return { async fetch(request: Request) { + let reactError: unknown | undefined; + let reactErrorInfo: ErrorInfo | undefined; + try { if (request.headers.get("Upgrade") === "websocket") { const match = wsPattern.exec(request.url); @@ -206,11 +212,36 @@ export function app(Layout: Layout, options?: AppOptions) { } return ( - (await handler(request, Layout)) ?? - new Response("Not found", { status: 404 }) + (await handler(request, Layout, (err, errorInfo) => { + reactError = err; + reactErrorInfo = errorInfo; + })) ?? new Response("Not found", { status: 404 }) ); } catch (error) { - return new Response("Error", { status: 500 }); + const err = reactError ?? error; + + const { renderErrorBoundaryResponse } = + await import.meta.viteRsc.loadModule( + "ssr", + "index" + ); + + const stream = await renderErrorBoundaryResponse( + err instanceof Error + ? { + message: err.message, + stack: err.stack, + } + : { + message: String(err), + } + ); + return new Response(stream, { + headers: { + "Content-type": "text/html", + vary: "accept", + }, + }); } }, }; diff --git a/packages/core/src/ssr.tsx b/packages/core/src/ssr.tsx index a6a9444..33c45c4 100644 --- a/packages/core/src/ssr.tsx +++ b/packages/core/src/ssr.tsx @@ -7,19 +7,11 @@ import type { ReactFormState } from "react-dom/client"; import * as ReactClient from "@vitejs/plugin-rsc/ssr"; import * as ReactDOMServer from "react-dom/server.edge"; import type { RscPayload } from "./server.js"; +import { ErrorFallback } from "./error-handling/browser.js"; -export type RenderHTML = typeof renderHTML; - -export class SSRError extends Error { - errorInfo: ErrorInfo; +export * from "./error-handling/browser.js"; - constructor(message: string, errorInfo: ErrorInfo, cause?: unknown) { - super(message); - this.name = "SSRError"; - this.errorInfo = errorInfo; - this.cause = cause; - } -} +export type RenderHTML = typeof renderHTML; export async function renderHTML( rscStream: ReadableStream, @@ -28,7 +20,7 @@ export async function renderHTML( nonce?: string; debugNojs?: boolean; onError?: (error: unknown, errorInfo: ErrorInfo) => void; - }, + } ) { // duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) @@ -64,9 +56,32 @@ export async function renderHTML( responseStream = responseStream.pipeThrough( injectRscStreamToHtml(rscStream2, { nonce: options?.nonce, - }), + }) ); } return responseStream; } + +export async function renderErrorBoundaryResponse(opts: { + message: string; + stack?: string; +}) { + let error: Error | null = null; + + if (process.env.NODE_ENV === "development") { + if (opts.stack) { + error = new Error(opts.message, { + // @ts-ignore + stack: opts.stack, + }); + } else { + error = new Error(opts.message); + } + } + + return await ReactDOMServer.renderToReadableStream( + , + {} + ); +} diff --git a/packages/vite/src/routing/fs-routes.ts b/packages/vite/src/routing/fs-routes.ts index bdf1292..94972b9 100644 --- a/packages/vite/src/routing/fs-routes.ts +++ b/packages/vite/src/routing/fs-routes.ts @@ -6,11 +6,12 @@ import type { Route } from "./index.js"; export function fsRoutes(): Route[] { const routesDir = path.resolve(process.cwd(), "app", "routes"); const routes = walkDir(routesDir).map((route) => - route.replace(`${routesDir}/`, ""), + route.replace(`${routesDir}/`, "") ); return routes .filter((route) => isEcmaLike(route)) + .filter((route) => !isBrowserFile(route)) .map((route) => { const pattern = fileNameToPattern(route); @@ -54,3 +55,7 @@ function walkDir(dir: string) { return files; } + +function isBrowserFile(fileName: string) { + return /\.browser\.(tsx?|jsx?)$/.test(fileName); +}