diff --git a/.changeset/add-marko-store.md b/.changeset/add-marko-store.md new file mode 100644 index 00000000..33cf4b83 --- /dev/null +++ b/.changeset/add-marko-store.md @@ -0,0 +1,5 @@ +--- +"@tanstack/marko-store": minor +--- + +Add `@tanstack/marko-store`, a Marko 6 adapter for TanStack Store. It ships five tags — `` (the `useSelector`/`useStore` equivalent, with `selector` and `compare` support), ``, `` / `` (per-request store provision for SSR without cross-request leakage), and `` (stores born from late-awaited server data that render server-side and stay live after resume) — and re-exports the full `@tanstack/store` core. Works client-only and under SSR with Marko's resume, including out-of-order streaming. diff --git a/.gitignore b/.gitignore index 182bead0..eecb8443 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ examples/angular/store-context/.angular/cache/21.2.7/store-context/angular-compi examples/angular/stores/.angular/cache/21.2.7/stores/.tsbuildinfo examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db-lock +test-results/ +*.tsbuildinfo +packages/marko-store/e2e/pw-results.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index c4d3dad5..c324b915 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,6 @@ "semi": false, "singleQuote": true, "trailingComma": "all", - "plugins": ["prettier-plugin-svelte"], + "plugins": ["prettier-plugin-svelte", "prettier-plugin-marko"], "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/docs/config.json b/docs/config.json index 3a34004f..4297029e 100644 --- a/docs/config.json +++ b/docs/config.json @@ -85,6 +85,15 @@ "to": "framework/lit/quick-start" } ] + }, + { + "label": "marko", + "children": [ + { + "label": "Quick Start - Marko", + "to": "framework/marko/quick-start" + } + ] } ] }, @@ -159,6 +168,12 @@ "to": "framework/lit/reference/index" } ] + }, + { + "label": "marko", + "children": [ + { "label": "Marko Tags", "to": "framework/marko/reference/index" } + ] } ] }, @@ -507,6 +522,16 @@ "to": "framework/lit/reference/interfaces/UseSelectorOptions" } ] + }, + { + "label": "marko", + "children": [ + { "label": "", "to": "framework/marko/reference/tags/store-provider" }, + { "label": "", "to": "framework/marko/reference/tags/store-context" }, + { "label": "", "to": "framework/marko/reference/tags/store-selector" }, + { "label": "", "to": "framework/marko/reference/tags/store-atom" }, + { "label": "", "to": "framework/marko/reference/tags/stream-store-provider" } + ] } ] }, @@ -672,6 +697,16 @@ "to": "framework/lit/examples/simple" } ] + }, + { + "label": "marko", + "children": [ + { "label": "Simple", "to": "framework/marko/examples/simple" }, + { "label": "Atoms", "to": "framework/marko/examples/atoms" }, + { "label": "Stores", "to": "framework/marko/examples/stores" }, + { "label": "Store Actions", "to": "framework/marko/examples/store-actions" }, + { "label": "Store Context", "to": "framework/marko/examples/store-context" } + ] } ] } diff --git a/docs/framework/marko/quick-start.md b/docs/framework/marko/quick-start.md new file mode 100644 index 00000000..6a893b61 --- /dev/null +++ b/docs/framework/marko/quick-start.md @@ -0,0 +1,88 @@ +--- +title: Quick Start +id: quick-start +--- + +The basic Marko app example to get started with TanStack `marko-store`. + +Marko exposes the store through tags rather than hooks. `` reads a slice of a store and keeps it live, re-rendering only when that selected slice changes. State is updated with `store.setState`, exactly as in the other adapters. + +```marko +import { createStore } from '@tanstack/marko-store' + +// You can instantiate a Store outside of Marko templates too! +export const store = createStore({ + dogs: 0, + cats: 0, +}) + +
+

How many of your friends like cats or dogs?

+

+ Press one of the buttons to add a counter of how many of your friends + like cats or dogs. +

+ + + // subscribes only to state.dogs + store selector=(state) => state.dogs/> +
dogs: ${dogs}
+ + + store selector=(state) => state.cats/> +
cats: ${cats}
+
+``` + +`` binds the latest selected value to its tag variable (here `dogs` and `cats`) and only updates when that specific selection changes. + +## A writable value: `` + +For a single writable value, `` gives two-way access to an atom — read its tag variable, and assign to it to write back: + +```marko +import { createAtom } from '@tanstack/marko-store' +export const countAtom = createAtom(0) + + countAtom/> + +``` + +## Sharing stores without imports: `` and `` + +To distribute one or more stores down the tree without importing them everywhere, wrap a subtree in `` with a bundle of stores, read any value below with `` in context mode, and grab the bundle to write with ``: + +```marko +import { createStore } from '@tanstack/marko-store' + + ({ cats: createStore({ count: 0 }) })> + c.cats selector=(s) => s.count/> +
cats: ${catCount}
+ + + +
+``` + +## Server rendering and streaming + +`marko-store` is resumable: a store rendered on the server stays live on the client after resume, with no re-execution. For server-computed, per-request data that arrives mid-render through ``, create the store from the awaited value inside the await using ``. The value then renders on the server with no first-paint flash and the store stays live after resume: + +```marko +import { createStore } from '@tanstack/marko-store' + + + ({ user: createStore(user) })> + c.user selector=(s) => s.name/> +
${name}
+
+
+``` + +Use a plain `` for in-order streaming, or wrap it in `` with a `<@placeholder>` for out-of-order streaming. Each `` needs a distinct `key`. + +For production SSR, use [`@marko/run`](https://github.com/marko-js/run), Marko's official server framework. Standalone `@marko/vite` SSR is intended for development only. diff --git a/docs/framework/marko/reference/index.md b/docs/framework/marko/reference/index.md new file mode 100644 index 00000000..79839fb5 --- /dev/null +++ b/docs/framework/marko/reference/index.md @@ -0,0 +1,20 @@ +--- +id: "@tanstack/marko-store" +title: "@tanstack/marko-store" +--- + +# @tanstack/marko-store + +The Marko adapter exposes its API as custom tags. Each tag is documented on its own page below. + +## Tags + +- [``](tags/store-provider.md) +- [``](tags/store-context.md) +- [``](tags/store-selector.md) +- [``](tags/store-atom.md) +- [``](tags/stream-store-provider.md) + +## Re-exports + +`@tanstack/marko-store` re-exports the full `@tanstack/store` core, including `createStore`, `createAtom`, `Store`, `batch`, and `shallow`. Derived state comes from the core too — pass a function to `createStore` or `createAtom` to get a computed, read-only store. These are framework-agnostic; see the Store API Reference for their documentation. diff --git a/docs/framework/marko/reference/tags/store-atom.md b/docs/framework/marko/reference/tags/store-atom.md new file mode 100644 index 00000000..226e7818 --- /dev/null +++ b/docs/framework/marko/reference/tags/store-atom.md @@ -0,0 +1,29 @@ +--- +id: store-atom +title: "" +--- + +# Tag: `` + +Binds a single writable atom from `createAtom()` as a two-way value: it reads the atom's current value and writes back through the atom's `set`. Use it for a standalone writable value; for a `Store` (from `createStore`), read it with `` instead. + +```marko + qtyAtom> +

Quantity: ${qty}

+``` + +## Type parameters + +- **`T`** — the atom's value type. + +## Attributes + +- **`from`** — `() => { get(); set(); subscribe() } | null` (required). A thunk returning the atom (from `createAtom()`), or `null` while nothing is available yet — the null-tolerant form matters for getter-fed atoms (`from=(() => ctx()?.countAtom ?? null)`), where the context bundle isn't parked for a beat during resume or a browser-only first paint. A non-null value without a `set` method (such as a `Store`) still throws with guidance to use ``. + +## Tag variable + +The atom's current value, typed `T`. The tag exposes a `value`/`valueChange` pair, so it is two-way bindable — bind it to a variable with `value:=` and assignments write back through the atom's `set`. + +## Behavior + +The value is seeded from `from().get()` at render and kept live by a subscription on mount, so it updates whenever the atom changes from anywhere. Writes go through `from().set(next)`. If `from()` resolves nothing yet (a getter-fed atom before its provider parks the bundle), the tag renders `undefined` for that beat and listens on the internal publish bus, attaching the moment the bundle lands — the same recovery `` performs. If `from()` returns a non-null value without a `set` method (such as a `Store`), the tag throws with guidance to use ``. diff --git a/docs/framework/marko/reference/tags/store-context.md b/docs/framework/marko/reference/tags/store-context.md new file mode 100644 index 00000000..3caa527c --- /dev/null +++ b/docs/framework/marko/reference/tags/store-context.md @@ -0,0 +1,28 @@ +--- +id: store-context +title: "" +--- + +# Tag: `` + +Reads the store bundle provided by the nearest `` with a matching key, so a descendant can use the live stores directly (for example to dispatch actions). Binds a getter. + +```marko + + + +``` + +## Attributes + +- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key to read. Must match the key the `` used. + +## Tag variable + +A getter function, `() => bundle`. Call it to read the current bundle parked by the provider. It is a thunk rather than the bundle itself so the read happens at call time, after the provider has parked the bundle. + +## Behavior + +At render the tag checks that a bundle exists on `$global` under the key and throws if none is found — that is, when there is no `` above it. It does not subscribe to anything; use `` when you need a value that re-renders on change. diff --git a/docs/framework/marko/reference/tags/store-provider.md b/docs/framework/marko/reference/tags/store-provider.md new file mode 100644 index 00000000..d2674e82 --- /dev/null +++ b/docs/framework/marko/reference/tags/store-provider.md @@ -0,0 +1,28 @@ +--- +id: store-provider +title: "" +--- + +# Tag: `` + +Creates a bundle of stores for the current request and makes it available to descendant tags by a context key. Wraps the content it is given. + +```marko + ({ cart: createStore({ items: [] }) })> + + +``` + +## Attributes + +- **`value`** — `() => Record` (required). A thunk returning the bundle of named stores or atoms to provide. It is called once on the server at render and again on the client at mount, so each environment gets its own live instances. +- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key the bundle is parked under. Use distinct keys to provide more than one bundle on a page. The matching `` and context-mode `` must use the same key. +- **`content`** — `Marko.Body` (optional). The body rendered inside the provider, usually supplied implicitly as the tag's children. + +## Tag variable + +None. `` is a wrapper: it renders its content and binds no tag variable. + +## Behavior + +The bundle is parked on `$global` under the context key at render, and re-created and published on mount so client subscribers attach to live instances. Only one provider may claim a given key: a second `` (or a ``) on the same key throws, to prevent two writers silently clobbering the same box. On destroy the box and its owner marker are cleared. diff --git a/docs/framework/marko/reference/tags/store-selector.md b/docs/framework/marko/reference/tags/store-selector.md new file mode 100644 index 00000000..b0434150 --- /dev/null +++ b/docs/framework/marko/reference/tags/store-selector.md @@ -0,0 +1,38 @@ +--- +id: store-selector +title: "" +--- + +# Tag: `` + +Subscribes to a store or atom, projects a slice of it, and re-renders when that slice changes. Works in two modes: `from` (a store passed directly) or `context` (a member of the provided bundle). Binds the selected value. + +```marko + + cartStore selector=(s) => s.items.length> +

${count} items

+ + + b.cart selector=(s) => s.items.length> +``` + +## Type parameters + +- **`TSource`** — the snapshot type of the source store or atom. +- **`TSelected`** — the projected slice type. Defaults to `TSource` when no `selector` is given. + +## Attributes + +- **`from`** — `() => { get(); subscribe() }` (optional). A thunk returning the source store or atom directly. Mutually exclusive with `context`. +- **`context`** — `(bundle) => { get(); subscribe() }` (optional). Selects the source from the provided bundle. Mutually exclusive with `from`. Annotate the parameter in typed projects, e.g. `context=(b: any) => b.cart`. +- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key to read in `context` mode. Must match the provider's key. +- **`selector`** — `(snapshot: TSource) => TSelected` (optional, default identity). Projects the slice to track. +- **`compare`** — `(a: TSelected, b: TSelected) => boolean` (optional, default `===`). Equality check used to drop redundant updates. Selectors returning an **object or array** build a fresh reference on every store change, so the default `===` never dedupes them — pass `shallow` (re-exported from the core) to compare contents instead of identity — on server-rendered pages through an inline arrow, `compare=((a, b) => shallow(a, b))`, since a bare imported function can't cross Marko's serialization boundary on resume (inline functions are compiler-registered; plain-JS imports are not). Single-field/primitive slices are fine with the default. + +## Tag variable + +The current selected value, typed `TSelected | undefined`. It is `undefined` only at the brief moment on resume before a context-mode provider has parked its bundle; the tag recovers automatically and updates once the bundle is available. + +## Behavior + +Passing both `from` and `context` throws. The value is seeded at render (null-tolerant in context mode), refreshed at mount in case the store changed in between (the refresh is `compare`-gated, so an unchanged slice doesn't force a re-render), then kept live by a subscription established on mount. In context mode the tag also listens on the internal publish bus, so it attaches to the store the moment the provider parks it during streaming or resume. Changing the source or swapping the `selector` re-projects; `compare` gates store mutations and the mount refresh, never a deliberate selector or source change. diff --git a/docs/framework/marko/reference/tags/stream-store-provider.md b/docs/framework/marko/reference/tags/stream-store-provider.md new file mode 100644 index 00000000..65ae8c69 --- /dev/null +++ b/docs/framework/marko/reference/tags/stream-store-provider.md @@ -0,0 +1,31 @@ +--- +id: stream-store-provider +title: "" +--- + +# Tag: `` + +A provider for progressive streaming: it creates a per-request store bundle from data that becomes available inside a late ``, and keeps it live across the server-to-client boundary. Place it inside the awaited body so the awaited value is captured into `value`. Wraps its content. + +```marko + + ({ order: createStore(order) })> + b.order selector=(o) => o.total> +

Total: ${total}

+
+
+``` + +## Attributes + +- **`value`** — `() => Record` (required). A thunk returning the bundle of stores or atoms, built from the awaited per-request data. Called on the server at render and again on the client at mount. +- **`key`** — `string` (**required**). The context key the bundle is parked under. Unlike ``, there is no default; each streamed provider needs an explicit, distinct key. +- **`content`** — `Marko.Body` (optional). The body rendered inside the provider. + +## Tag variable + +None. Like ``, it renders its content and binds no tag variable. + +## Behavior + +This is the "born-with-data" transport. The awaited value is captured by `value` and crosses to the client through the await subtree's resume scope; the bundle is built into `$global` under the key on the server, and rebuilt and published on mount so client subscribers attach to live instances. It shares ``'s owner marker, so a `` and a `` on the same key detect each other and throw rather than clobber. For production, a streaming Marko app must run under `@marko/run`; a standalone `@marko/vite` production build does not coordinate resume correctly. diff --git a/examples/marko/README.md b/examples/marko/README.md new file mode 100644 index 00000000..f560077f --- /dev/null +++ b/examples/marko/README.md @@ -0,0 +1,23 @@ +# Marko examples for TanStack Store + +These mirror the sibling adapter examples (react, svelte, …) using +`@tanstack/marko-store`. Every example is an [@marko/run](https://github.com/marko-js/run) +app — Marko's SSR meta-framework and the way real Marko apps ship — so each one +server-renders, resumes in the browser, and stays live. The parity examples +(`stores`, `simple`, `atoms`, `store-actions`, `store-context`) demonstrate the +same scenarios the other frameworks ship; `ssr-resume`, `ssr-per-request-multi`, +and `streaming` demonstrate the Marko-specific SSR patterns (resume, per-request +stores, streamed `` data). + +Each example runs standalone: + +- `npm install` +- `npm run dev` — dev server +- `npm run preview` — production build + serve +- `npm run test:e2e` — a small Playwright smoke (renders, one interaction, no + client errors) +- `npm run test:types` — `marko-type-check` + +Every example carries a `marko.json` (`tags-dir` → the package's `src/tags`) and a +tsconfig `include` of those tags. This works around a Marko tag-discovery issue +under pnpm's isolated `node_modules`; when the upstream fix ships, both can go. diff --git a/examples/marko/atoms/.marko-run/routes.d.ts b/examples/marko/atoms/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/atoms/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/atoms/README.md b/examples/marko/atoms/README.md new file mode 100644 index 00000000..fd0d066b --- /dev/null +++ b/examples/marko/atoms/README.md @@ -0,0 +1,13 @@ +# Marko Store — atoms + +A module-level `createAtom`. The read-only total uses `` (no +selector function = the whole value); the editable count uses ``, +whose binding writes back through `atom.set`. Reset calls `atom.set(0)`. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/atoms/e2e/atoms.spec.ts b/examples/marko/atoms/e2e/atoms.spec.ts new file mode 100644 index 00000000..8f304fbb --- /dev/null +++ b/examples/marko/atoms/e2e/atoms.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('atoms: renders and increments', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Total: 0')).toBeVisible() + await page.getByRole('button', { name: 'Increment', exact: true }).click() + await expect(page.getByText('Total: 1')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/atoms/marko.json b/examples/marko/atoms/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/atoms/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/atoms/package.json b/examples/marko/atoms/package.json new file mode 100644 index 00000000..2988ca49 --- /dev/null +++ b/examples/marko/atoms/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-atoms", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/atoms/playwright.config.ts b/examples/marko/atoms/playwright.config.ts new file mode 100644 index 00000000..111d419a --- /dev/null +++ b/examples/marko/atoms/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:3061', + }, + webServer: { + command: 'npm run dev -- --port 3061', + url: 'http://localhost:3061', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/atoms/src/App.marko b/examples/marko/atoms/src/App.marko new file mode 100644 index 00000000..b5cb22ad --- /dev/null +++ b/examples/marko/atoms/src/App.marko @@ -0,0 +1,20 @@ +import { countAtom } from './atom' + +
+

Marko Atoms

+

This example creates a module-level atom and reads and updates it with the tags.

+ + + countAtom)/> +

Total: ${total}

+ +
+ + +
+ + + countAtom)/> +

Editable count: ${count}

+ +
diff --git a/examples/marko/atoms/src/atom.ts b/examples/marko/atoms/src/atom.ts new file mode 100644 index 00000000..79fa57fc --- /dev/null +++ b/examples/marko/atoms/src/atom.ts @@ -0,0 +1,4 @@ +import { createAtom } from '@tanstack/marko-store' + +// You can create atoms outside of components, at module scope. +export const countAtom = createAtom(0) diff --git a/examples/marko/atoms/src/routes/+page.marko b/examples/marko/atoms/src/routes/+page.marko new file mode 100644 index 00000000..54443bcd --- /dev/null +++ b/examples/marko/atoms/src/routes/+page.marko @@ -0,0 +1,9 @@ +import App from '../App.marko' + + + + marko-store: atoms + + + + diff --git a/examples/marko/atoms/tsconfig.json b/examples/marko/atoms/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/atoms/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/atoms/vite.config.ts b/examples/marko/atoms/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/atoms/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/simple/.marko-run/routes.d.ts b/examples/marko/simple/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/simple/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/simple/README.md b/examples/marko/simple/README.md new file mode 100644 index 00000000..599de715 --- /dev/null +++ b/examples/marko/simple/README.md @@ -0,0 +1,13 @@ +# Marko Store — simple + +A multi-component counter split across files. `Increment.marko` writes with +`store.setState`; `Display.marko` reads a slice with ``; +`App.marko` composes them around a module-level store in `store.ts`. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/simple/e2e/simple.spec.ts b/examples/marko/simple/e2e/simple.spec.ts new file mode 100644 index 00000000..80077196 --- /dev/null +++ b/examples/marko/simple/e2e/simple.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('simple: renders and counts a click', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('dogs: 0')).toBeVisible() + await page.getByRole('button', { name: 'My Friend Likes dogs' }).click() + await expect(page.getByText('dogs: 1')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/simple/marko.json b/examples/marko/simple/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/simple/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/simple/package.json b/examples/marko/simple/package.json new file mode 100644 index 00000000..6324b998 --- /dev/null +++ b/examples/marko/simple/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-simple", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/simple/playwright.config.ts b/examples/marko/simple/playwright.config.ts new file mode 100644 index 00000000..1b531105 --- /dev/null +++ b/examples/marko/simple/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:3060', + }, + webServer: { + command: 'npm run dev -- --port 3060', + url: 'http://localhost:3060', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/simple/src/App.marko b/examples/marko/simple/src/App.marko new file mode 100644 index 00000000..50d72bf4 --- /dev/null +++ b/examples/marko/simple/src/App.marko @@ -0,0 +1,11 @@ +import Increment from './Increment.marko' +import Display from './Display.marko' + +
+

How many of your friends like cats or dogs?

+

Press a button to add a counter of how many of your friends like cats or dogs.

+ + + + +
diff --git a/examples/marko/simple/src/Display.marko b/examples/marko/simple/src/Display.marko new file mode 100644 index 00000000..68449c2c --- /dev/null +++ b/examples/marko/simple/src/Display.marko @@ -0,0 +1,9 @@ +import { store } from './store' + +export interface Input { + animal: 'dogs' | 'cats' +} + +// Re-renders only when state[animal] changes; an unrelated change won't re-render it. + store) selector=((s: { dogs: number; cats: number }) => s[input.animal])/> +
${input.animal}: ${count}
diff --git a/examples/marko/simple/src/Increment.marko b/examples/marko/simple/src/Increment.marko new file mode 100644 index 00000000..3d771c5d --- /dev/null +++ b/examples/marko/simple/src/Increment.marko @@ -0,0 +1,11 @@ +import { store } from './store' + +export interface Input { + animal: 'dogs' | 'cats' +} + + diff --git a/examples/marko/simple/src/routes/+page.marko b/examples/marko/simple/src/routes/+page.marko new file mode 100644 index 00000000..b276278b --- /dev/null +++ b/examples/marko/simple/src/routes/+page.marko @@ -0,0 +1,9 @@ +import App from '../App.marko' + + + + marko-store: simple + + + + diff --git a/examples/marko/simple/src/store.ts b/examples/marko/simple/src/store.ts new file mode 100644 index 00000000..d3cc5d82 --- /dev/null +++ b/examples/marko/simple/src/store.ts @@ -0,0 +1,7 @@ +import { createStore } from '@tanstack/marko-store' + +// You can instantiate a Store outside of components, at module scope. +export const store = createStore({ + dogs: 0, + cats: 0, +}) diff --git a/examples/marko/simple/tsconfig.json b/examples/marko/simple/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/simple/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/simple/vite.config.ts b/examples/marko/simple/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/simple/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/ssr-per-request-multi/.marko-run/routes.d.ts b/examples/marko/ssr-per-request-multi/.marko-run/routes.d.ts new file mode 100644 index 00000000..0e10f713 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/.marko-run/routes.d.ts @@ -0,0 +1,53 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + "/multi": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/multi/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/multi"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/ssr-per-request-multi/README.md b/examples/marko/ssr-per-request-multi/README.md new file mode 100644 index 00000000..e8fddb88 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/README.md @@ -0,0 +1,17 @@ +# Marko Store — ssr-per-request-multi + +Two pages. The home page (`/`) rebuilds a store from per-request DATA passed by +the route — deliberately richer than a counter: a string, a Date, and a nested +array of objects, with selectors slicing each (the array slice through the +SSR-safe inline `shallow` compare). The data crosses in the serialized payload +and the store is rebuilt on each side. The `/multi` page parks a bundle of two stores in one provider, read +independently, with a context write targeting one. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. +- open `/` and `/multi` diff --git a/examples/marko/ssr-per-request-multi/e2e/ssr-per-request-multi.spec.ts b/examples/marko/ssr-per-request-multi/e2e/ssr-per-request-multi.spec.ts new file mode 100644 index 00000000..af7459a3 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/e2e/ssr-per-request-multi.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('per-request page: data rebuilds on resume and a context write lands', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Count: 42', { exact: true })).toBeVisible() + // The rich slices: a string, a Date, and a shallow-compared array slice, all + // built from the per-request seed and painted server-side. + await expect(page.getByText('quarterly report (updated 2026-07-14)')).toBeVisible() + await expect(page.getByText('Rows: alpha, beta (2)')).toBeVisible() + await page.getByRole('button', { name: 'inc via context' }).click() + await expect(page.getByText('Count: 43', { exact: true })).toBeVisible() + // A write to the nested array lands live through the shallow-compared slice. + await page.getByRole('button', { name: 'add row' }).click() + await expect(page.getByText('Rows: alpha, beta, gamma (3)')).toBeVisible() + expect(errors).toEqual([]) +}) + +test('multi page renders', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/multi') + await expect(page.locator('h1')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/ssr-per-request-multi/marko.json b/examples/marko/ssr-per-request-multi/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/ssr-per-request-multi/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/ssr-per-request-multi/package.json b/examples/marko/ssr-per-request-multi/package.json new file mode 100644 index 00000000..b1692d24 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-ssr-per-request-multi", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/ssr-per-request-multi/playwright.config.ts b/examples/marko/ssr-per-request-multi/playwright.config.ts new file mode 100644 index 00000000..9cda1d48 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:5201', + }, + webServer: { + command: 'npm run dev -- --port 5201', + url: 'http://localhost:5201', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/ssr-per-request-multi/src/routes/+page.marko b/examples/marko/ssr-per-request-multi/src/routes/+page.marko new file mode 100644 index 00000000..a6c10df0 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/src/routes/+page.marko @@ -0,0 +1,70 @@ +import { createStore, shallow } from '@tanstack/marko-store' +import type { Store } from '@tanstack/marko-store' + +// The seed is deliberately RICHER than a counter: real routes pass real data. A +// string, a nested array of objects, and a Date all cross the serialized payload +// (Marko's serializer supports plain data plus built-ins like Date/Map/Set), and +// the store is rebuilt from them per request AND on the client — a live store is +// never serialized. In an app this object would come from the route's handler; +// here it is inlined (the count seed, 42, is from the old dev-server route table). +static interface Report { + count: number + title: string + updatedAt: Date + rows: Array<{ id: number; label: string; tags: Array }> +} +static const seed = (): Report => ({ + count: 42, + title: 'quarterly report', + updatedAt: new Date('2026-07-14T00:00:00.000Z'), + rows: [ + { id: 1, label: 'alpha', tags: ['a', 'b'] }, + { id: 2, label: 'beta', tags: [] }, + ], +}) + + + + Per-request data + +
+

Per-request data, rebuilt on resume

+

The route passes plain DATA (here, the inlined seed); the provider creates the store from it. The data crosses in the serialized payload and the store is rebuilt on the client — a live store is never serialized.

+ + + +

Resumed in browser: ${resumed}

+ + ({ report: createStore(seed()) })> + }) => c.report) selector=((s: Report) => s.count)/> +

Count: ${count}

+ + }) => c.report) selector=((s: Report) => s.title)/> + }) => c.report) selector=((s: Report) => s.updatedAt.toISOString().slice(0, 10))/> +

${title} (updated ${updated})

+ + + }) => c.report) + selector=((s: Report) => s.rows.map((r) => r.label)) + compare=((a, b) => shallow(a, b)) + /> +

Rows: ${(labels ?? []).join(', ')} (${(labels ?? []).length})

+ + + + +
+
+ + diff --git a/examples/marko/ssr-per-request-multi/src/routes/multi/+page.marko b/examples/marko/ssr-per-request-multi/src/routes/multi/+page.marko new file mode 100644 index 00000000..0fd31b97 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/src/routes/multi/+page.marko @@ -0,0 +1,28 @@ +import { a, b } from '../../stores' + + + + Multiple stores in one provider + +
+

Multiple stores in one provider

+

One provider parks a bundle of two stores. Each selector picks a different member; a write to one moves only its own selector.

+ + + +

Resumed in browser: ${resumed}

+ + ({ a, b })> + c.a) selector=((s: any) => s.count)/> + c.b) selector=((s: any) => s.count)/> +

A: ${countA}

+

B: ${countB}

+ + +
+ + + +
+ + diff --git a/examples/marko/ssr-per-request-multi/src/stores.ts b/examples/marko/ssr-per-request-multi/src/stores.ts new file mode 100644 index 00000000..6d55e079 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/src/stores.ts @@ -0,0 +1,5 @@ +import { createStore } from '@tanstack/marko-store' + +// Two isolated module stores for the multi-store page. +export const a = createStore({ count: 5 }) +export const b = createStore({ count: 50 }) diff --git a/examples/marko/ssr-per-request-multi/tsconfig.json b/examples/marko/ssr-per-request-multi/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/ssr-per-request-multi/vite.config.ts b/examples/marko/ssr-per-request-multi/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/ssr-per-request-multi/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/ssr-resume/.marko-run/routes.d.ts b/examples/marko/ssr-resume/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/ssr-resume/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/ssr-resume/README.md b/examples/marko/ssr-resume/README.md new file mode 100644 index 00000000..da4427ee --- /dev/null +++ b/examples/marko/ssr-resume/README.md @@ -0,0 +1,14 @@ +# Marko Store — ssr-resume + +A server-rendered page: the server paints the store's value, then the page +resumes and the store stays live (external writes and the atom write work after +load). The store is a module singleton and is rebuilt on each side, so no live +store is ever serialized. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/ssr-resume/e2e/ssr-resume.spec.ts b/examples/marko/ssr-resume/e2e/ssr-resume.spec.ts new file mode 100644 index 00000000..1433ef1c --- /dev/null +++ b/examples/marko/ssr-resume/e2e/ssr-resume.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('ssr-resume: server paints, page resumes, tags stay live', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Resumed in browser: yes')).toBeVisible() + await expect(page.getByText('Atom: 7')).toBeVisible() + await page.getByRole('button', { name: 'Atom inc' }).click() + await expect(page.getByText('Atom: 8')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/ssr-resume/marko.json b/examples/marko/ssr-resume/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/ssr-resume/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/ssr-resume/package.json b/examples/marko/ssr-resume/package.json new file mode 100644 index 00000000..198e3eda --- /dev/null +++ b/examples/marko/ssr-resume/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-ssr-resume", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/ssr-resume/playwright.config.ts b/examples/marko/ssr-resume/playwright.config.ts new file mode 100644 index 00000000..48683080 --- /dev/null +++ b/examples/marko/ssr-resume/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:5200', + }, + webServer: { + command: 'npm run dev -- --port 5200', + url: 'http://localhost:5200', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/ssr-resume/src/routes/+page.marko b/examples/marko/ssr-resume/src/routes/+page.marko new file mode 100644 index 00000000..e102b7a3 --- /dev/null +++ b/examples/marko/ssr-resume/src/routes/+page.marko @@ -0,0 +1,24 @@ +import { counterStore, countAtom } from '../store' + + + + SSR resume + +
+

Server render and resume

+

The server paints the store's values; the page then resumes and the store stays live in the browser.

+ + + +

Resumed in browser: ${resumed}

+ + counterStore) selector=((s: { count: number }) => s.count)/> +

Count: ${count}

+ + + countAtom)/> +

Atom: ${atom}

+ +
+ + diff --git a/examples/marko/ssr-resume/src/store.ts b/examples/marko/ssr-resume/src/store.ts new file mode 100644 index 00000000..77651d15 --- /dev/null +++ b/examples/marko/ssr-resume/src/store.ts @@ -0,0 +1,7 @@ +import { createAtom, createStore } from '@tanstack/marko-store' + +// Module-level store + atom. The page's inline thunks (() => counterStore) close over these +// stable references, which keeps the live store OUT of the serialized resume payload — the +// store is rebuilt on each side, never serialized. +export const counterStore = createStore({ count: 5 }) +export const countAtom = createAtom(7) diff --git a/examples/marko/ssr-resume/tsconfig.json b/examples/marko/ssr-resume/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/ssr-resume/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/ssr-resume/vite.config.ts b/examples/marko/ssr-resume/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/ssr-resume/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/store-actions/.marko-run/routes.d.ts b/examples/marko/store-actions/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/store-actions/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/store-actions/README.md b/examples/marko/store-actions/README.md new file mode 100644 index 00000000..d5ea98c8 --- /dev/null +++ b/examples/marko/store-actions/README.md @@ -0,0 +1,14 @@ +# Marko Store — store-actions + +A module-level store created with actions. Reads go through ``; +mutations call `store.actions.addCat` / `addDog` / `log`. There is no Marko +equivalent of the experimental `_useStore` tuple, and none is needed — holding +the store directly, `` plus `store.actions` cover the same ground. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/store-actions/e2e/store-actions.spec.ts b/examples/marko/store-actions/e2e/store-actions.spec.ts new file mode 100644 index 00000000..2ab65b63 --- /dev/null +++ b/examples/marko/store-actions/e2e/store-actions.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('store-actions: renders and votes through an action', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Cats: 0')).toBeVisible() + await page.getByRole('button', { name: 'Vote for cats' }).click() + await expect(page.getByText('Cats: 1')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/store-actions/marko.json b/examples/marko/store-actions/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/store-actions/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/store-actions/package.json b/examples/marko/store-actions/package.json new file mode 100644 index 00000000..c38ba81d --- /dev/null +++ b/examples/marko/store-actions/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-store-actions", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/store-actions/playwright.config.ts b/examples/marko/store-actions/playwright.config.ts new file mode 100644 index 00000000..7ac4de8b --- /dev/null +++ b/examples/marko/store-actions/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:3062', + }, + webServer: { + command: 'npm run dev -- --port 3062', + url: 'http://localhost:3062', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/store-actions/src/App.marko b/examples/marko/store-actions/src/App.marko new file mode 100644 index 00000000..2565f11a --- /dev/null +++ b/examples/marko/store-actions/src/App.marko @@ -0,0 +1,23 @@ +import { petStore } from './store' + +
+ +

Marko Store Actions

+

+ This example creates a module-level store with actions. Components read state + with a selector and mutate through store.actions. Marko has no equivalent of + the experimental _useStore tuple, and does not need one: you already hold the + store, so a selector for the read plus store.actions for the write cover it. +

+ + petStore) selector=(s => s.cats)/> +

Cats: ${cats}

+ + + petStore) selector=(s => s.dogs)/> +

Dogs: ${dogs}

+ + + petStore) selector=(s => s.cats + s.dogs)/> +

Total votes: ${total}

+
diff --git a/examples/marko/store-actions/src/routes/+page.marko b/examples/marko/store-actions/src/routes/+page.marko new file mode 100644 index 00000000..ae102b1b --- /dev/null +++ b/examples/marko/store-actions/src/routes/+page.marko @@ -0,0 +1,9 @@ +import App from '../App.marko' + + + + marko-store: store actions + + + + diff --git a/examples/marko/store-actions/src/store.ts b/examples/marko/store-actions/src/store.ts new file mode 100644 index 00000000..e84b6c5e --- /dev/null +++ b/examples/marko/store-actions/src/store.ts @@ -0,0 +1,11 @@ +import { createStore } from '@tanstack/marko-store' + +// A module-level store created WITH actions (the second argument). +export const petStore = createStore( + { cats: 0, dogs: 0 }, + ({ setState, get }) => ({ + addCat: () => setState((prev) => ({ ...prev, cats: prev.cats + 1 })), + addDog: () => setState((prev) => ({ ...prev, dogs: prev.dogs + 1 })), + log: () => console.log(get()), + }), +) diff --git a/examples/marko/store-actions/tsconfig.json b/examples/marko/store-actions/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/store-actions/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/store-actions/vite.config.ts b/examples/marko/store-actions/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/store-actions/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/store-context/.marko-run/routes.d.ts b/examples/marko/store-context/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/store-context/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/store-context/README.md b/examples/marko/store-context/README.md new file mode 100644 index 00000000..a59625fd --- /dev/null +++ b/examples/marko/store-context/README.md @@ -0,0 +1,15 @@ +# Marko Store — store-context + +The flagship for the adapter's delivery trio. `` parks a bundle +of `{ votesStore, countAtom }`; nested components read the store with +``, read the atom the same way (and with `` +for read/write), and write through ``. Where the other frameworks +lean on their native context, Marko uses provider + context + selector. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/store-context/e2e/store-context.spec.ts b/examples/marko/store-context/e2e/store-context.spec.ts new file mode 100644 index 00000000..6fd1dd59 --- /dev/null +++ b/examples/marko/store-context/e2e/store-context.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('store-context: renders and a context write propagates', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Total votes: 0')).toBeVisible() + await page.getByRole('button', { name: 'Add cat' }).click() + await expect(page.getByText('Total votes: 1')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/store-context/marko.json b/examples/marko/store-context/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/store-context/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/store-context/package.json b/examples/marko/store-context/package.json new file mode 100644 index 00000000..b61a4b6b --- /dev/null +++ b/examples/marko/store-context/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-store-context", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/store-context/playwright.config.ts b/examples/marko/store-context/playwright.config.ts new file mode 100644 index 00000000..32e699df --- /dev/null +++ b/examples/marko/store-context/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:3063', + }, + webServer: { + command: 'npm run dev -- --port 3063', + url: 'http://localhost:3063', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/store-context/src/App.marko b/examples/marko/store-context/src/App.marko new file mode 100644 index 00000000..3e8c6647 --- /dev/null +++ b/examples/marko/store-context/src/App.marko @@ -0,0 +1,36 @@ +import { createStore, createAtom } from '@tanstack/marko-store' +import CatCard from './CatCard.marko' +import DogCard from './DogCard.marko' +import TotalCard from './TotalCard.marko' +import StoreButtons from './StoreButtons.marko' +import AtomSummary from './AtomSummary.marko' +import NestedAtomControls from './NestedAtomControls.marko' +import DeepAtomEditor from './DeepAtomEditor.marko' + +// Marko has no native context, so this example uses the adapter's delivery trio: +// parks a bundle, children read it with , +// and writes go through . (A real app would give the bundle a named type; +// the context pickers here use `any` to keep the example focused.) +// Build the stores INSIDE the provider's value thunk. Under server rendering a +// component-local const store would be dead after resume (the component body +// never re-runs in the browser); the thunk is rebuilt per request on the server +// and again on the client, which is exactly the per-request pattern. + ({ votesStore: createStore({ cats: 0, dogs: 0 }), countAtom: createAtom(0) })> +
+

Marko Store Context

+

+ This example provides both a store and an atom through a single context + bundle, then reads them from nested components. +

+ + + + +
+

Nested Atom Components

+ + + +
+
+
diff --git a/examples/marko/store-context/src/AtomSummary.marko b/examples/marko/store-context/src/AtomSummary.marko new file mode 100644 index 00000000..13842cc8 --- /dev/null +++ b/examples/marko/store-context/src/AtomSummary.marko @@ -0,0 +1,3 @@ + + c.countAtom)/> +

Atom count: ${count}

diff --git a/examples/marko/store-context/src/CatCard.marko b/examples/marko/store-context/src/CatCard.marko new file mode 100644 index 00000000..f2267ecb --- /dev/null +++ b/examples/marko/store-context/src/CatCard.marko @@ -0,0 +1,2 @@ + c.votesStore) selector=((s: any) => s.cats)/> +

Cats: ${cats}

diff --git a/examples/marko/store-context/src/DeepAtomEditor.marko b/examples/marko/store-context/src/DeepAtomEditor.marko new file mode 100644 index 00000000..5e64c20e --- /dev/null +++ b/examples/marko/store-context/src/DeepAtomEditor.marko @@ -0,0 +1,7 @@ + + + (ctx() as any)?.countAtom ?? null)/> +
+

Editable atom count: ${count}

+ +
diff --git a/examples/marko/store-context/src/DogCard.marko b/examples/marko/store-context/src/DogCard.marko new file mode 100644 index 00000000..0a1c2dd6 --- /dev/null +++ b/examples/marko/store-context/src/DogCard.marko @@ -0,0 +1,2 @@ + c.votesStore) selector=((s: any) => s.dogs)/> +

Dogs: ${dogs}

diff --git a/examples/marko/store-context/src/NestedAtomControls.marko b/examples/marko/store-context/src/NestedAtomControls.marko new file mode 100644 index 00000000..aacb2a78 --- /dev/null +++ b/examples/marko/store-context/src/NestedAtomControls.marko @@ -0,0 +1,5 @@ + +
+ + +
diff --git a/examples/marko/store-context/src/StoreButtons.marko b/examples/marko/store-context/src/StoreButtons.marko new file mode 100644 index 00000000..52b1de6b --- /dev/null +++ b/examples/marko/store-context/src/StoreButtons.marko @@ -0,0 +1,5 @@ + +
+ + +
diff --git a/examples/marko/store-context/src/TotalCard.marko b/examples/marko/store-context/src/TotalCard.marko new file mode 100644 index 00000000..f143f66b --- /dev/null +++ b/examples/marko/store-context/src/TotalCard.marko @@ -0,0 +1,2 @@ + c.votesStore) selector=((s: any) => s.cats + s.dogs)/> +

Total votes: ${total}

diff --git a/examples/marko/store-context/src/routes/+page.marko b/examples/marko/store-context/src/routes/+page.marko new file mode 100644 index 00000000..0b904553 --- /dev/null +++ b/examples/marko/store-context/src/routes/+page.marko @@ -0,0 +1,9 @@ +import App from '../App.marko' + + + + marko-store: store context + + + + diff --git a/examples/marko/store-context/tsconfig.json b/examples/marko/store-context/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/store-context/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/store-context/vite.config.ts b/examples/marko/store-context/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/store-context/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/stores/.marko-run/routes.d.ts b/examples/marko/stores/.marko-run/routes.d.ts new file mode 100644 index 00000000..4c22b843 --- /dev/null +++ b/examples/marko/stores/.marko-run/routes.d.ts @@ -0,0 +1,34 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/stores/README.md b/examples/marko/stores/README.md new file mode 100644 index 00000000..0824ae9d --- /dev/null +++ b/examples/marko/stores/README.md @@ -0,0 +1,15 @@ +# Marko Store Example + +This example demonstrates: + +- `` +- `store.setState` +- module-level `Store` + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. diff --git a/examples/marko/stores/e2e/stores.spec.ts b/examples/marko/stores/e2e/stores.spec.ts new file mode 100644 index 00000000..b84cfa51 --- /dev/null +++ b/examples/marko/stores/e2e/stores.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('stores: renders, counts a click, and the shallow pair follows', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('Cats: 0')).toBeVisible() + await expect(page.getByText('As a pair: 0 cats / 0 dogs')).toBeVisible() + await page.getByRole('button', { name: 'Add cat' }).click() + await expect(page.getByText('Cats: 1')).toBeVisible() + await expect(page.getByText('As a pair: 1 cats / 0 dogs')).toBeVisible() + expect(errors).toEqual([]) +}) diff --git a/examples/marko/stores/marko.json b/examples/marko/stores/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/stores/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/stores/package.json b/examples/marko/stores/package.json new file mode 100644 index 00000000..5a46d11b --- /dev/null +++ b/examples/marko/stores/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-stores", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/stores/playwright.config.ts b/examples/marko/stores/playwright.config.ts new file mode 100644 index 00000000..94ab5eb3 --- /dev/null +++ b/examples/marko/stores/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:3070', + }, + webServer: { + command: 'npm run dev -- --port 3070', + url: 'http://localhost:3070', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/stores/src/App.marko b/examples/marko/stores/src/App.marko new file mode 100644 index 00000000..8343621c --- /dev/null +++ b/examples/marko/stores/src/App.marko @@ -0,0 +1,44 @@ +import { shallow } from '@tanstack/marko-store' +import { petStore } from './store' + +
+

Marko Store

+

+ This example creates a module-level store. Components read state with a + selector and update it directly with store.setState. +

+ + petStore) selector=(s => s.cats)/> +

Cats: ${cats}

+ + petStore) selector=(s => s.dogs)/> +

Dogs: ${dogs}

+ + petStore) selector=(s => s.cats + s.dogs)/> +

Total votes: ${total}

+ + + petStore) + selector=(s => ({ cats: s.cats, dogs: s.dogs })) + compare=((a, b) => shallow(a, b)) + /> +

As a pair: ${pair?.cats ?? 0} cats / ${pair?.dogs ?? 0} dogs

+ +
+ + +
+
diff --git a/examples/marko/stores/src/routes/+page.marko b/examples/marko/stores/src/routes/+page.marko new file mode 100644 index 00000000..f7be619a --- /dev/null +++ b/examples/marko/stores/src/routes/+page.marko @@ -0,0 +1,9 @@ +import App from '../App.marko' + + + + marko-store: stores + + + + diff --git a/examples/marko/stores/src/store.ts b/examples/marko/stores/src/store.ts new file mode 100644 index 00000000..c6fa9f56 --- /dev/null +++ b/examples/marko/stores/src/store.ts @@ -0,0 +1,7 @@ +import { createStore } from '@tanstack/marko-store' + +// You can create stores at module scope and import them wherever you need them. +export const petStore = createStore({ + cats: 0, + dogs: 0, +}) diff --git a/examples/marko/stores/tsconfig.json b/examples/marko/stores/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/stores/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/stores/vite.config.ts b/examples/marko/stores/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/stores/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/examples/marko/streaming/.marko-run/routes.d.ts b/examples/marko/streaming/.marko-run/routes.d.ts new file mode 100644 index 00000000..6fd70e36 --- /dev/null +++ b/examples/marko/streaming/.marko-run/routes.d.ts @@ -0,0 +1,53 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + "/out-of-order": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/out-of-order/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/out-of-order"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/examples/marko/streaming/README.md b/examples/marko/streaming/README.md new file mode 100644 index 00000000..8457c420 --- /dev/null +++ b/examples/marko/streaming/README.md @@ -0,0 +1,20 @@ +# Marko Store — streaming + +Streaming server-computed per-request data into a live store with +``. The home page (`/`) shows the in-order case: a store +born from a value that arrives inside a late ``, painted server-side with +no flash and live after resume. The `/out-of-order` page wraps each `` in +`` with a `<@placeholder>`, so the fast block paints before the slow one +while each store stays live. + +Note the server gives each request its own fresh `$global`: born-with-data parks +the store on `$global`, so requests must not share it. + +To run this example: + +- `npm install` +- `npm run dev` (or `npm run preview` for the production build) + +It's an `@marko/run` app: the page is server-rendered, resumes in the browser, +and stays live. `npm run test:e2e` runs its Playwright smoke test. +- open `/` and `/out-of-order` diff --git a/examples/marko/streaming/e2e/streaming.spec.ts b/examples/marko/streaming/e2e/streaming.spec.ts new file mode 100644 index 00000000..cba9f587 --- /dev/null +++ b/examples/marko/streaming/e2e/streaming.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test' + +// Example smoke: the page renders, one interaction works, zero client errors. +test('in-order: streamed store paints during streaming and stays live', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + await expect(page.getByText('77')).toBeVisible({ timeout: 15_000 }) + expect(errors).toEqual([]) +}) + +test('out-of-order: both streamed values land', async ({ page }) => { + // Dev-server noise (HMR websocket handshakes) is not an application error. + const ignore = (t: string) => t.includes('WebSocket') || t.includes('[vite]') + const errors: Array = [] + page.on('pageerror', (e) => { + if (!ignore(e.message)) errors.push('pageerror: ' + e.message) + }) + page.on('console', (m) => { + if (m.type() === 'error' && !ignore(m.text())) errors.push('console.error: ' + m.text()) + }) + + await page.goto('/out-of-order') + await expect(page.getByText('33')).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('44')).toBeVisible({ timeout: 15_000 }) + expect(errors).toEqual([]) +}) diff --git a/examples/marko/streaming/marko.json b/examples/marko/streaming/marko.json new file mode 100644 index 00000000..8e56a67e --- /dev/null +++ b/examples/marko/streaming/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../../../packages/marko-store/src/tags" } diff --git a/examples/marko/streaming/package.json b/examples/marko/streaming/package.json new file mode 100644 index 00000000..39cb9e48 --- /dev/null +++ b/examples/marko/streaming/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/store-example-marko-streaming", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "build": "marko-run build", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check -p tsconfig.json" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/marko-store": "^0.11.0", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "vite": "^8.0.8" + } +} diff --git a/examples/marko/streaming/playwright.config.ts b/examples/marko/streaming/playwright.config.ts new file mode 100644 index 00000000..52606b27 --- /dev/null +++ b/examples/marko/streaming/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test' + +// Example smoke e2e. Dev-mode webServer on purpose: examples demonstrate, they +// don't gate — the production-mode gate lives in packages/marko-store/e2e. +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:5202', + }, + webServer: { + command: 'npm run dev -- --port 5202', + url: 'http://localhost:5202', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + }, +}) diff --git a/examples/marko/streaming/src/routes/+page.marko b/examples/marko/streaming/src/routes/+page.marko new file mode 100644 index 00000000..7ee70685 --- /dev/null +++ b/examples/marko/streaming/src/routes/+page.marko @@ -0,0 +1,28 @@ +import { createStore } from '@tanstack/marko-store' + + + + + + Streaming (in-order) + +
+

Streaming a store into a late await (in-order)

+

The server computes per-request data inside a late await; the store is born from that value with <stream-store-provider>, renders server-side with no flash, and stays live after resume.

+ + + +

Resumed in browser: ${resumed}

+ + + setTimeout(() => r(77), 2000))> + ({ store: createStore({ n: slice }) })> + c.store) selector=((s: any) => s.n)/> +

Streamed value: ${n}

+ + +
+ +
+ + diff --git a/examples/marko/streaming/src/routes/out-of-order/+page.marko b/examples/marko/streaming/src/routes/out-of-order/+page.marko new file mode 100644 index 00000000..86dae216 --- /dev/null +++ b/examples/marko/streaming/src/routes/out-of-order/+page.marko @@ -0,0 +1,43 @@ +import { createStore } from '@tanstack/marko-store' + + + + + + Streaming (out-of-order) + +
+

Streaming out-of-order with try/placeholder

+

Each awaited block is born with its own store. Wrapping an await in <try> with a <@placeholder> lets the fast block paint before the slow one, while each store stays live. Click either inc button after the block lands to confirm the store is interactive post-resume.

+ + + +

Resumed in browser: ${resumed}

+ + + + setTimeout(() => r(33), 2500))> + ({ store: createStore({ n: ss }) })> + c.store) selector=((s: any) => s.n)/> +

Slow: ${vs}

+ + +
+ + <@placeholder>

loading slow…

+
+ + + setTimeout(() => r(44), 800))> + ({ store: createStore({ n: sf }) })> + c.store) selector=((s: any) => s.n)/> +

Fast: ${vf}

+ + +
+ + <@placeholder>

loading fast…

+
+
+ + diff --git a/examples/marko/streaming/tsconfig.json b/examples/marko/streaming/tsconfig.json new file mode 100644 index 00000000..539796e4 --- /dev/null +++ b/examples/marko/streaming/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "../../../packages/marko-store/src/tags/**/*", + "e2e/**/*.ts", + "playwright.config.ts", + "vite.config.ts" + ] +} diff --git a/examples/marko/streaming/vite.config.ts b/examples/marko/streaming/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/examples/marko/streaming/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/knip.json b/knip.json index 7b3bcfa8..3f0b722f 100644 --- a/knip.json +++ b/knip.json @@ -1,10 +1,39 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "workspaces": { + "packages/marko-store": { + "entry": [ + "src/tags/**/*" + ], + "ignoreDependencies": [ + "@marko/language-tools", + "marko" + ] + }, "packages/vue-store": { - "ignoreDependencies": ["vue2", "vue2.7"] + "ignoreDependencies": [ + "vue2", + "vue2.7" + ] + }, + "packages/marko-store/e2e": { + "entry": [ + "src/routes/**/+page.marko", + "src/*.ts", + "*.spec.ts" + ], + "ignore": [ + ".marko-run/**" + ], + "ignoreDependencies": [ + "@marko/language-tools" + ] } }, - "ignoreWorkspaces": ["examples/**"], - "ignore": ["packages/store/src/signal.ts"] + "ignoreWorkspaces": [ + "examples/**" + ], + "ignore": [ + "packages/store/src/signal.ts" + ] } diff --git a/package.json b/package.json index b3e0f4b6..61cfa049 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "nx": "22.6.5", "premove": "^4.0.0", "prettier": "^3.8.2", + "prettier-plugin-marko": "^4.0.9", "prettier-plugin-svelte": "^3.5.1", "publint": "^0.3.18", "sherif": "^1.11.1", diff --git a/packages/marko-store/README.md b/packages/marko-store/README.md new file mode 100644 index 00000000..abda0403 --- /dev/null +++ b/packages/marko-store/README.md @@ -0,0 +1,648 @@ +# @tanstack/marko-store + +Marko 6 adapter for [TanStack Store](https://tanstack.com/store). It re-exports the +entire `@tanstack/store` core and adds five Marko tags so a component can read a +store, stay in sync as it changes, write to it, share stores with its children, and +build a store from data that streams in. + +The re-exported core includes everything you'd import from `@tanstack/store` +directly — `createStore`, `createAtom`, `batch`, `shallow`, and friends. Derived +state is part of that core too: pass a function to `createStore` +(`createStore(() => count.state * 2)`) and read the result through +`` like any other store. + +## The five tags + +- `` — read a value out of a store and re-render when it changes. +- `` — read and write a single-value atom. +- `` — hand a group of stores down to children. +- `` — grab those stores from a child in order to write to them. +- `` — build a store from data that arrives inside a streamed + ``, then hand it down like a provider. + +`` and `` are a pair: `` shares the stores, +and `` (or a selector in `context` mode) reads them back. `` +does nothing on its own — with no provider above it, it throws (see Sharing stores). +`` is the streaming sibling of `` — the same bundle, +for data that only shows up inside an `` (see Streaming data into a store). + +## Words used in this README + +The rest of the doc leans on these, so it's worth pinning them down first. + +- **Store** — an object that holds some state and lets you read it, change it, and + listen for changes: `createStore({ count: 0, name: "Ada" })`. +- **Snapshot** (the store's *value*) — the whole state object right now, + `{ count: 0, name: "Ada" }`. You read it with `store.state`. +- **Slice** — the part of a store's snapshot you select to watch. It can be a single + field or a few related fields grouped together: `s => s.count`, or + `s => ({ id: s.id, name: s.name })`. A slice is a portion of *one store's* state — + not the same as a bundle member, which is a whole store inside a bundle (see Bundle). +- **Selector** — that picking function. `` re-renders only when the + slice the selector returns changes, not on every store change. +- **Atom** — a store for a *single value* (`createAtom(0)`) with a direct setter. + Reach for it when the state is just one thing. +- **Bundle** — one plain object that groups several stores under names, + `{ user, cart }`. It's what `` shares. A bundle is one object + holding many stores — not many providers (see "Sharing stores" below). + +## Installation + +```sh +npm install @tanstack/store @tanstack/marko-store +``` + +`marko` is a peer dependency (`^6`). + +## Store or atom — which one? + +Use a **Store** when your state has more than one field, or when you want to read +just a slice of it and re-render only when that slice changes. Use an **Atom** when +the state is a single value — a number, a flag, a string — that you read and set as +a whole. + +The same counter, written both ways: + +```marko +import { createStore, createAtom } from "@tanstack/store" + +// Atom: the state IS the number. Read and set it directly. +static const countAtom = createAtom(0) + countAtom)> + + + +// Store: the number is one field of an object. Pick it with a selector, +// change it with setState. +static const countStore = createStore({ count: 0, updatedAt: 0 }) + countStore) selector=(s => s.count)> + + +``` + +Rule of thumb: one value → atom; an object you slice → store. + +## Reading a store: `` + +### Why not just read the value directly? + +A store holds *live* state. Reading `store.state.count` once gives you the number at +that instant and nothing more — when the store changes later, your markup won't. +`` subscribes to the store for you and re-renders only when the slice +you selected changes. So `${store.state.count}` would show a stale `0` forever, while +`` keeps it correct. In short, `` is the piece that +makes your markup *reactive* to the store — a plain read is a one-off snapshot that +never updates. + +### Basic use + +```marko +static const store = createStore({ count: 0, name: "Ada" }) + store) selector=(s => s.count)> +

Count: ${count}

+
+``` + +`from` is a *function* that returns the store, never the store itself. That keeps the +store out of serialized component state, which is what lets the page resume after +server rendering. It must return the same store every call — don't create the store +inside it. + +(`static const store = ...`, as in the examples, runs once when the module loads — not +on every render — so the store lives at module level and never becomes part of +serializable component state. That's why it resumes cleanly and isn't "caught" by +serialization. The one catch is that a module-level store is shared across requests on +a server, which is why server rendering builds stores per request instead — see +Module-level vs per-request stores.) + +### Attributes + +| Attribute | Required | Description | +| ---------- | -------- | ------------------------------------------------------------------ | +| `from` | \* | Function returning the store/atom (`() => store`). | +| `context` | \* | Pick a store out of a provided bundle (see Sharing stores). Use instead of `from`. | +| `selector` | no | Maps the snapshot to the slice you want. Defaults to the whole snapshot. | +| `compare` | no | Decides what counts as "changed" for the slice. Defaults to `===`. | +| `key` | no | Which provided bundle to read, when using `context`. Defaults to the shared key. | + +\* Supply exactly one of `from` or `context`. Passing both throws. + +### The optional attributes, by example + +No `selector` — track the whole snapshot: + +```marko + store)> +
${JSON.stringify(whole)}
+
+``` + +`compare` — define what "changed" means. Here, ignore case so `"Ada"` → `"ada"` does +not re-render: + +```marko + store) + selector=(s => s.name) + compare=((a, b) => a.toLowerCase() === b.toLowerCase()) +> +

${name}

+
+``` + +**Object and array slices need `compare=shallow`.** A selector that returns an object +or array (`s => ({ id: s.id, name: s.name })`) builds a *fresh reference on every +store change*, so the default `===` compare never sees two equal slices — the tag +re-renders on every change to the store, even ones your slice doesn't care about. +Pass `shallow` (re-exported from the core) to compare contents instead of identity: + +```marko +import { shallow } from "@tanstack/marko-store" + + store) + selector=(s => ({ id: s.id, name: s.name })) + compare=((a, b) => shallow(a, b)) +> +

${user.name}

+
+``` + +`shallow` compares one level deep and also understands `Map`, `Set`, and `Date`. +Single-field slices (`s => s.count`) don't need it — primitives compare fine +with `===`. + +One rule on **server-rendered pages**: pass it through an inline arrow, +`compare=((a, b) => shallow(a, b))`, as in the snippet above written inline. The +tag's input crosses Marko's serialization boundary on resume, and a bare imported +function isn't serializable (inline functions in `.marko` files are +compiler-registered; imports from plain JS are not — the dev server fails loudly +with "Unable to serialize … reading compare"). In a browser-only mount, +`compare=shallow` directly is fine. + +`context` — read a store out of a bundle a `` shared, instead of from +a store you hold directly: + +```marko + c.user) selector=(s => s.name)> +

${name}

+
+``` + +Use `context` when the store comes from a provider above you; use `from` when you +already hold the store (imported or module-level). The two are exclusive — exactly one. + +`key` — when using `context`, picks which bundle to read if more than one provider is +in play; covered under Sharing stores. + +Swapping the `selector` always re-publishes the new slice; `compare` only suppresses +repeat updates coming from store changes, never a deliberate change of selector. + +## Writing an atom: `` + +Two-way binding for an atom. The bound value is writable: assigning it calls the +atom's setter. + +```marko +static const count = createAtom(0) + count)> + + +``` + +| Attribute | Required | Description | +| --------- | -------- | ------------------------------------ | +| `from` | yes | Function returning the atom (`() => a`). | + +`` is for atoms — it writes through the atom's `.set`. Hand it a `Store` +and it throws a clear error at render, because a `Store` has no such setter: read a +`Store` with `` and write it with `store.setState(...)`. + +> Several writes in one tick collapse into one update. For dependent updates, use the +> function form: `count.set(p => p + 1)`. + +## Sharing stores with children: `` + `` + +### What a bundle is, and how it differs from separate stores + +A bundle is one plain object that groups stores under names: `{ user, cart }`. +`` parks that one object where children can find it, and each child +picks the store it wants out of it. This is deliberately **one provider holding many +stores** — not one provider per store. (Two providers under the same name clash; see +Nesting.) + +### Is this just Marko's missing context tag? + +It fills a similar role, but it isn't a generic context tag — and Marko's own +mechanism is worth knowing. + +First, two ideas worth separating, since both involve "sharing." A **store** is the +live, changing state itself — you read it, write it, and subscribe to it, and it lasts +as long as you hold a reference to it. **Context** is only a *delivery* mechanism: a way +for a value to reach a deep child without threading it through every tag in between. On +its own, context says nothing about reactivity or how long something lives — it just +gets a value from an ancestor to its descendants. `` combines the two: +it delivers a *store* through a context-style channel, so children get both the reach +of context and the reactivity of a store. + +"Context" usually means sharing a value down the tree without threading it through +every tag in between — a theme, the current user, a request id. Marko v5 did have a +`` tag (in `@marko/tags`), but it's a class-component tag that passes plain +data, updates only when the provider re-renders (client-side), and predates Marko 6's +resumable runtime — so it's not a reactive store and not resume-safe. Marko 6 — the +tags API this adapter targets — has no `` tag at all. Either way, the built-in +way to share a value is `$global` (the same object older Marko calls `out.global`): a +plain bag of values attached to one render. Two things +matter about it. It is **not reactive** — writing to `$global` doesn't re-render +anything. And it exists on **both** the server and the client render, but a value you +set on the server only reaches the client if you name it in `$global.serializedGlobals` +(a list/map of keys Marko then serializes into the page — the classic example is a CSP +nonce). Plain reads of those serialized values are available as `out.global` / +`$global` on the client. + +`` is built on `$global` but is **store-specific**, not a general value +carrier. It groups stores as a bundle; because a live store can't be serialized it does +*not* put stores in `serializedGlobals` — instead it rebuilds the stores per request on +each side from data (the data rides the normal serialized input), so server and client +end up with matching stores. And since `$global` isn't reactive, the provider rings a +small internal signal when it mounts so selectors notice the bundle. So the rule of +thumb: to share a plain value, use `$global` / `serializedGlobals` directly; to share +*stores*, use ``. + +### Can providers be nested? + +Yes. Give each provider a distinct `key`, and a child reads whichever bundle it wants +by passing the matching key. Two providers sharing a key throw on purpose — that's +what stops two features from clobbering each other. (Verified by tests.) + +The two `key`s play different roles: the **provider's** `key` *names* the bundle (the +slot it parks under), while a **selector's** or **``'s** `key` *chooses* +which named bundle to read. It's the same name seen from the writing side and the +reading side, so set them to match. Omit `key` everywhere and they all use one shared +default name. + +```marko + ({ session }))> + ({ cart }))> + c.session) selector=(s => s.user)/> + c.cart) selector=(s => s.items)/> + + +``` + +### The usual (server-render-ready) shape + +Pass the per-request **data** and let the provider build the stores from it. Each +request gets its own fresh stores, and because only the data is serialized — the +stores are rebuilt from it on both the server and the client — the page resumes live +with nothing non-serializable crossing. A bundle can hold any number of stores; each +is read by its own selector and resumes on its own. + +```marko +import { createStore } from "@tanstack/store" + +export interface Input { + user: { name: string } + cart: { items: Array } +} + + ({ + user: createStore(input.user), + cart: createStore(input.cart), +}))> + + c.user) selector=(s => s.name)> +

${name}

+
+ + c.cart) selector=(s => s.items.length)> +

${itemCount} in cart

+
+ + + + + +
+``` + +`user` and `cart` live in one bundle, are each created from their own slice of the +per-request input, and resume independently on the client — incrementing the cart +doesn't touch `user`, and either store rehydrates on its own. + +`` returns a *getter* for the bundle, so write handlers call `ctx()` +and pick the member: `ctx().user.setState(...)`. It only works inside a +`` — there has to be a bundle for it to read — so with no provider +above it, it throws a clear error at render. (The selector's `context` mode is the +same; only `from` mode works without a provider.) + +So when *do* you write through `` versus directly? If you already hold a +store — imported or module-level — you write to it directly: `store.setState(...)`, +right in the handler. There's no write tag for that case and you don't need one +(`` only *reads*). `` exists for the one case where you +*don't* hold the store: a `` handed it down, so the child has no direct +reference, and `` is how it reaches back to the bundle to call +`setState`. (Atoms are the same idea: hold one directly and you use `` or +`atom.set(...)`.) + +### What about a browser-only app (no server rendering)? + +If your app runs only in the browser — a plain single-page app — there's no server +render to match, so you don't need per-request data. Create the stores once when the +module loads and put those same stores in the bundle: + +```marko +static const user = createStore({ name: "Ada" }) +static const cart = createStore({ items: [] }) + + ({ user, cart }))> + ... + +``` + +The per-request version above exists only because server rendering needs a fresh set +of stores for every request. A browser-only app has just one session, so module-level +stores are fine. + +### Module-level vs per-request stores + +A **module-level store** is created once, at the top of a module +(`static const store = createStore(...)`), and there is a single shared instance for +the whole process. In the browser that's exactly what you want, and here's why: each +visitor loads the page in their *own* browser, so each gets their own copy of the +module and their own store instance. Its state builds up over that one session, fully +isolated from every other user. It's what the selector and atom examples above use. + +A **per-request store** is created inside the provider's `value` from incoming data +(`createStore(input.data)`), so a fresh instance is built for each request, and again +on the client during resume. + +Why it matters: on a server a module-level store is shared by *every* request at once, +so one visitor's state would leak into another's. Server rendering therefore needs +per-request stores. A browser-only app doesn't have that problem, so either works. +Rule of thumb: server-rendered → build the stores per request from data; browser-only +→ module-level stores are fine. + +There's a third shape that looks like a fix for the leak but is broken under server +rendering: creating the store as a local `const` *inside a component* +(`` or a plain `const` in the component's body). It's +per-request on the server, but the client is dead: Marko's resume doesn't re-run the +component's body in the browser, so that store is never recreated there — handlers +fire, `setState` hits an instance nothing is subscribed to, and selectors never move. +Under server rendering, share stores through `` (which rebuilds them +on the client) or use a module-level store; don't create one inside a single +component and expect it to be live after resume. + +### `` attributes + +| Attribute | Required | Description | +| --------- | -------- | ----------------------------------------------------------------- | +| `value` | yes | Function returning the bundle object (`() => ({ a, b })`). | +| `key` | no | Name this bundle so a selector/context can target it. Defaults to a shared name. | + +## Streaming data into a store: `` + +### What "streaming" means here, and in-order vs out-of-order + +Server rendering normally sends the whole page at once. **Streaming** lets the server send the +page in pieces: it flushes the shell immediately, then sends each slow piece — a section waiting on +a database call, say — later, as its data becomes ready. In Marko you mark a slow piece with +``: the shell paints right away, and the awaited block streams in when its promise resolves. + +Two slow pieces can finish in either order. **In-order** streaming keeps them in document order — a +piece that finishes early still waits its turn, so the page fills top to bottom. A plain `` +is in-order. **Out-of-order** streaming lets a piece that finishes first paint first, even if it +sits lower in the page, with a placeholder holding its spot until then; you opt into it by wrapping +the `` in `` with a `<@placeholder>`. Out-of-order reaches first content sooner but +needs the placeholder slot. `` behaves identically either way. + +### Why a separate tag from `` + +`` builds its stores up front, in the shell, because their data is ready before the +page renders. Streamed data isn't ready then — it only exists once the `` resolves, which is +*after* the shell (and the provider) have already rendered. So the provider can't build a store +from streamed data: it runs too early. `` is the provider you put *inside* +the awaited block, where the data finally exists. It builds the store there — on the server as the +piece renders, and again in the browser as the piece resumes — so the store is **born with the +data** on both sides. The value paints on the server (no empty first frame, no flash) and the store +is live after resume, exactly like ``, just sourced from data that arrived late. + +Under the hood it is the same machinery as ``: the store is parked on `$global` +(never serialized), and only the plain awaited value crosses the wire, carried inside the block +because the `value` thunk names it. A live store is never serialized. This is also why you must not +hand the awaited data to a store you hold in an ordinary variable inside the block — see Gotchas. + +### When to use it, and when not to + +Use `` when a store's data arrives **inside a streamed ``** — a +per-request fetch you have deferred so the shell can paint first. It works whether that `` +is in-order or wrapped in `` for out-of-order. + +Do **not** use it for data that is ready at render — a normal per-request store built from `input`. +That is ``'s job; reach for it there. And it is not a general "lazy provider": its +whole reason to exist is the streamed-`` timing, so put it inside an ``. (Used outside +one it degrades to provider-like behavior and still works, but then you have just written a heavier +``.) + +Rule of thumb: data ready at render → ``; data that streams in inside an `` +→ ``. + +### Basic use + +```marko +import { createStore } from "@tanstack/store" + +export interface Input { + userId: string +} + + + ({ profile: createStore(profile) }))> + c.profile) selector=(s => s.name)> +

${name}

+
+
+
+``` + +`profile` is the awaited data. The `value` thunk closes over it and builds the bundle right there. +Because the thunk *names* `profile`, Marko ships that data with the block, so the same thunk +rebuilds the store in the browser when the block resumes. Children read it with `` +in `context` mode exactly as under ``, and write it through ``. The +`value` thunk and the bundle are identical to `` — a bundle can hold any number of +stores, each read by its own selector. The only shape difference is *where* the tag goes: inside the +``, so it can see the data. + +### Out-of-order, and catching errors: `` + +The helper is ordering-agnostic — it only builds the store inside the block. Ordering is decided by +the block around it. A plain `` is in-order. To go out-of-order, wrap the `` in +`` with a `<@placeholder>`; the block can then paint as soon as its data resolves, with the +placeholder holding its slot until then: + +```marko + + + ({ profile: createStore(profile) }))> + c.profile) selector=(s => s.name)> +

${name}

+
+
+
+ <@placeholder> +

Loading…

+ +
+``` + +`` is also Marko's error boundary for a streamed block. If anything inside the `` +throws — a rejected fetch, or the duplicate-key guard below — add a `<@catch>` and the block renders +the fallback instead of breaking the stream: + +```marko + + + ({ profile: createStore(profile) }))> + c.profile) selector=(s => s.name)> +

${name}

+
+
+
+ <@catch|err|> +

Couldn't load that section: ${err.message}

+ +
+``` + +This is Marko's `` / `<@catch>`, not a JavaScript `try`/`catch`. + +### Keys: name the box, and don't share it + +`key` works exactly like ``'s, and is **required** here (no default). It names the +box the bundle is parked in, and the matching `key` on a selector or `` chooses which +box to read. Give each streamed provider a distinct `key`. + +A `` and a `` — or two of either — on the **same key** is +always a mistake: a box holds one bundle, so the second would clobber the first. They detect each +other and **throw** a duplicate-key error rather than overwrite silently. Wrap the block in +`` / `<@catch>` if you want that surfaced as a fallback instead of a broken stream (see above). +`key` is required, with no shared default, precisely so a streamed provider cannot quietly collide +with a top-level provider's default name. + +### Gotchas + +- **Do not hold the store in a plain variable inside the block.** Build it through the `value` thunk + (which parks it on `$global`); never as `` inside the ``. A + `` becomes part of the block's serialized state, a live store cannot be serialized, and the + server render throws "Unable to serialize". The thunk keeps the store off that path. This is the + one real trap, and the reason the tag exists instead of a one-liner. +- **A selector *above* the block** cannot have the data yet — the block has not rendered. It shows + its default (empty / `undefined`, since the selector is null-tolerant) and then updates the instant + the block lands and the store is parked. A selector *inside* the block shows the value immediately, + with no flash. Put readers that must be correct on first paint inside the block. +- **`` above the block throws** at render, because the box is still empty at that + point — you cannot grab a streamed store from above where it is created. Read or write the streamed + store from inside the block. +- **Browser-only apps** (no server render) build the store once: the tag guards against the build + running twice on a pure client render. You generally do not need streaming in a browser-only app, + but it is safe if it appears. + +### `` attributes + +| Attribute | Required | Description | +| --------- | -------- | --------------------------------------------------------------------------- | +| `value` | yes | Function returning the bundle object (`() => ({ a, b })`), built from the awaited data. Same as ``. | +| `key` | yes | Names this bundle's box so a selector / `` can target it. Required, and must be distinct per provider — there is no shared default. | + +Reading and writing the streamed stores is unchanged: `` in `context` mode reads a +member, `` reaches the bundle to write, and typing the bundle works the same way +(see Typing the bundle). + +## Typing the bundle + +This section is about TypeScript and editor autocomplete only — it changes nothing at +runtime. + +When you read a member with `context=(c => c.user)`, TypeScript can't tell what `c` +is. The bundle's shape doesn't travel across the tag boundary in the current Marko +type tooling, so `c` comes through as `unknown` — TypeScript's "I have no idea what +this is" type. On an `unknown` value, `c.user` has no type, you get no autocomplete, +and if you try to use it as a specific shape TypeScript reports an error. + +The fix is to tell TypeScript the shape yourself, in two places: + +```marko +// 1) annotate the picker's parameter + c.user) + selector=(s => s.name) +/> + +// 2) assert the getter when writing +(ctx() as { user: typeof user; cart: typeof cart }).user.setState(...) +``` + +`typeof user` just reuses the store's own type, so you don't retype its shape. Once +annotated, `c.user` and the write are fully typed with working autocomplete. (We +looked hard for a way to make this automatic; in the current Marko toolchain it isn't, +so the annotation is the supported way. It's a typing-ergonomics gap, not a +functionality one.) + +## SSR vs client-side: things to know + +- **Server-rendered page.** Every tag paints correctly on the first render, and the + page picks up live updates after it hydrates. Nothing special to do. +- **Don't create stores inside a component under SSR.** A store built as a local + `const` inside a component renders fine on the server but is dead in the browser — + resume doesn't re-run the component's body, so the store is never recreated + client-side. Provide stores via `` or at module level instead + (see "Module-level vs per-request stores"). +- **Browser-only first paint.** When a page is mounted purely in the browser (not + server-rendered) and a selector reads from a context bundle, that selector can show + an empty value for the very first frame and then immediately correct itself. The + provider fills the bundle a beat before the selector reads it, and a built-in + recovery step catches up right away. Server-rendered pages don't show this. +- **Streaming with ``.** When a store's data arrives inside a streamed ``, + build the store right inside the block with `` (see "Streaming + data into a store"). It's born with the data on both the server and the client, so the + value paints during streaming with no flash and resumes live. The one rule: build the + store through the tag's `value` thunk — never hold it in a plain variable inside the + block, since a live store can't be serialized with the block's state. +- **What store data can cross the wire.** Anything Marko's serializer supports can sit + in the data your stores are built from: primitives (including `bigint`), plain + objects and arrays (nested fine), `Date`, `Map`, `Set`, `RegExp`, typed arrays and + `ArrayBuffer`, `URL`/`URLSearchParams`, and errors. What can't: instances of your own + classes, DOM nodes, and functions Marko didn't register (top-level/template functions + are fine). In dev, a non-serializable value fails loudly with an "Unable to serialize" + error pointing at the offending code; a production build silently drops it — so trust + the dev error, never prod, to catch this. Keep server-provided store data as plain + data (or the supported built-ins) and you'll never meet either behavior. The source + of truth is Marko's `runtime-tags/src/html/serializer.ts`. + +## TypeScript configs + +These four `tsconfig` files are about building and type-checking this package +itself — if you're only *using* `@tanstack/marko-store`, you can skip this. Each does +one job: two type-check, two emit, split across the two tools involved (`tsdown` for +the plain-TypeScript index, `marko-type-check` for the `.marko` tags). + +| File | What it does | Why it's needed | +| --- | --- | --- | +| `tsconfig.json` | The base every other config extends. Holds the shared compiler settings and maps `@tanstack/store` to `../store/src`. | One place for the defaults, used by editors and inherited by the rest. The path mapping gives live types from the sibling `store` package with no build step. | +| `tsconfig.build.json` | Used by `tsdown` to compile `src/index.ts` into `dist` (the ESM/CJS core re-export). Blanks the `@tanstack/store` path mapping. | The published index must resolve `@tanstack/store` as the real installed dependency — the way a consumer's build sees it — not as local workspace source. | +| `tsconfig.marko.json` | Used by `marko-type-check` to type-check the package, `.marko` files included, writing nothing (`noEmit`). Covers `src` and `tests`. | Checking `.marko` needs Marko-aware resolution. This is the `test:types:marko` check; a check step should never emit output. The `e2e` app is its own nested package and runs its own `marko-type-check`. | +| `tsconfig.tags.json` | Used by `marko-type-check` to *build* the tags: compiles `src/tags` into `dist/tags` (stripped `.marko` + generated `.d.marko` + the `store-bus` helper). Scoped to `src/tags` only. | Publishing ships built tags, not raw source. It's scoped so it doesn't rebuild the index (`tsdown` owns that), and unlike the check config it emits (`declaration` on, `noEmit` off). | + +Why four rather than one: two things vary and can't share a file — **check vs emit** +(a config has a single `noEmit` and `outDir`) and **which tool/scope** (`tsdown` for +the index vs `marko-type-check` for the tags, with different `include` lists). Two of +them use the same tool for opposite ends — one checks and emits nothing, the other +emits just the tags. + +## License + +MIT diff --git a/packages/marko-store/e2e/.marko-run/routes.d.ts b/packages/marko-store/e2e/.marko-run/routes.d.ts new file mode 100644 index 00000000..e9f20094 --- /dev/null +++ b/packages/marko-store/e2e/.marko-run/routes.d.ts @@ -0,0 +1,376 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; + + +declare module "@marko/run" { + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + "/client-only-first-paint": { verb: "get"; }; + "/context": { verb: "get"; }; + "/context-above": { verb: "get"; }; + "/context-multi": { verb: "get"; }; + "/context-perreq": { verb: "get"; }; + "/dup-try": { verb: "get"; }; + "/effect-order": { verb: "get"; }; + "/gate-render-only": { verb: "get"; }; + "/liveness": { verb: "get"; }; + "/multi": { verb: "get"; }; + "/ooo": { verb: "get"; }; + "/outside": { verb: "get"; }; + "/payload": { verb: "get"; }; + "/provider-collision": { verb: "get"; }; + "/streaming-fill-onmount": { verb: "get"; }; + "/streaming-fill-render": { verb: "get"; }; + "/streaming-liveness": { verb: "get"; }; + "/streaming-out-of-order": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/client-only-first-paint/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/client-only-first-paint"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/context/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/context"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/context-above/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/context-above"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/context-multi/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/context-multi"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/context-perreq/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/context-perreq"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/dup-try/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/dup-try"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/effect-order/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/effect-order"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/gate-render-only/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/gate-render-only"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/liveness/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/liveness"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/multi/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/multi"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/ooo/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/ooo"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/outside/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/outside"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/payload/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/payload"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/provider-collision/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/provider-collision"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/streaming-fill-onmount/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/streaming-fill-onmount"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/streaming-fill-render/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/streaming-fill-render"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/streaming-liveness/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/streaming-liveness"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + +declare module "../src/routes/streaming-out-of-order/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/streaming-out-of-order"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/packages/marko-store/e2e/README.md b/packages/marko-store/e2e/README.md new file mode 100644 index 00000000..41fa589d --- /dev/null +++ b/packages/marko-store/e2e/README.md @@ -0,0 +1,56 @@ +# marko-store e2e — the production gate + +A nested [@marko/run](https://github.com/marko-js/run) app that is this package's +end-to-end regression suite. It runs **against the production build, deliberately**: +`marko-run preview` (production build + serve) is the Playwright `webServer`, so every +run exercises what consumers actually get — tree-shaking, the hydration registry, +minified resume. Dev mode does none of that; it is exactly how the `sideEffects` +hydration bug shipped in a sibling package with a fully green dev-mode suite. + +## Layout + +- `src/routes//+page.marko` — one route per scenario (file-based routing). Seed + values that used to live in the old hand-rolled harness's route table are inlined + into the pages, each marked with a `Seeds inlined…` comment. +- `src/*.ts` — module-singleton stores, kept in separate modules per spec family so + specs never share mutable state. +- `*.spec.ts` — the Playwright specs, one file per scenario. +- `marko.json` — resolves the package tags from `../src/tags` (source), the same + workspace-source arrangement the examples use. + +## What the suite covers + +The migrated scenarios: from-mode selector/atom resume liveness, the context delivery +chain (provider → context-selector → store-context writes), per-request rebuild, +multi-store providers, in-order and out-of-order `` streaming liveness, +streamed-helper independence/no-flash/no-JS, error composition rules (duplicate keys, +provider collisions, context-above-the-stream), and the fill characterizations. + +New in this harness: + +- `gate-render-only.spec.ts` — **the sideEffects hydration gate.** A render-only + `` page (no handler touches any package function) is what a + production bundler tree-shakes to death under a bad `"sideEffects": false` + manifest: the page then never resumes. Verified fail→heal: with `false` the gate + fails (resume marker stays `no`); with `"sideEffects": ["**/*.marko"]` the suite is + green. Tree-shaking applies the manifest by nearest-`package.json` ownership, so + the workspace-source tag channel reproduces the consumer bug faithfully. +- `payload.spec.ts` — rich payloads (string + nested array of objects, `Date`, `Map`, + `Set`) through both provision channels, asserted in server HTML and live after + resume. +- `client-only-first-paint.spec.ts` — the real-browser half of the getter-fed + first-paint story: a browser-only mounted subtree recovers to the store value; the + observed paint history documents the blank beat when it occurs. + +One behavioral note from the migration: a pre-stream render throw (the +context-above rule) is surfaced by `@marko/run`'s node adapter as a transport-level +failure (connection dropped before any bytes), not the clean 500 the old hand-rolled +handler manufactured. The spec asserts the consumer-visible contract — request fails, +no partial body — and the throw itself stays pinned by the unit fixture. + +## Running + +``` +pnpm -C e2e test:e2e # production build + real Chromium (the gate) +pnpm -C e2e dev # dev server, for interactive debugging only +``` diff --git a/packages/marko-store/e2e/client-only-first-paint.spec.ts b/packages/marko-store/e2e/client-only-first-paint.spec.ts new file mode 100644 index 00000000..5496e6b3 --- /dev/null +++ b/packages/marko-store/e2e/client-only-first-paint.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test' + +// The real-browser half of the getter-fed first-paint story (the jsdom test pins the jsdom +// behavior and documents that the blank is real-browser-only). The page toggles a provider + +// getter-fed selector subtree on AFTER mount — a genuine browser-only first paint of the +// subtree — and records every value the consuming interpolation observes into a plain window +// array. Primary assertion: the selector RECOVERS to the correct value (the guarantee that +// matters). Documentation assertion: the recorded history shows the blank first entry when the +// beat occurs. Observed in verification: under the @marko/run PRODUCTION build this toggled +// subtree mounts with no blank at all (history === ['5']) — the beat did not manifest in this +// shape; it remains documented as possible on a pure SPA first paint (the jsdom test's note), +// and this spec stays correct for either shape while pinning the recovery guarantee. +test('a browser-only mounted getter-fed selector recovers to the store value', async ({ + page, +}) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/client-only-first-paint') + await expect(page.getByTestId('resumed')).toHaveText('yes') + + // The guarantee: the built-in recovery lands on the real value. + await expect(page.getByTestId('count')).toHaveText('5') + + const history = await page.evaluate( + () => ((globalThis as any).__paintHistory ?? []) as Array, + ) + // The history always ends at the recovered value; when the first-paint beat occurs it starts + // with the blank entry. Both shapes are legitimate; a wrong FINAL value never is. + expect(history[history.length - 1]).toBe('5') + expect(history.length).toBeGreaterThanOrEqual(1) + if (history.length > 1) { + expect(history[0]).toBe('') // the documented blank beat, when observed + } + + expect(errors).toEqual([]) +}) diff --git a/packages/marko-store/e2e/gate-render-only.spec.ts b/packages/marko-store/e2e/gate-render-only.spec.ts new file mode 100644 index 00000000..c4c2a0fa --- /dev/null +++ b/packages/marko-store/e2e/gate-render-only.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test' + +// THE SIDEEFFECTS HYDRATION GATE. A render-only page (no handler calls any +// package function) is exactly the shape a production bundler tree-shakes to death when the +// package manifest carries `"sideEffects": false`: the tag's client module registers its +// renderer and resume effects via module side effects, the shaken bundle never runs them, and +// resume dies on load ("effects[i++] is not a function"). This suite runs against the +// PRODUCTION build, so this spec is the permanent regression test for the +// `"sideEffects": ["**/*.marko"]` manifest fix. Three checks: the page resumes, zero client +// errors, and the render-only selector is genuinely LIVE afterwards (mutated externally via a +// window-exposed store — page code that touches only the store, never the tag). +test('a render-only selector page hydrates and stays live under the production build', async ({ + page, +}) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/gate-render-only') + await expect(page.getByTestId('count')).toHaveText('7') // SSR value present either way + await expect( + page.getByTestId('resumed'), + 'page did not resume — with a bad sideEffects manifest this is the failure signature', + ).toHaveText('yes') + + // Liveness: the tag's subscription must be real, not just painted HTML. + await page.evaluate(() => { + ;(globalThis as any).__gateStore.setState((s: any) => ({ count: s.count + 1 })) + }) + await expect(page.getByTestId('count')).toHaveText('8') + + expect(errors, 'zero client errors expected on the render-only page').toEqual([]) +}) diff --git a/packages/marko-store/e2e/marko.json b/packages/marko-store/e2e/marko.json new file mode 100644 index 00000000..31e34666 --- /dev/null +++ b/packages/marko-store/e2e/marko.json @@ -0,0 +1 @@ +{ "tags-dir": "../src/tags" } diff --git a/packages/marko-store/e2e/package.json b/packages/marko-store/e2e/package.json new file mode 100644 index 00000000..dfc091ff --- /dev/null +++ b/packages/marko-store/e2e/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-marko-store-e2e", + "private": true, + "type": "module", + "scripts": { + "dev": "marko-run dev", + "preview": "marko-run preview", + "test:e2e": "playwright test", + "test:types": "marko-type-check" + }, + "dependencies": { + "@marko/run": "^0.10.0", + "@tanstack/store": "workspace:*", + "marko": "^6" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@playwright/test": "^1.53.1", + "typescript": "^5.6.3", + "vite": "^6.4.2" + } +} diff --git a/packages/marko-store/e2e/payload.spec.ts b/packages/marko-store/e2e/payload.spec.ts new file mode 100644 index 00000000..e49bbff2 --- /dev/null +++ b/packages/marko-store/e2e/payload.spec.ts @@ -0,0 +1,41 @@ +import { expect, test } from '@playwright/test' + +// Rich-payload resume: strings, nested arrays of objects, Date, Map and Set crossing through +// the provider thunk channel, plus a Date through the streamed channel — all asserted in the +// server HTML AND live after resume in a real browser under the production build. This is the +// suite's answer to "every store that crosses SSR is a small object of numbers". +test('rich payloads render server-side, resume, and stay live', async ({ page }) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/payload') + await expect(page.getByTestId('resumed')).toHaveText('yes') + + await expect(page.getByTestId('title')).toHaveText('quarterly report') + await expect(page.getByTestId('rows')).toHaveText( + JSON.stringify([ + { id: 1, tags: ['a', 'b'] }, + { id: 2, tags: [] }, + ]), + ) + await expect(page.getByTestId('date')).toHaveText('2026-07-14T00:00:00.000Z') + await expect(page.getByTestId('map')).toHaveText('alpha') + await expect(page.getByTestId('set')).toHaveText('true') + await expect(page.getByTestId('streamed-date')).toHaveText('2026-01-02T03:04:05.000Z') + + // Liveness over the nested-array slice: a write through appends a row and the + // selector re-renders with it. + await page.getByTestId('add-row').click() + await expect(page.getByTestId('rows')).toHaveText( + JSON.stringify([ + { id: 1, tags: ['a', 'b'] }, + { id: 2, tags: [] }, + { id: 3, tags: ['c'] }, + ]), + ) + + expect(errors).toEqual([]) +}) diff --git a/packages/marko-store/e2e/playwright.config.ts b/packages/marko-store/e2e/playwright.config.ts new file mode 100644 index 00000000..a886d7f3 --- /dev/null +++ b/packages/marko-store/e2e/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from '@playwright/test' + +// PRODUCTION-MODE GATE, deliberately. `marko-run preview` builds the production +// bundle and serves it, so every e2e run exercises what consumers actually get: +// tree-shaking, the hydration registry, minified resume. Dev mode does none of +// that — it's exactly how the sideEffects hydration bug shipped in a sibling +// package with a fully green dev-mode suite. Use `pnpm dev` for interactive +// debugging; the gate stays production. +export default defineConfig({ + testDir: '.', + testMatch: '*.spec.ts', + timeout: 60_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:5189', + }, + webServer: { + command: 'npm run preview -- --port 5189', + url: 'http://localhost:5189', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 180_000, + }, +}) diff --git a/packages/marko-store/e2e/src/context-store.ts b/packages/marko-store/e2e/src/context-store.ts new file mode 100644 index 00000000..d3b6ca78 --- /dev/null +++ b/packages/marko-store/e2e/src/context-store.ts @@ -0,0 +1,11 @@ +// Dedicated module singleton for the context-delivery resume spec, kept separate from store.ts +// so the two specs never share mutable state. The provider parks { counter: ctxCounterStore }; +// the inline value thunk closes over this compiler-registered reference, so the store stays out +// of the serialized payload (the keystone SSR pattern). +import { createStore } from '@tanstack/store' + +export const ctxCounterStore = createStore({ count: 5 }) + +// A second, isolated pair for the multiple-stores-in-one-provider e2e. +export const ctxMultiA = createStore({ count: 5 }) +export const ctxMultiB = createStore({ count: 50 }) diff --git a/packages/marko-store/e2e/src/gate-store.ts b/packages/marko-store/e2e/src/gate-store.ts new file mode 100644 index 00000000..616a53c7 --- /dev/null +++ b/packages/marko-store/e2e/src/gate-store.ts @@ -0,0 +1,7 @@ +import { createStore } from '@tanstack/store' + +// Module singleton for the render-only hydration gate. Lives in its own module so +// the gate page's ONLY package-adjacent imports are this store and the +// autodiscovered tag — no handler-driven import keeps the tag module alive, which +// is the tree-shaking condition the gate exists to test. +export const gateStore = createStore({ count: 7 }) diff --git a/packages/marko-store/e2e/src/helper-eo.ts b/packages/marko-store/e2e/src/helper-eo.ts new file mode 100644 index 00000000..bf39b819 --- /dev/null +++ b/packages/marko-store/e2e/src/helper-eo.ts @@ -0,0 +1,22 @@ +import { createStore } from '@tanstack/store' +// Characterization aid: records, on the client only, the order of store creation vs the +// selector's first read, plus how many times the store is built. Server render has no window. +export function buildInstrumented(n: number): Record { + const w = (globalThis as unknown as { window?: Record }).window + const s = createStore({ n }) as unknown as { get: () => unknown; __read?: boolean } + if (w) { + const log = (w.__eolog as Array | undefined) ?? [] + w.__eolog = log + w.__eobuilds = ((w.__eobuilds as number) || 0) + 1 + log.push('build') + const orig = s.get.bind(s) + s.get = () => { + if (!s.__read) { + s.__read = true + log.push('read') + } + return orig() + } + } + return { counter: s } +} diff --git a/packages/marko-store/e2e/src/routes/+page.marko b/packages/marko-store/e2e/src/routes/+page.marko new file mode 100644 index 00000000..c1c91e67 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/+page.marko @@ -0,0 +1,56 @@ +import { counterStore, countAtom } from '../store' + + + + + + marko-store resume liveness probe + + + + + +
${resumed}
+ + + counterStore selector=(s) => s.count/> +
${count}
+ + + + countAtom/> +
${value}
+ + + diff --git a/packages/marko-store/e2e/src/routes/client-only-first-paint/+page.marko b/packages/marko-store/e2e/src/routes/client-only-first-paint/+page.marko new file mode 100644 index 00000000..88ea87eb --- /dev/null +++ b/packages/marko-store/e2e/src/routes/client-only-first-paint/+page.marko @@ -0,0 +1,44 @@ +import { createStore } from '@tanstack/store' + +// Client-only first-paint probe (the real-browser half of the jsdom pin in +// tests/store-context-tags.test.ts). When a provider + getter-fed selector +// subtree first paints purely in the BROWSER (no server render of the subtree), +// the selector can read before the provider has parked the bundle: one blank +// frame, then the built-in recovery corrects it. jsdom mounts synchronously and +// never shows the blank, so this page toggles the subtree on after mount to get +// a genuine browser-only first paint, and records every value the consuming +// interpolation observes into window.__paintHistory — a PLAIN array, not +// reactive state, so recording is a benign side effect of the interpolation and +// never feeds back into rendering. The spec reads the history: first entry +// blank, a later entry the recovered value. + + + client-only first paint + + + + { + const s = v === undefined || v === null ? '' : String(v) + const g = globalThis as any + g.__paintHistory = g.__paintHistory ?? [] + if (g.__paintHistory[g.__paintHistory.length - 1] !== s) g.__paintHistory.push(s) + return s + }> + +
${resumed}
+ + + ({ counter: createStore({ count: 5 }) })> + + ((ctx() as any)?.counter ?? null) + selector=(s: any) => s.count + /> +
${observe(count)}
+
+ + + diff --git a/packages/marko-store/e2e/src/routes/context-above/+page.marko b/packages/marko-store/e2e/src/routes/context-above/+page.marko new file mode 100644 index 00000000..5fdec12c --- /dev/null +++ b/packages/marko-store/e2e/src/routes/context-above/+page.marko @@ -0,0 +1,15 @@ +import { createStore } from '@tanstack/store' + + + + helper context-above + + +
${ctx() ? 'has' : 'no'}
+ setTimeout(() => r(88), 50))> + ({ counter: createStore({ n: sv }) })> +
${sv}
+
+
+ + diff --git a/packages/marko-store/e2e/src/routes/context-multi/+page.marko b/packages/marko-store/e2e/src/routes/context-multi/+page.marko new file mode 100644 index 00000000..565f2f98 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/context-multi/+page.marko @@ -0,0 +1,33 @@ +import { ctxMultiA, ctxMultiB } from '../../context-store' + + + + marko-store multi-store resume probe + + + +
${resumed}
+ + + ({ a: ctxMultiA, b: ctxMultiB })> + c.a selector=(s) => s.count/> + c.b selector=(s) => s.count/> +
${countA}
+
${countB}
+ + + +
+ + + + + diff --git a/packages/marko-store/e2e/src/routes/context-perreq/+page.marko b/packages/marko-store/e2e/src/routes/context-perreq/+page.marko new file mode 100644 index 00000000..83e22902 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/context-perreq/+page.marko @@ -0,0 +1,30 @@ +import { createStore } from '@tanstack/store' +import type { Store } from '@tanstack/store' + + + + + + marko-store per-request resume probe + + + +
${resumed}
+ + + ({ counter: createStore({ count: 42 }) })> + }) => c.counter selector=(s) => s.count/> +
${count}
+ + +
+ + diff --git a/packages/marko-store/e2e/src/routes/context/+page.marko b/packages/marko-store/e2e/src/routes/context/+page.marko new file mode 100644 index 00000000..013f3572 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/context/+page.marko @@ -0,0 +1,33 @@ +import { ctxCounterStore } from '../../context-store' + + + + marko-store context delivery resume probe + + + +
${resumed}
+ + + ({ counter: ctxCounterStore })> + c.counter selector=(s) => s.count/> +
${count}
+ + + +
+ + + + diff --git a/packages/marko-store/e2e/src/routes/dup-try/+page.marko b/packages/marko-store/e2e/src/routes/dup-try/+page.marko new file mode 100644 index 00000000..be7923c5 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/dup-try/+page.marko @@ -0,0 +1,30 @@ +import { createStore } from '@tanstack/store' + + + + helper duplicate-in-try + + + +
${resumed}
+ +
+ setTimeout(() => r(111), 60))> + ({ counter: createStore({ n: sa }) })> + c.counter selector=(s: any) => s.n/> +
${va}
+
+ + setTimeout(() => r(222), 120))> + ({ counter: createStore({ n: sb }) })> + c.counter selector=(s: any) => s.n/> +
${vb}
+
+ +
+ <@catch|err|> +
caught:${(err as Error).message}
+ +
+ + diff --git a/packages/marko-store/e2e/src/routes/effect-order/+page.marko b/packages/marko-store/e2e/src/routes/effect-order/+page.marko new file mode 100644 index 00000000..90a325d4 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/effect-order/+page.marko @@ -0,0 +1,17 @@ +import { buildInstrumented } from '../../helper-eo' + + + + helper effect-order + + + +
${resumed}
+ setTimeout(() => r(66), 120))> + buildInstrumented(sv as number)> + c.counter selector=(s: any) => s.n/> +
${ev}
+
+
+ + diff --git a/packages/marko-store/e2e/src/routes/gate-render-only/+page.marko b/packages/marko-store/e2e/src/routes/gate-render-only/+page.marko new file mode 100644 index 00000000..661f5b9f --- /dev/null +++ b/packages/marko-store/e2e/src/routes/gate-render-only/+page.marko @@ -0,0 +1,32 @@ +import { gateStore } from '../../gate-store' + +// THE SIDEEFFECTS HYDRATION GATE (render-only page). +// +// This page uses RENDER-ONLY: it displays a value and wires no +// event handler that calls any package function. Marko tags register their client +// renderers and resume effects via MODULE SIDE EFFECTS, so with a bad +// `"sideEffects": false` in the package manifest a production bundler is licensed +// to tree-shake the tag's client module out of exactly this kind of page — the +// server HTML's resume payload then references registrations that never ran and +// hydration dies on load (`effects[i++] is not a function`). Dev servers don't +// tree-shake and handler-wired pages retain the modules, which is how the bug +// stays invisible everywhere else; this page, under the PRODUCTION build the +// harness runs, is the one place it must show up. The spec asserts resume, zero +// page errors, and post-resume liveness driven externally (window.__gateStore, +// exposed below — page code that touches only the store, never the tag). + + + + render-only hydration gate + + + +
${resumed}
+ + gateStore selector=(s: any) => s.count/> +
${count}
+ + diff --git a/packages/marko-store/e2e/src/routes/liveness/+page.marko b/packages/marko-store/e2e/src/routes/liveness/+page.marko new file mode 100644 index 00000000..920360b4 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/liveness/+page.marko @@ -0,0 +1,20 @@ +import { createStore } from '@tanstack/store' + + + + helper liveness + + + +
${resumed}
+ setTimeout(() => r(77), 150))> + ({ counter: createStore({ n: slice }) })> + c.counter selector=(s: any) => s.n/> +
${value}
+
+
+ + + diff --git a/packages/marko-store/e2e/src/routes/multi/+page.marko b/packages/marko-store/e2e/src/routes/multi/+page.marko new file mode 100644 index 00000000..aa642c8f --- /dev/null +++ b/packages/marko-store/e2e/src/routes/multi/+page.marko @@ -0,0 +1,25 @@ +import { createStore } from '@tanstack/store' + + + + helper multi + + + +
${resumed}
+ setTimeout(() => r(11), 150))> + ({ counter: createStore({ n: sa }) })> + c.counter selector=(s: any) => s.n/> +
${va}
+
+
+ setTimeout(() => r(22), 70))> + ({ counter: createStore({ n: sb }) })> + c.counter selector=(s: any) => s.n/> +
${vb}
+
+
+ + + + diff --git a/packages/marko-store/e2e/src/routes/ooo/+page.marko b/packages/marko-store/e2e/src/routes/ooo/+page.marko new file mode 100644 index 00000000..f44de00c --- /dev/null +++ b/packages/marko-store/e2e/src/routes/ooo/+page.marko @@ -0,0 +1,29 @@ +import { createStore } from '@tanstack/store' + + + + helper out-of-order + + + +
${resumed}
+ + setTimeout(() => r(33), 220))> + ({ counter: createStore({ n: ss }) })> + c.counter selector=(s: any) => s.n/> +
${vs}
+
+
+ <@placeholder>
loading-slow
+
+ + setTimeout(() => r(44), 50))> + ({ counter: createStore({ n: sf }) })> + c.counter selector=(s: any) => s.n/> +
${vf}
+
+
+ <@placeholder>
loading-fast
+
+ + diff --git a/packages/marko-store/e2e/src/routes/outside/+page.marko b/packages/marko-store/e2e/src/routes/outside/+page.marko new file mode 100644 index 00000000..e5a0a69b --- /dev/null +++ b/packages/marko-store/e2e/src/routes/outside/+page.marko @@ -0,0 +1,19 @@ +import { createStore } from '@tanstack/store' + + + + helper selector-outside + + + +
${resumed}
+ c == null ? null : c.counter selector=(s: any) => s == null ? -1 : s.n/> +
${before == null ? 'undef' : before}
+ setTimeout(() => r(55), 150))> + ({ counter: createStore({ n: sv }) })> + c.counter selector=(s: any) => s.n/> +
${inside}
+
+
+ + diff --git a/packages/marko-store/e2e/src/routes/payload/+page.marko b/packages/marko-store/e2e/src/routes/payload/+page.marko new file mode 100644 index 00000000..6494ee9d --- /dev/null +++ b/packages/marko-store/e2e/src/routes/payload/+page.marko @@ -0,0 +1,56 @@ +import { createStore } from '@tanstack/store' +import type { Store } from '@tanstack/store' + +// Rich-payload resume probe. Every other store crossing SSR in this suite is a +// small object of numbers; this page proves the tags stay live over the payloads +// real apps carry, through both provision channels: +// - : a string + a NESTED ARRAY OF OBJECTS, plus Date/Map/Set +// (all supported built-ins per Marko's serializer) built in the value thunk. +// - : a Date arriving inside an . +// The data below is inline plain data — exactly what a route handler would pass. + + + rich payload resume + + + +
${resumed}
+ + ({ + report: createStore({ + title: 'quarterly report', + rows: [ + { id: 1, tags: ['a', 'b'] }, + { id: 2, tags: [] }, + ], + when: new Date('2026-07-14T00:00:00.000Z'), + labels: new Map([['a', 'alpha']]), + ids: new Set([1, 2, 3]), + }), + })> + c.report selector=(s: any) => s.title/> +
${title}
+ c.report selector=(s: any) => JSON.stringify(s.rows)/> +
${rows}
+ c.report selector=(s: any) => (s.when instanceof Date ? s.when.toISOString() : 'not-a-date')/> +
${when}
+ c.report selector=(s: any) => (s.labels instanceof Map ? String(s.labels.get('a')) : 'not-a-map')/> +
${label}
+ c.report selector=(s: any) => (s.ids instanceof Set ? String(s.ids.has(2)) : 'not-a-set')/> +
${hasTwo}
+ + + +
+ + void) => setTimeout(() => r(new Date('2026-01-02T03:04:05.000Z')), 80))> + ({ clock: createStore({ when }) })> + c.clock selector=(s: any) => (s.when instanceof Date ? s.when.toISOString() : 'not-a-date')/> +
${streamed}
+
+
+ + diff --git a/packages/marko-store/e2e/src/routes/provider-collision/+page.marko b/packages/marko-store/e2e/src/routes/provider-collision/+page.marko new file mode 100644 index 00000000..1161fc18 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/provider-collision/+page.marko @@ -0,0 +1,25 @@ +import { createStore } from '@tanstack/store' + + + + helper provider-collision + + + +
${resumed}
+ ({ counter: createStore({ n: 5 }) })> + c.counter selector=(s: any) => s.n/> +
${u}
+ + setTimeout(() => r(9), 60))> + ({ counter: createStore({ n: late }) })> +
${late}
+
+
+ <@catch|err|> +
caught:${(err as Error).message}
+ +
+
+ + diff --git a/packages/marko-store/e2e/src/routes/streaming-fill-onmount/+page.marko b/packages/marko-store/e2e/src/routes/streaming-fill-onmount/+page.marko new file mode 100644 index 00000000..adc83915 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/streaming-fill-onmount/+page.marko @@ -0,0 +1,34 @@ +import { streamFillOnmountStore } from '../../streaming-store' + + + + streaming fill: onMount (client-only, flash) + + + ({ counter: streamFillOnmountStore })> + c.counter selector=(s) => s.count/> +
${shellCount}
+ + setTimeout(() => r('A'), 100))> + + ({ count: 99 })) + }> +
${aResumed}
+ c.counter selector=(s) => s.count/> +
${aCount}
+
+
+ + + + diff --git a/packages/marko-store/e2e/src/routes/streaming-fill-render/+page.marko b/packages/marko-store/e2e/src/routes/streaming-fill-render/+page.marko new file mode 100644 index 00000000..f3bd1613 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/streaming-fill-render/+page.marko @@ -0,0 +1,34 @@ +import { streamFillRenderStore } from '../../streaming-store' + + + + streaming fill: render-time side effect (server-only) + + + ({ counter: streamFillRenderStore })> + c.counter selector=(s) => s.count/> +
${shellCount}
+ + setTimeout(() => r('A'), 100))> + + +
${aResumed}
+ + ${(streamFillRenderStore.setState(() => ({ count: 99 })), '')} + c.counter selector=(s) => s.count/> +
${aCount}
+
+
+ + + + diff --git a/packages/marko-store/e2e/src/routes/streaming-liveness/+page.marko b/packages/marko-store/e2e/src/routes/streaming-liveness/+page.marko new file mode 100644 index 00000000..deee1480 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/streaming-liveness/+page.marko @@ -0,0 +1,39 @@ +import { streamLivenessStore } from '../../streaming-store' + + + + marko-store streaming liveness (in-order) + + + ({ counter: streamLivenessStore })> + c.counter selector=(s) => s.count/> +
${shellCount}
+ + setTimeout(() => r('A'), 100))> + + +
${aResumed}
+ c.counter selector=(s) => s.count/> +
${aCount}
+ + + setTimeout(() => r('B'), 500))> + + +
${bResumed}
+ c.counter selector=(s) => s.count/> +
${bCount}
+ +
+ + + + diff --git a/packages/marko-store/e2e/src/routes/streaming-out-of-order/+page.marko b/packages/marko-store/e2e/src/routes/streaming-out-of-order/+page.marko new file mode 100644 index 00000000..1004c6f8 --- /dev/null +++ b/packages/marko-store/e2e/src/routes/streaming-out-of-order/+page.marko @@ -0,0 +1,36 @@ +import { streamOooStore } from '../../streaming-store' + + + + marko-store streaming liveness (out-of-order) + + + ({ counter: streamOooStore })> + c.counter selector=(s) => s.count/> +
${shellCount}
+ + + <@placeholder>
loading
+ setTimeout(() => r('X'), 500))> + + +
${xResumed}
+ c.counter selector=(s) => s.count/> +
${xCount}
+ +
+
+ +
FOOTER
+ + + diff --git a/packages/marko-store/e2e/src/store.ts b/packages/marko-store/e2e/src/store.ts new file mode 100644 index 00000000..7a0485e7 --- /dev/null +++ b/packages/marko-store/e2e/src/store.ts @@ -0,0 +1,8 @@ +// Module singletons so the page's inline thunks (() => counterStore, () => countAtom) close +// over stable, compiler-registered references -- the keystone pattern the SSR tests proved +// keeps the store out of the serialized resume payload. createStore/createAtom come straight +// from @tanstack/store (the same module the tags import, so there is a single store instance). +import { createAtom, createStore } from '@tanstack/store' + +export const counterStore = createStore({ count: 5 }) +export const countAtom = createAtom(7) diff --git a/packages/marko-store/e2e/src/streaming-store.ts b/packages/marko-store/e2e/src/streaming-store.ts new file mode 100644 index 00000000..28a9c8a8 --- /dev/null +++ b/packages/marko-store/e2e/src/streaming-store.ts @@ -0,0 +1,19 @@ +// Module singletons for the Phase 3 streaming e2e pages. Kept separate from store.ts and +// context-store.ts so these specs never share mutable state with the Phase 2a/2b specs (the same +// isolation rule the existing fixtures follow). The inline value thunks in the pages close over +// these compiler-registered references, so the store stays out of the serialized resume payload +// -- the keystone SSR pattern proved in earlier phases. +import { createStore } from '@tanstack/store' + +// Core-liveness + out-of-order pages: a read-only seed, mutated only by the external-inc button, +// so there is no per-request fill to leak across a shared (reuseExistingServer) dev server. +export const streamLivenessStore = createStore({ count: 5 }) +export const streamOooStore = createStore({ count: 7 }) + +// Fill CHARACTERIZATION pages: constructed at 0 so a filled value of 99 is distinguishable from +// the client-side construction default. If a streamed subtree reverted to the empty client store +// it would read 0, not 99 -- that gap is the entire point of the characterization. The specs +// assert CLIENT-side outcomes, which are deterministic regardless of any server-side leak (the +// client store is a fresh module instance per navigation). +export const streamFillRenderStore = createStore({ count: 0 }) +export const streamFillOnmountStore = createStore({ count: 0 }) diff --git a/packages/marko-store/e2e/store-context-resume.spec.ts b/packages/marko-store/e2e/store-context-resume.spec.ts new file mode 100644 index 00000000..6cbc0d52 --- /dev/null +++ b/packages/marko-store/e2e/store-context-resume.spec.ts @@ -0,0 +1,88 @@ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +// jsdom cannot replay Marko's resume, so the context delivery path's wake-up behavior is +// proven here, in real Chromium, against the real @marko/vite dev-server resume pipeline. +// A store-independent resume marker (data-testid="resumed") confirms the page resumed before +// the tag output is judged: "yes" only after a client onMount runs. +const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]|favicon/i.test(e) + +function trackErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + return () => errors.filter((e) => !isDevServerNoise(e)) +} + +test('context chain resumes and stays live (selector seeds, external + context writes propagate)', async ({ + page, +}) => { + const realErrors = trackErrors(page) + await page.goto('/context') + + // The empty-shelf moment is fatal without the guard; this asserts the page actually resumes. + await expect( + page.getByTestId('resumed'), + 'page did not resume -- client JS did not run', + ).toHaveText('yes') + + // Seeded via the context picker (createStore({ count: 5 })), no blank. + await expect(page.getByTestId('selector-count')).toHaveText('5') + + // Live: an external store mutation propagates to the context-fed selector after resume. + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('selector-count')).toHaveText('6') + + // Live: a write (grab the store in a handler, setState) propagates. + await page.getByTestId('ctx-reset').click() + await expect(page.getByTestId('selector-count')).toHaveText('0') + + expect(realErrors(), 'client errors during resume').toEqual([]) +}) + +test('per-request data rebuilds from the serialized payload on resume and stays live', async ({ + page, +}) => { + const realErrors = trackErrors(page) + await page.goto('/context-perreq') + + await expect(page.getByTestId('resumed')).toHaveText('yes') + // The provider did createStore(input.initial={count:42}); the client rebuilds from the + // serialized data, not a serialized live store. + await expect(page.getByTestId('selector-count')).toHaveText('42') + + await page.getByTestId('ctx-inc').click() + await expect(page.getByTestId('selector-count')).toHaveText('43') + + expect(realErrors(), 'client errors during resume').toEqual([]) +}) + +test('multiple stores in one provider resume independently (read, isolation, and a context write)', async ({ + page, +}) => { + const realErrors = trackErrors(page) + await page.goto('/context-multi') + + await expect(page.getByTestId('resumed')).toHaveText('yes') + // each selector seeds from its own bundle member + await expect(page.getByTestId('selector-a')).toHaveText('5') + await expect(page.getByTestId('selector-b')).toHaveText('50') + + // mutating one store moves only its own selector + await page.getByTestId('external-inc-a').click() + await expect(page.getByTestId('selector-a')).toHaveText('6') + await expect(page.getByTestId('selector-b')).toHaveText('50') + + await page.getByTestId('external-inc-b').click() + await expect(page.getByTestId('selector-b')).toHaveText('51') + await expect(page.getByTestId('selector-a')).toHaveText('6') + + // a write targets only the named member + await page.getByTestId('ctx-reset-a').click() + await expect(page.getByTestId('selector-a')).toHaveText('0') + await expect(page.getByTestId('selector-b')).toHaveText('51') + + expect(realErrors(), 'client errors during resume').toEqual([]) +}) diff --git a/packages/marko-store/e2e/store-resume-liveness.spec.ts b/packages/marko-store/e2e/store-resume-liveness.spec.ts new file mode 100644 index 00000000..fb2de1b5 --- /dev/null +++ b/packages/marko-store/e2e/store-resume-liveness.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test' + +// The Phase 2a proof: in a REAL browser, does an SSR'd page with and +// resume and stay LIVE, or render once on the server and go inert? jsdom cannot +// answer this honestly (it was the reason this round-trip was deferred from the vitest suite), +// so it runs in real Chromium. +// +// A store-independent resume marker (data-testid="resumed") confirms the page resumed before we +// judge the tags: "no" on the server, "yes" after a client onMount. That lets a stale value be +// attributed correctly: +// resumed=yes + stale value => the page resumed but a tag is inert (the failure we care about) +// resumed=no => the page never resumed (setup issue); the result is inconclusive + +test("an SSR'd store page resumes and stays live in a real browser", async ({ + page, +}) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/') + + // Precondition: the page actually resumed (client JS ran). Auto-waits up to the timeout. + await expect( + page.getByTestId('resumed'), + 'page did not resume -- client JS did not run; result inconclusive', + ).toHaveText('yes') + + // Server-seeded values are present after resume (createStore({ count: 5 }) / createAtom(7)). + await expect(page.getByTestId('selector-count')).toHaveText('5') + await expect(page.getByTestId('atom-value')).toHaveText('7') + + // Decisive 1 -- selector subscription is LIVE post-resume: a store mutation triggered from + // OUTSIDE any tag (setState on the module singleton) propagates to the rendered selection. + await page.getByTestId('external-inc').click() + await expect( + page.getByTestId('selector-count'), + 'selector did not react to an external store update after resume -- tag inert', + ).toHaveText('6') + + // Decisive 2 -- atom two-way write-back is LIVE post-resume: mutating the bound value flows + // through the tag's valueChange into countAtom.set and re-renders. + await page.getByTestId('atom-inc').click() + await expect( + page.getByTestId('atom-value'), + 'atom did not write back / re-render after resume -- tag inert', + ).toHaveText('8') + + // Keystone: no serialization crash or uncaught client error during render/resume. + // Vite's dev client opens an HMR WebSocket even in middleware mode with hmr disabled, and in + // a test there is no WS server, so it logs connection failures. That noise is unrelated to + // store serialization or resume, so it is filtered out before the assertion -- a real tag or + // serialization error would not match these patterns and would still fail the test. + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + const realErrors = errors.filter((e) => !isDevServerNoise(e)) + expect(realErrors, 'client errors during resume').toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-fill.spec.ts b/packages/marko-store/e2e/streaming-fill.spec.ts new file mode 100644 index 00000000..58130b40 --- /dev/null +++ b/packages/marko-store/e2e/streaming-fill.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test' + +// CHARACTERIZATION -- not recommended-pattern tests. These pin how the two naive progressive-fill +// mechanisms behave on resume, which is WHY Phase 3's no-flash data delivery serializes the +// awaited data across the wire instead of relying on a setState inside the await body. +// +// Both assertions are CLIENT-side outcomes and are deterministic regardless of the shared dev +// server's state: a full navigation re-initializes the page's module store, so the client store +// starts at its construction default (0) on every load. + +test('render-time setState fill is server-only -- the client store stays at its default', async ({ + page, +}) => { + await page.goto('/streaming-fill-render') + + await expect(page.getByTestId('a-resumed')).toHaveText('yes') + + // The render-time side effect filled the SERVER store to 99, but it is not replayed on resume, + // so the live selector reflects the fresh client store: 0, not 99. + await expect( + page.getByTestId('a-count'), + 'render-time fill unexpectedly carried to the client store', + ).toHaveText('0') + + // Still a live subscription: an external mutation moves it (0 -> 1). + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('a-count')).toHaveText('1') +}) + +test('onMount fill is client-only -- the value reaches the client store (with a server flash)', async ({ + page, +}) => { + await page.goto('/streaming-fill-onmount') + + await expect(page.getByTestId('a-resumed')).toHaveText('yes') + + // onMount ran on the client and filled the store; the live selector shows it: 99. (The server + // rendered 0 first -- the flash -- which is the trade-off this mechanism makes.) + await expect( + page.getByTestId('a-count'), + 'onMount fill did not reach the client store', + ).toHaveText('99') + + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('a-count')).toHaveText('100') +}) diff --git a/packages/marko-store/e2e/streaming-helper-context-above.spec.ts b/packages/marko-store/e2e/streaming-helper-context-above.spec.ts new file mode 100644 index 00000000..1c03731d --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-context-above.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test' + +// Composition rule: grabs the bundle off the shelf AT RENDER and throws if the +// shelf is empty. Placed ABOVE the streamed helper, the box is still empty at that point, so it +// throws during the shell render. The old hand-rolled harness manufactured a clean 500 from that +// throw; @marko/run's node adapter instead fails the request at the transport level (the +// connection is dropped before any bytes flush). This spec asserts the CONSUMER-VISIBLE contract +// either way: the request fails, and the streamed value never reaches a body. The throw itself +// stays pinned handler-independently by the unit fixture +// (ssr-stream-ctx-above → "without a above it"). +test('store-context above the streamed helper fails fast', async ({ request }) => { + let failed = false + let body = '' + try { + const res = await request.get('/context-above') + body = await res.text() + failed = res.status() >= 500 + } catch { + failed = true // transport-level failure (current @marko/run behavior) + } + expect(failed, 'the request must fail — no partial success').toBe(true) + expect(body).not.toContain('88') +}) diff --git a/packages/marko-store/e2e/streaming-helper-dup-try.spec.ts b/packages/marko-store/e2e/streaming-helper-dup-try.spec.ts new file mode 100644 index 00000000..a7c8f2db --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-dup-try.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test' + +// The throw case: two helpers given the SAME key is a programming error. The second build throws +// at render ("Duplicate ... distinct key"). Because the helper runs inside a late , the +// throw surfaces mid-stream; the idiomatic containment is Marko's /<@catch>, which catches +// it and renders the fallback with the message. This proves both that the guard fires and that +// it is containable the same way as any streamed error. +test('duplicate key throws and is caught by /<@catch>', async ({ page }) => { + await page.goto('/dup-try') + const caught = page.getByTestId('dup-caught') + await expect(caught, 'duplicate-key throw was not raised/caught').toBeVisible() + await expect(caught).toContainText('stream-store-provider') + await expect(caught).toContainText('distinct key') +}) diff --git a/packages/marko-store/e2e/streaming-helper-effect-order.spec.ts b/packages/marko-store/e2e/streaming-helper-effect-order.spec.ts new file mode 100644 index 00000000..67fa1184 --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-effect-order.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +// Characterization: within the streamed block the store is built before the selector reads it, +// and exactly once on the client. This is why there is no flash; the bus is the backstop if the +// order ever changed. Asserting it makes a regression in resume effect-ordering visible. +test('store is built before the selector reads, exactly once on the client', async ({ page }) => { + await page.goto('/effect-order') + await expect(page.getByTestId('resumed')).toHaveText('yes') + await expect(page.getByTestId('count-eo')).toHaveText('66') + + const probe = await page.evaluate(() => ({ + log: (window as unknown as { __eolog?: Array }).__eolog ?? null, + builds: (window as unknown as { __eobuilds?: number }).__eobuilds ?? null, + })) + expect(probe.log, 'store was read before it was built (would flash)').toEqual(['build', 'read']) + expect(probe.builds, 'store built more than once on the client').toBe(1) +}) diff --git a/packages/marko-store/e2e/streaming-helper-liveness.spec.ts b/packages/marko-store/e2e/streaming-helper-liveness.spec.ts new file mode 100644 index 00000000..fdd698da --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-liveness.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test' + +// Core proof for the streamed born-with-data helper: an SSR'd living +// inside a late must (1) cross the awaited value to the browser so the store rebuilds +// with it, (2) show it with no flash, and (3) stay live after resume. The resume marker is +// store-independent so a stale value can be attributed correctly. +test('streamed helper store crosses, shows no flash, and stays live', async ({ page }) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()) }) + + // Catch any flash: record every distinct value the count shows from first paint onward. + const seen: Array = [] + await page.exposeFunction('__record', (v: string) => { + if (seen.length === 0 || seen[seen.length - 1] !== v) seen.push(v) + }) + + await page.goto('/liveness', { waitUntil: 'commit' }) + // Sample the count tightly while the stream lands, to expose a build-then-patch flash. + await page.evaluate(async () => { + for (let i = 0; i < 60; i++) { + const el = document.querySelector('[data-testid=count]') + if (el) await (window as unknown as { __record: (v: string) => void }).__record((el.textContent || '').trim()) + await new Promise((r) => setTimeout(r, 16)) + } + }) + + await expect(page.getByTestId('resumed'), 'page did not resume; result inconclusive').toHaveText('yes') + // The streamed server value is present and correct after resume. + await expect(page.getByTestId('count')).toHaveText('77') + // No flash: the only non-empty value ever shown was the final one. + expect(seen.filter((v) => v !== ''), 'a transient (flash) value appeared before the final value').toEqual(['77']) + + // Live: an external store mutation (outside any tag) propagates to the selection after resume. + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('count'), 'selector inert after resume').toHaveText('78') + + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect(errors.filter((e) => !isDevServerNoise(e)), 'client errors during resume').toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-helper-multi.spec.ts b/packages/marko-store/e2e/streaming-helper-multi.spec.ts new file mode 100644 index 00000000..0f308702 --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-multi.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test' + +// Two streamed helpers with distinct keys must not collide: both resume live and an external +// mutation moves only its own store. +test('multiple streamed helpers with distinct keys stay independent and live', async ({ page }) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()) }) + + await page.goto('/multi') + await expect(page.getByTestId('resumed')).toHaveText('yes') + await expect(page.getByTestId('count-a')).toHaveText('11') + await expect(page.getByTestId('count-b')).toHaveText('22') + + await page.getByTestId('inc-a').click() + await expect(page.getByTestId('count-a')).toHaveText('12') + await expect(page.getByTestId('count-b'), 'inc-a leaked into the other store').toHaveText('22') + + await page.getByTestId('inc-b').click() + await expect(page.getByTestId('count-b')).toHaveText('23') + await expect(page.getByTestId('count-a'), 'inc-b leaked into the other store').toHaveText('12') + + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect(errors.filter((e) => !isDevServerNoise(e))).toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-helper-no-js.spec.ts b/packages/marko-store/e2e/streaming-helper-no-js.spec.ts new file mode 100644 index 00000000..78d1aad7 --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-no-js.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/test' + +// SSR-only graceful: with JavaScript disabled the page never resumes, but the streamed value is +// already in the server HTML, so a no-JS visitor still sees it. +test.use({ javaScriptEnabled: false }) +test('streamed value is present in server HTML without JavaScript', async ({ page }) => { + await page.goto('/liveness') + await expect(page.getByTestId('count')).toHaveText('77') + // Confirms JS truly did not run (so the value above came from SSR, not resume). + await expect(page.getByTestId('resumed')).toHaveText('no') +}) diff --git a/packages/marko-store/e2e/streaming-helper-ooo.spec.ts b/packages/marko-store/e2e/streaming-helper-ooo.spec.ts new file mode 100644 index 00000000..07fcf95e --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-ooo.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +// Out-of-order streaming (/<@placeholder>, slow block before a fast one) must still land +// each helper's value correctly with no crash. +test('out-of-order streamed helpers each land correct', async ({ page }) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()) }) + + await page.goto('/ooo') + await expect(page.getByTestId('resumed')).toHaveText('yes') + await expect(page.getByTestId('count-fast')).toHaveText('44') + await expect(page.getByTestId('count-slow')).toHaveText('33') + + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect(errors.filter((e) => !isDevServerNoise(e))).toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-helper-outside.spec.ts b/packages/marko-store/e2e/streaming-helper-outside.spec.ts new file mode 100644 index 00000000..e747422d --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-outside.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +// A selector placed ABOVE the await (before the helper has built its store) cannot have the +// value yet: it shows its default, then updates once the streamed block lands and rings the bus. +// A selector INSIDE the block shows the value immediately (no flash). +test('selector above the await shows default then updates; inside shows no flash', async ({ page }) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()) }) + + await page.goto('/outside', { waitUntil: 'commit' }) + // At first paint the outside selector has no store -> its null-tolerant default. + await expect(page.getByTestId('before-count')).toHaveText('undef') + // After the streamed block lands, the outside selector recovers via the bus. + await expect(page.getByTestId('resumed')).toHaveText('yes') + await expect(page.getByTestId('inside-count')).toHaveText('55') + await expect(page.getByTestId('before-count'), 'outside selector never recovered').toHaveText('55') + + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect(errors.filter((e) => !isDevServerNoise(e))).toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-helper-provider-collision.spec.ts b/packages/marko-store/e2e/streaming-helper-provider-collision.spec.ts new file mode 100644 index 00000000..73393c03 --- /dev/null +++ b/packages/marko-store/e2e/streaming-helper-provider-collision.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test' + +// The shared-sticker proof: and on the SAME key must +// detect each other (they both own one box on the shelf) and throw, rather than silently clobber. +// The provider claims the key during the shell render; the streamed helper, rendering later inside +// the , sees the same "$owner" marker and throws, caught by /<@catch>. The provider's +// own selector keeps working, proving only the colliding streamed branch failed. +test('a stream-store-provider sharing a key with a store-provider is caught', async ({ page }) => { + await page.goto('/provider-collision') + await expect(page.getByTestId('resumed')).toHaveText('yes') + await expect(page.getByTestId('provider-count'), 'the provider itself should be unaffected').toHaveText('5') + + const caught = page.getByTestId('collision-caught') + await expect(caught, 'provider/helper key collision was not caught').toBeVisible() + await expect(caught).toContainText('Duplicate') + await expect(caught).toContainText('distinct key') + + // The colliding streamed branch never rendered its content. + await expect(page.getByTestId('never')).toHaveCount(0) +}) diff --git a/packages/marko-store/e2e/streaming-liveness.spec.ts b/packages/marko-store/e2e/streaming-liveness.spec.ts new file mode 100644 index 00000000..49c1d650 --- /dev/null +++ b/packages/marko-store/e2e/streaming-liveness.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from '@playwright/test' + +// Phase 3 core proof, in a REAL browser: a store delivered via stays LIVE inside +// progressively-streamed subtrees -- including a LATE subtree (500ms) -- after resume. +// Per-subtree resume markers (a-resumed / b-resumed) confirm each streamed branch's client JS ran +// before we judge liveness, so a stale value is attributed correctly (resumed=yes + stale => the +// branch resumed but the selector is inert, which is the failure we care about). +// +// Bare streams in document order; this spec asserts liveness across that streaming, not +// the order itself (see streaming-out-of-order.spec.ts for order). + +test('a store stays live across progressive streaming, including a late subtree', async ({ + page, +}) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/streaming-liveness') + + // Each streamed subtree resumed (its client onMount ran), including the late 500ms one. + await expect( + page.getByTestId('a-resumed'), + 'await A subtree did not resume', + ).toHaveText('yes') + await expect( + page.getByTestId('b-resumed'), + 'late await B subtree did not resume -- result inconclusive', + ).toHaveText('yes') + + // The seeded value reached the shell and both await subtrees (createStore({ count: 5 })). + await expect(page.getByTestId('shell-count')).toHaveText('5') + await expect(page.getByTestId('a-count')).toHaveText('5') + await expect(page.getByTestId('b-count')).toHaveText('5') + + // Decisive: a store mutation from OUTSIDE any tag (setState on the module singleton) after full + // load moves the shell AND the late subtree together -- selectors inside streamed branches are + // live subscriptions, not inert server snapshots. + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('shell-count')).toHaveText('6') + await expect( + page.getByTestId('a-count'), + 'await A selector did not react to an external update -- tag inert', + ).toHaveText('6') + await expect( + page.getByTestId('b-count'), + 'late await B selector did not react to an external update -- tag inert', + ).toHaveText('6') + + // No serialization crash or uncaught client error during render/resume. Vite's dev client opens + // an HMR WebSocket even with hmr disabled in middleware mode, and there is no WS server in a + // test, so it logs connection failures; that noise is filtered out. A real tag/serialization + // error would not match these patterns and would still fail the assertion. + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect( + errors.filter((e) => !isDevServerNoise(e)), + 'client errors during resume', + ).toEqual([]) +}) diff --git a/packages/marko-store/e2e/streaming-out-of-order.spec.ts b/packages/marko-store/e2e/streaming-out-of-order.spec.ts new file mode 100644 index 00000000..fbf8938a --- /dev/null +++ b/packages/marko-store/e2e/streaming-out-of-order.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test' + +// Out-of-order streaming proof, in a REAL browser. Content wrapped in with an <@placeholder> +// streams out of document order: the placeholder renders in place now, content after it flushes +// early, and the real content is reordered into place by a client script when its promise +// resolves. (Confirmed at the wire level: the footer, which is after the awaited block in the +// document, arrives in the stream BEFORE the awaited content.) This spec asserts a store delivered +// via stays LIVE inside that reordered subtree after it resumes. + +test('a store stays live inside an out-of-order (try/placeholder) reordered subtree', async ({ + page, +}) => { + const errors: Array = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console.error: ' + m.text()) + }) + + await page.goto('/streaming-out-of-order') + + // The footer (after the awaited block in the document) is present immediately -- it did not wait + // for the slow awaited content, which is the out-of-order behavior. + await expect(page.getByTestId('footer')).toHaveText('FOOTER') + + // The reordered subtree eventually resumes: its client onMount ran after it was moved into place. + await expect( + page.getByTestId('x-resumed'), + 'reordered subtree did not resume -- result inconclusive', + ).toHaveText('yes') + await expect(page.getByTestId('x-count')).toHaveText('7') + + // Decisive: an external store mutation after full load moves the shell AND the reordered subtree + // together -- the selector inside the out-of-order branch is a live subscription. + await page.getByTestId('external-inc').click() + await expect(page.getByTestId('shell-count')).toHaveText('8') + await expect( + page.getByTestId('x-count'), + 'reordered-subtree selector did not react to an external update -- tag inert', + ).toHaveText('8') + + const isDevServerNoise = (e: string) => /websocket|ws:\/\/|\[vite\]/i.test(e) + expect( + errors.filter((e) => !isDevServerNoise(e)), + 'client errors during resume', + ).toEqual([]) +}) diff --git a/packages/marko-store/e2e/tsconfig.json b/packages/marko-store/e2e/tsconfig.json new file mode 100644 index 00000000..f9ed3868 --- /dev/null +++ b/packages/marko-store/e2e/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src", + "*.spec.ts", + "playwright.config.ts", + "vite.config.ts", + "../src/tags/**/*" + ] +} diff --git a/packages/marko-store/e2e/vite.config.ts b/packages/marko-store/e2e/vite.config.ts new file mode 100644 index 00000000..d9f966e9 --- /dev/null +++ b/packages/marko-store/e2e/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import marko from '@marko/run/vite' + +export default defineConfig({ + plugins: [marko() as any], +}) diff --git a/packages/marko-store/eslint.config.js b/packages/marko-store/eslint.config.js new file mode 100644 index 00000000..a0cfcabc --- /dev/null +++ b/packages/marko-store/eslint.config.js @@ -0,0 +1,13 @@ +// @ts-check + +import { defineConfig } from 'eslint/config' +import rootConfig from '../../eslint.config.js' + +export default defineConfig([ + ...rootConfig, + { + // The e2e @marko/run app generates route types and a production build in + // place; neither is source. + ignores: ['e2e/.marko-run/**', 'e2e/dist/**'], + }, +]) diff --git a/packages/marko-store/marko.json b/packages/marko-store/marko.json new file mode 100644 index 00000000..bc929c34 --- /dev/null +++ b/packages/marko-store/marko.json @@ -0,0 +1 @@ +{ "exports": "./dist/tags" } diff --git a/packages/marko-store/package.json b/packages/marko-store/package.json new file mode 100644 index 00000000..77d90538 --- /dev/null +++ b/packages/marko-store/package.json @@ -0,0 +1,77 @@ +{ + "name": "@tanstack/marko-store", + "version": "0.11.0", + "description": "Framework agnostic type-safe store w/ reactive framework adapters", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/store.git", + "directory": "packages/marko-store" + }, + "homepage": "https://tanstack.com/store", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "store", + "marko", + "typescript" + ], + "scripts": { + "clean": "premove ./dist ./coverage", + "test:eslint": "eslint ./src ./tests ./e2e", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\" && pnpm run test:types:marko", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js", + "test:types:ts59": "tsc", + "test:types:marko": "marko-type-check -p tsconfig.marko.json", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict", + "test:e2e": "pnpm -C e2e test:e2e", + "build": "tsdown --tsconfig tsconfig.build.json && marko-type-check -p tsconfig.tags.json" + }, + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json", + "./marko.json": "./marko.json", + "./dist/tags/*": "./dist/tags/*" + }, + "sideEffects": [ + "**/*.marko" + ], + "files": [ + "dist", + "marko.json", + "README.md" + ], + "dependencies": { + "@tanstack/store": "workspace:*" + }, + "devDependencies": { + "@marko/language-tools": "^2.6.0", + "@marko/type-check": "^3.1.0", + "@marko/vite": "^6", + "@testing-library/dom": "^10.4.0", + "marko": "^6" + }, + "peerDependencies": { + "marko": "^6" + } +} diff --git a/packages/marko-store/src/index.ts b/packages/marko-store/src/index.ts new file mode 100644 index 00000000..877b23ea --- /dev/null +++ b/packages/marko-store/src/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/store' diff --git a/packages/marko-store/src/tags/store-atom.marko b/packages/marko-store/src/tags/store-atom.marko new file mode 100644 index 00000000..7bb0ff39 --- /dev/null +++ b/packages/marko-store/src/tags/store-atom.marko @@ -0,0 +1,67 @@ +import { subscribeStorePublish } from './store-bus' + +export interface Input { + from: () => { + get(): T + set(value: T | ((prev: T) => T)): void + subscribe(listener: (value: T) => void): { unsubscribe(): void } + } | null +} + +// Maps an untyped (any/unknown) source to `any` so reads stay usable, exactly +// like the implementation (value = source.get()); a concrete T is preserved. +export type Value = unknown extends T ? any : T + +// The source thunk may legitimately return null for a beat: a getter fed from +// (from=(() => ctx()?.countAtom ?? null)) resolves nothing until +// the provider parks its bundle — during resume, and on a browser-only first +// paint. Like , this tag tolerates the empty beat and listens on +// the internal publish bus to attach the moment the bundle lands. A non-null +// source that isn't an atom (no set()) still fails loudly — that's a usage +// error, not a timing beat. + { + if (source != null && typeof source.set !== 'function') { + throw new Error( + ' expects an atom from createAtom(); the provided value has no set() ' + + 'method. A Store (createStore) uses setState — read it with instead.' + ) + } + return source +}> + + =(() => { + const source = assertAtom(input.from()) + return source == null ? undefined : source.get() +})()> + + void) | null; busOff?: (() => void) | null }> + onMount() { + const subscribeTo = (source: any) => { + value = source.get() + this.unsub = source.subscribe((next: any) => { + value = next + }).unsubscribe + } + const source = assertAtom(input.from()) + if (source != null) subscribeTo(source) + // Recovery: if the source wasn't there yet (context bundle not parked), + // attach when a provider publishes. + this.busOff = subscribeStorePublish(() => { + if (this.unsub) return + const late = assertAtom(input.from()) + if (late != null) subscribeTo(late) + }) + } + onDestroy() { + this.unsub?.() + this.unsub = null + this.busOff?.() + this.busOff = null + } +> + + diff --git a/packages/marko-store/src/tags/store-bus.ts b/packages/marko-store/src/tags/store-bus.ts new file mode 100644 index 00000000..8ee33b79 --- /dev/null +++ b/packages/marko-store/src/tags/store-bus.ts @@ -0,0 +1,10 @@ +const subscribers = new Set<() => void>() +export function subscribeStorePublish(fn: () => void): () => void { + subscribers.add(fn) + return () => subscribers.delete(fn) +} +export function publishStore(): void { + subscribers.forEach((fn) => { + try { fn() } catch { /* torn-down subscriber */ } + }) +} diff --git a/packages/marko-store/src/tags/store-context.marko b/packages/marko-store/src/tags/store-context.marko new file mode 100644 index 00000000..28d9feae --- /dev/null +++ b/packages/marko-store/src/tags/store-context.marko @@ -0,0 +1,16 @@ +export interface Input { + key?: string +} + + + + { + const g = ($global ?? {}) as unknown as Record + if (g[contextKey] == null) { + throw new Error( + ' used without a above it (no bundle on the shelf at render).', + ) + } +})()> + + (($global ?? {}) as unknown as Record)[contextKey])> diff --git a/packages/marko-store/src/tags/store-provider.marko b/packages/marko-store/src/tags/store-provider.marko new file mode 100644 index 00000000..fe12a712 --- /dev/null +++ b/packages/marko-store/src/tags/store-provider.marko @@ -0,0 +1,39 @@ +export interface Input { + value: () => Record + key?: string + content?: Marko.Body +} + +import { publishStore } from './store-bus' + + + + { + const g = ($global ?? {}) as unknown as Record + const marker = contextKey + '$owner' + if (g[marker]) { + throw new Error( + 'Duplicate for context key "' + + contextKey + + '". Only one provider per key is supported in Marko; use distinct keys, or share a module-singleton store directly.', + ) + } + g[marker] = true + g[contextKey] = input.value() +})()> + + + g[contextKey] = input.value() + g[contextKey + '$owner'] = true + publishStore() + } + onDestroy() { + const g = ($global ?? {}) as unknown as Record + g[contextKey] = undefined + g[contextKey + '$owner'] = undefined + } +/> + +<${input.content}/> diff --git a/packages/marko-store/src/tags/store-selector.marko b/packages/marko-store/src/tags/store-selector.marko new file mode 100644 index 00000000..beae476c --- /dev/null +++ b/packages/marko-store/src/tags/store-selector.marko @@ -0,0 +1,109 @@ +import { subscribeStorePublish } from './store-bus' + +export interface Input { + from?: () => { + get(): TSource + subscribe(listener: (value: TSource) => void): { unsubscribe(): void } + } + context?: (bundle: any) => { + get(): TSource + subscribe(listener: (value: TSource) => void): { unsubscribe(): void } + } + key?: string + selector?: (snapshot: TSource) => TSelected + compare?: (a: TSelected, b: TSelected) => boolean +} + + + a === b> +// When no selector is given, TSelected defaults to TSource (see Input), so the identity +// fallback is type-correct; the assertion only bridges TS's inability to narrow the `??`. + snapshot as unknown as TSelected)> + + { + if (input.from && input.context) { + throw new Error(' takes either `from` or `context`, not both.') + } +})()> + +// Resolve the current source store. In context mode this reads the bundle off the shelf and +// picks a member; the shelf can be momentarily empty on resume (provider not parked yet), so +// this may return null — callers must tolerate that. + { + if (input.context) { + const bundle = (($global ?? {}) as any)[input.key ?? DEFAULT_KEY] + return bundle == null ? null : input.context(bundle) + } + return input.from ? input.from() : null +}> + +// Seed at render — null-tolerant (empty shelf => undefined, no crash). + { const s = resolveSource(); return s == null ? undefined : select(s.get()) })()> + + void) | null + src?: any + sel?: Input['selector'] + last?: unknown + busOff?: (() => void) | null +}> + onMount() { + const subscribeTo = (source: any) => { + this.src = source + this.sel = input.selector + const cmp = input.compare ?? defaultCompare + // Refresh the render-time seed in case the store changed between render and + // mount (or the shelf was empty at render) — but let `compare` drop a + // redundant reseed. An object/array slice is a fresh identity on every + // select() call, so an unconditional assignment would re-render consumers + // once at mount with identical content. `this.last` still takes the fresh + // slice so future comparisons run against current data. + const seeded = select(source.get()) + if (!cmp(value as any, seeded)) value = seeded as any + this.last = seeded + this.unsub = source.subscribe((snapshot: any) => { + const next = (input.selector ?? ((s: any) => s))(snapshot) + if (!cmp(this.last as any, next)) { this.last = next; value = next as any } + }).unsubscribe + } + const source = resolveSource() + if (source != null) subscribeTo(source) + // context mode: recover from the empty-shelf moment when the provider parks and rings. + if (input.context) { + this.busOff = subscribeStorePublish(() => { + const s = resolveSource() + if (s != null && s !== this.src) { this.unsub?.(); subscribeTo(s) } + }) + } + } + onUpdate() { + const source = resolveSource() + if (source != null && source !== this.src) { + this.unsub?.() + this.src = source + this.sel = input.selector + this.last = select(source.get()) + value = this.last as any + this.unsub = source.subscribe((snapshot: any) => { + const next = (input.selector ?? ((s: any) => s))(snapshot) + const c = input.compare ?? defaultCompare + if (!c(this.last as any, next)) { this.last = next; value = next as any } + }).unsubscribe + } else if (input.selector !== this.sel && this.src != null) { + // A deliberate selector swap always takes effect. `compare` exists to drop + // redundant updates from store mutations, not to gate a change of selector, + // so the new projection is published unconditionally. + this.sel = input.selector + this.last = select(this.src.get()) + value = this.last as any + } + } + onDestroy() { + this.unsub?.() + this.unsub = null + this.busOff?.() + this.busOff = null + } +> + + diff --git a/packages/marko-store/src/tags/stream-store-provider.marko b/packages/marko-store/src/tags/stream-store-provider.marko new file mode 100644 index 00000000..dc6b7584 --- /dev/null +++ b/packages/marko-store/src/tags/stream-store-provider.marko @@ -0,0 +1,47 @@ +export interface Input { + value: () => Record + key: string + content?: Marko.Body +} + +import { publishStore } from './store-bus' + + + + { + const g = ($global ?? {}) as unknown as Record + // Shares the provider's "$owner" marker on purpose: a and a + // on the same key both write the same box, which is always a mistake, + // so they must detect each other and throw rather than silently clobber. + const marker = contextKey + '$owner' + if (g[marker]) { + throw new Error( + 'Duplicate for key "' + + contextKey + + '": a provider already claimed this key. Each streamed provider needs a distinct key.', + ) + } + g[marker] = true + if (!g[contextKey]) g[contextKey] = input.value() + return 0 +})()> + + + if (!g[contextKey]) g[contextKey] = input.value() + g[contextKey + '$owner'] = true + publishStore() + } + onDestroy() { + // Parity with : clear the shelf and the marker on teardown. On the client + // $global lives for the page, so without this the store would linger after the subtree + // unmounts. Not required to fix any observed bug (the streamed await subtree mounts once and + // stays), but it keeps the two providers symmetric and the shelf clean. + const g = ($global ?? {}) as unknown as Record + g[contextKey] = undefined + g[contextKey + '$owner'] = undefined + } +/> + +<${input.content}/> diff --git a/packages/marko-store/tests/fixtures/atom-host.marko b/packages/marko-store/tests/fixtures/atom-host.marko new file mode 100644 index 00000000..c9d7d008 --- /dev/null +++ b/packages/marko-store/tests/fixtures/atom-host.marko @@ -0,0 +1,20 @@ +import StoreAtom from '../../src/tags/store-atom.marko' + +export interface Input { + from: () => { + get(): any + set(value: any): void + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + +
${value}
+ diff --git a/packages/marko-store/tests/fixtures/atom-late-context-host.marko b/packages/marko-store/tests/fixtures/atom-late-context-host.marko new file mode 100644 index 00000000..b3981c09 --- /dev/null +++ b/packages/marko-store/tests/fixtures/atom-late-context-host.marko @@ -0,0 +1,17 @@ +import StoreAtom from '../../src/tags/store-atom.marko' + +// A getter-fed atom whose source resolves NOTHING at mount (empty shelf) and only +// becomes available after a provider publishes — the resume/browser-only beat that +// used to crash the tag ("Cannot read properties of undefined"). The null-tolerant +// getter is the documented form; the tag's bus recovery attaches late. +export interface Input { + from: () => { + get(): any + set(v: any): void + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } | null +} + + +
${value ?? ''}
+ diff --git a/packages/marko-store/tests/fixtures/csr-stream-build-host.marko b/packages/marko-store/tests/fixtures/csr-stream-build-host.marko new file mode 100644 index 00000000..949ad218 --- /dev/null +++ b/packages/marko-store/tests/fixtures/csr-stream-build-host.marko @@ -0,0 +1,7 @@ +import StreamStoreProvider from '../../src/tags/stream-store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { onBuild: () => void; store: any } + { input.onBuild(); return { counter: input.store } }> + c.counter selector=(s: any) => s.count key='csrBuild'/> +
${count}
+
diff --git a/packages/marko-store/tests/fixtures/csr-stream-multi-host.marko b/packages/marko-store/tests/fixtures/csr-stream-multi-host.marko new file mode 100644 index 00000000..83244a77 --- /dev/null +++ b/packages/marko-store/tests/fixtures/csr-stream-multi-host.marko @@ -0,0 +1,12 @@ +import StreamStoreProvider from '../../src/tags/stream-store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { storeA: any; storeB: any } + ({ a: input.storeA, b: input.storeB })> + c.a selector=(s: any) => s.count key='csrMulti'/> + c.b selector=(s: any) => s.count key='csrMulti'/> +
${countA}
+
${countB}
+ + +
diff --git a/packages/marko-store/tests/fixtures/ctx-customkey-host.marko b/packages/marko-store/tests/fixtures/ctx-customkey-host.marko new file mode 100644 index 00000000..4c7a4939 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-customkey-host.marko @@ -0,0 +1,10 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { store: any } + ({ counter: input.store })> + c.counter selector=(s: any) => s.count/> +
${count}
+ + +
diff --git a/packages/marko-store/tests/fixtures/ctx-dup-provider-host.marko b/packages/marko-store/tests/fixtures/ctx-dup-provider-host.marko new file mode 100644 index 00000000..2e016c97 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-dup-provider-host.marko @@ -0,0 +1,7 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import { counterStore } from './ctx-store' + ({ counter: counterStore })> + ({ counter: counterStore })> +
nested
+
+
diff --git a/packages/marko-store/tests/fixtures/ctx-getter-fed-host.marko b/packages/marko-store/tests/fixtures/ctx-getter-fed-host.marko new file mode 100644 index 00000000..af731a9b --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-getter-fed-host.marko @@ -0,0 +1,9 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { store: any } + ({ counter: input.store })> + + (ctx() as any)?.counter ?? null selector=(s: any) => s.count/> +
${count}
+
diff --git a/packages/marko-store/tests/fixtures/ctx-missing-provider-host.marko b/packages/marko-store/tests/fixtures/ctx-missing-provider-host.marko new file mode 100644 index 00000000..6b18a518 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-missing-provider-host.marko @@ -0,0 +1,3 @@ +import StoreContext from '../../src/tags/store-context.marko' + +
${typeof ctx}
diff --git a/packages/marko-store/tests/fixtures/ctx-multi-host.marko b/packages/marko-store/tests/fixtures/ctx-multi-host.marko new file mode 100644 index 00000000..a56b9774 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-multi-host.marko @@ -0,0 +1,12 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { storeA: any; storeB: any } + ({ a: input.storeA, b: input.storeB })> + c.a selector=(s: any) => s.count/> + c.b selector=(s: any) => s.count/> +
${countA}
+
${countB}
+ + +
diff --git a/packages/marko-store/tests/fixtures/ctx-nested-host.marko b/packages/marko-store/tests/fixtures/ctx-nested-host.marko new file mode 100644 index 00000000..c217d230 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-nested-host.marko @@ -0,0 +1,19 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' + +// Two providers nested with DISTINCT keys. A child reads the outer bundle by key "outer" +// and the inner bundle by key "inner". (Two providers under the SAME key throw the +// duplicate guard — see ctx-dup-provider-host; distinct keys coexist.) +export interface Input { + outer: any + inner: any +} + + ({ app: input.outer })> + ({ section: input.inner })> + c.app selector=(s: any) => s.count/> + c.section selector=(s: any) => s.count/> +
${appCount}
+
${sectionCount}
+
+
diff --git a/packages/marko-store/tests/fixtures/ctx-no-provider-host.marko b/packages/marko-store/tests/fixtures/ctx-no-provider-host.marko new file mode 100644 index 00000000..dee4277f --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-no-provider-host.marko @@ -0,0 +1,3 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + c.counter selector=(s: any) => s.count/> +
${count}
diff --git a/packages/marko-store/tests/fixtures/ctx-selector-host.marko b/packages/marko-store/tests/fixtures/ctx-selector-host.marko new file mode 100644 index 00000000..1a3c4ae1 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-selector-host.marko @@ -0,0 +1,7 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { store: any } + ({ counter: input.store })> + c.counter selector=(s: any) => s.count/> +
${count}
+
diff --git a/packages/marko-store/tests/fixtures/ctx-ssr-host.marko b/packages/marko-store/tests/fixtures/ctx-ssr-host.marko new file mode 100644 index 00000000..de14cbd0 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-ssr-host.marko @@ -0,0 +1,7 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import { counterStore } from './ctx-store' + ({ counter: counterStore })> + c.counter selector=(s: any) => s.count/> +
${count}
+
diff --git a/packages/marko-store/tests/fixtures/ctx-store.ts b/packages/marko-store/tests/fixtures/ctx-store.ts new file mode 100644 index 00000000..685c7537 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-store.ts @@ -0,0 +1,4 @@ +import { createStore } from '../../src/index' +// Module singleton so the provider's inline value thunk closes over a compiler-registered +// reference; the store stays out of the serialized payload (the keystone SSR pattern). +export const counterStore = createStore({ count: 5 }) diff --git a/packages/marko-store/tests/fixtures/ctx-write-host.marko b/packages/marko-store/tests/fixtures/ctx-write-host.marko new file mode 100644 index 00000000..9e5186f4 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ctx-write-host.marko @@ -0,0 +1,10 @@ +import StoreProvider from '../../src/tags/store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { store: any } + ({ counter: input.store })> + c.counter selector=(s: any) => s.count/> +
${count}
+ + +
diff --git a/packages/marko-store/tests/fixtures/selector-both-host.marko b/packages/marko-store/tests/fixtures/selector-both-host.marko new file mode 100644 index 00000000..564a9ad3 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-both-host.marko @@ -0,0 +1,8 @@ +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { store: any } + input.store + context=(c: any) => c.counter + selector=(s: any) => s.count +/> +
${count}
diff --git a/packages/marko-store/tests/fixtures/selector-custom-compare-host.marko b/packages/marko-store/tests/fixtures/selector-custom-compare-host.marko new file mode 100644 index 00000000..de00b185 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-custom-compare-host.marko @@ -0,0 +1,18 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// Custom `compare`: a tolerance comparator that treats counts within 10 of each +// other as equal. Exercises the `input.compare ?? defaultCompare` custom arm and +// shows the supplied comparator (not ===) decides whether `value` updates. +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + s.count + compare=(a: any, b: any) => Math.abs(a - b) < 10 +/> +
${value}
diff --git a/packages/marko-store/tests/fixtures/selector-host.marko b/packages/marko-store/tests/fixtures/selector-host.marko new file mode 100644 index 00000000..5baef9f0 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-host.marko @@ -0,0 +1,12 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } + selector?: (snapshot: any) => any +} + + +
${value}
diff --git a/packages/marko-store/tests/fixtures/selector-no-selector-host.marko b/packages/marko-store/tests/fixtures/selector-no-selector-host.marko new file mode 100644 index 00000000..9ecc8ae2 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-no-selector-host.marko @@ -0,0 +1,14 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// No `selector` attr: falls back to the identity passthrough +// (input.selector ?? ((s) => s)), so `value` is the whole store snapshot and +// updates whenever any field changes. Covers the identity arm of that `??`. +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + +
${JSON.stringify(value)}
diff --git a/packages/marko-store/tests/fixtures/selector-no-selector-swap-host.marko b/packages/marko-store/tests/fixtures/selector-no-selector-swap-host.marko new file mode 100644 index 00000000..1cae4baf --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-no-selector-swap-host.marko @@ -0,0 +1,28 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// No selector AND a source swap: flipping the source reaches onUpdate's +// resubscribe branch while `selector` stays undefined, so it covers the identity +// arm of `input.selector ?? ((s) => s)` inside onUpdate (not just onMount). +export interface Input { + storeA: { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } + storeB: { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + + + active/> +
${JSON.stringify(value)}
+ diff --git a/packages/marko-store/tests/fixtures/selector-render-count-host.marko b/packages/marko-store/tests/fixtures/selector-render-count-host.marko new file mode 100644 index 00000000..39fe939d --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-render-count-host.marko @@ -0,0 +1,25 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// Render-count probe. `track` is called from the interpolation that consumes the +// selected value, so it re-runs exactly when `value` is reassigned — Marko's +// fine-grained reactivity re-evaluates the expression only when its dependency +// changes identity. That makes over-rendering OBSERVABLE: an object/array slice +// is a fresh reference on every store change, so under the default `===` compare +// the tag reassigns `value` (and `track` re-runs) even when the slice's contents +// are unchanged. With `compare=shallow` the reassignment is suppressed. +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } + selector?: (snapshot: any) => any + compare?: (a: any, b: any) => boolean + track: (value: any) => string +} + + +
${input.track(value)}
diff --git a/packages/marko-store/tests/fixtures/selector-selector-swap-host.marko b/packages/marko-store/tests/fixtures/selector-selector-swap-host.marko new file mode 100644 index 00000000..40c2d139 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-selector-swap-host.marko @@ -0,0 +1,24 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// Keeps one source but toggles the selector. Flipping it leaves the source equal +// (source === this.src) and changes input.selector, so the tag's onUpdate takes +// the re-seed branch (input.selector !== this.sel). +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + + s.other : (s: any) => s.count> + +
${value}
+ diff --git a/packages/marko-store/tests/fixtures/selector-source-swap-host.marko b/packages/marko-store/tests/fixtures/selector-source-swap-host.marko new file mode 100644 index 00000000..e3dc12f9 --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-source-swap-host.marko @@ -0,0 +1,28 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// Toggles which of two stores is passed to through `from`. +// Flipping the toggle changes the resolved source, so the tag's onUpdate takes +// the resubscribe-and-re-seed branch (source !== this.src). +export interface Input { + storeA: { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } + storeB: { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + + + active selector=(s: any) => s.count/> +
${value}
+ diff --git a/packages/marko-store/tests/fixtures/selector-swap-compare-host.marko b/packages/marko-store/tests/fixtures/selector-swap-compare-host.marko new file mode 100644 index 00000000..59b6120f --- /dev/null +++ b/packages/marko-store/tests/fixtures/selector-swap-compare-host.marko @@ -0,0 +1,29 @@ +import StoreSelector from '../../src/tags/store-selector.marko' + +// Point 8: a deliberate selector swap must take effect even when `compare` would +// treat the new slice as equal to the old. The tolerance comparator reports equal +// for values within 10; swapping from `a` (1) to `b` (2) is inside that tolerance, +// so a compare-gated swap would wrongly keep showing 1. The swap must publish 2. +export interface Input { + from: () => { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + + s.b : (s: any) => s.a> + Math.abs(x - y) < 10 +/> +
${value}
+ diff --git a/packages/marko-store/tests/fixtures/ssr-atom.marko b/packages/marko-store/tests/fixtures/ssr-atom.marko new file mode 100644 index 00000000..e6658050 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-atom.marko @@ -0,0 +1,5 @@ +import { countAtom } from './ssr-store' +import StoreAtom from '../../src/tags/store-atom.marko' + + countAtom/> +
${value}
diff --git a/packages/marko-store/tests/fixtures/ssr-object-antipattern.marko b/packages/marko-store/tests/fixtures/ssr-object-antipattern.marko new file mode 100644 index 00000000..9d68c875 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-object-antipattern.marko @@ -0,0 +1,25 @@ +// Anti-pattern on purpose: the store arrives as a bare OBJECT attribute and is +// captured in a client-resumable lifecycle closure. The serializer walks the +// attribute value (a Store instance) and rejects. This is the failure the thunk +// channel exists to avoid; the real tags never trigger it because they only ever +// close over the registered `from` function. +export interface Input { + store: { + get(): any + subscribe(listener: (value: any) => void): { unsubscribe(): void } + } +} + + + void) | null }> + onMount() { + this.unsub = input.store.subscribe((next) => { + value = next + }).unsubscribe + } + onDestroy() { + this.unsub?.() + this.unsub = null + } +> +
${JSON.stringify(value)}
diff --git a/packages/marko-store/tests/fixtures/ssr-payload-host.marko b/packages/marko-store/tests/fixtures/ssr-payload-host.marko new file mode 100644 index 00000000..c9fba9fe --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-payload-host.marko @@ -0,0 +1,27 @@ +// Serialization-boundary probe. `input.payload` is held in a and captured +// by a client-resumable lifecycle closure, which forces Marko's serializer to +// walk it (the same forcing pattern as ssr-object-antipattern). The probes below +// are inline (compiler-registered) so nothing test-file-defined crosses the wire. +// +// Fed a payload of supported built-ins (Date, Map, Set, strings, nested arrays +// of plain objects) the render resolves and the probes print real values. Fed a +// custom class instance, the serializer rejects with its "Unable to serialize" +// error — in debug builds; production silently drops the value (see serializer +// source: `MARKO_DEBUG && throwUnserializable`). +export interface Input { + payload: any +} + + + + onMount() { + // Capturing `value` here is what makes the scope resumable and the payload + // subject to serialization. + this.touched = value != null + } +> +
${value.title ?? ''}
+
${value.when instanceof Date ? value.when.toISOString() : 'not-a-date'}
+
${value.tags instanceof Map ? String(value.tags.get('a')) : 'not-a-map'}
+
${value.ids instanceof Set ? String(value.ids.has(2)) : 'not-a-set'}
+
${JSON.stringify(value.rows ?? null)}
diff --git a/packages/marko-store/tests/fixtures/ssr-selector.marko b/packages/marko-store/tests/fixtures/ssr-selector.marko new file mode 100644 index 00000000..18d5f2e5 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-selector.marko @@ -0,0 +1,5 @@ +import { counterStore } from './ssr-store' +import StoreSelector from '../../src/tags/store-selector.marko' + + counterStore selector=(s: any) => s.count/> +
${count}
diff --git a/packages/marko-store/tests/fixtures/ssr-store.ts b/packages/marko-store/tests/fixtures/ssr-store.ts new file mode 100644 index 00000000..b1c6949c --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-store.ts @@ -0,0 +1,6 @@ +import { createAtom, createStore } from '../../src/index' + +// Module singletons so the SSR fixtures' inline thunks close over a stable, +// compiler-registered reference (see ssr.test.ts header for why this matters). +export const counterStore = createStore({ count: 5, other: 0 }) +export const countAtom = createAtom(7) diff --git a/packages/marko-store/tests/fixtures/ssr-stream-const-antipattern.marko b/packages/marko-store/tests/fixtures/ssr-stream-const-antipattern.marko new file mode 100644 index 00000000..48ed2daf --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-stream-const-antipattern.marko @@ -0,0 +1,8 @@ +import { createStore } from '../../src/index' +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { value: number } + setTimeout(() => r(input.value), 5))> + + s selector=(x: any) => x.n/> +
${c}
+
diff --git a/packages/marko-store/tests/fixtures/ssr-stream-ctx-above.marko b/packages/marko-store/tests/fixtures/ssr-stream-ctx-above.marko new file mode 100644 index 00000000..c41ca1aa --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-stream-ctx-above.marko @@ -0,0 +1,11 @@ +import { createStore } from '../../src/index' +import StreamStoreProvider from '../../src/tags/stream-store-provider.marko' +import StoreContext from '../../src/tags/store-context.marko' +export interface Input { value: number } + +
${ctx() ? 'has' : 'no'}
+ setTimeout(() => r(input.value), 5))> + ({ counter: createStore({ n: slice }) })> +
${slice}
+
+
diff --git a/packages/marko-store/tests/fixtures/ssr-stream-dup.marko b/packages/marko-store/tests/fixtures/ssr-stream-dup.marko new file mode 100644 index 00000000..01087da3 --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-stream-dup.marko @@ -0,0 +1,16 @@ +import { createStore } from '../../src/index' +import StreamStoreProvider from '../../src/tags/stream-store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { a: number; b: number } + setTimeout(() => r(input.a), 5))> + ({ counter: createStore({ n: sa }) })> + c.counter selector=(s: any) => s.n key='dupBox'/> +
${va}
+
+
+ setTimeout(() => r(input.b), 10))> + ({ counter: createStore({ n: sb }) })> + c.counter selector=(s: any) => s.n key='dupBox'/> +
${vb}
+
+
diff --git a/packages/marko-store/tests/fixtures/ssr-stream-helper.marko b/packages/marko-store/tests/fixtures/ssr-stream-helper.marko new file mode 100644 index 00000000..064ee7fd --- /dev/null +++ b/packages/marko-store/tests/fixtures/ssr-stream-helper.marko @@ -0,0 +1,10 @@ +import { createStore } from '../../src/index' +import StreamStoreProvider from '../../src/tags/stream-store-provider.marko' +import StoreSelector from '../../src/tags/store-selector.marko' +export interface Input { value: number } + setTimeout(() => r(input.value), 5))> + ({ counter: createStore({ n: slice }) })> + c.counter selector=(s: any) => s.n key='box'/> +
${value}
+
+
diff --git a/packages/marko-store/tests/index.test.ts b/packages/marko-store/tests/index.test.ts new file mode 100644 index 00000000..4a87c746 --- /dev/null +++ b/packages/marko-store/tests/index.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from 'vitest' +import { + ReadonlyStore, + Store, + batch, + createAsyncAtom, + createAtom, + createStore, + flush, + shallow, + toObserver, +} from '../src/index' + +describe('@tanstack/marko-store re-exports @tanstack/store', () => { + test('store constructors are present', () => { + expect(typeof createStore).toBe('function') + expect(typeof Store).toBe('function') + expect(typeof ReadonlyStore).toBe('function') + }) + + test('atom constructors are present', () => { + expect(typeof createAtom).toBe('function') + expect(typeof createAsyncAtom).toBe('function') + }) + + test('utilities are present', () => { + for (const fn of [batch, flush, shallow, toObserver]) { + expect(typeof fn).toBe('function') + } + }) + + test('a re-exported store actually works', () => { + const store = createStore({ n: 1 }) + store.setState((prev) => ({ n: prev.n + 1 })) + expect(store.get().n).toBe(2) + }) +}) diff --git a/packages/marko-store/tests/marko.d.ts b/packages/marko-store/tests/marko.d.ts new file mode 100644 index 00000000..8e45baed --- /dev/null +++ b/packages/marko-store/tests/marko.d.ts @@ -0,0 +1,14 @@ +// Ambient declaration for *.marko template imports in tests. +// @marko/vite compiles .marko files to browser templates exposing mount(); the +// SSR (node-environment) tests additionally use the server template's render(), +// for which they cast the import to `any`. We declare the minimal browser shape +// here so the .ts test files type-check without the full Marko language toolchain. +declare module '*.marko' { + const template: { + mount: ( + input: Record, + container: Element | DocumentFragment, + ) => { destroy: () => void } + } + export default template +} diff --git a/packages/marko-store/tests/setup.ts b/packages/marko-store/tests/setup.ts new file mode 100644 index 00000000..9f8bc9d5 --- /dev/null +++ b/packages/marko-store/tests/setup.ts @@ -0,0 +1,48 @@ +/** + * Marko scheduler polyfills for jsdom. + * + * Marko 6's scheduler (schedule.ts) walks queueMicrotask -> requestAnimationFrame + * -> MessageChannel. jsdom's rAF and MessageChannel are unreliable: rAF may never + * fire and MessageChannel may not integrate with the event loop. Without these, + * the module-level `isScheduled` flag sticks after the first scheduled render and + * every later update queues but never flushes. These mirror Marko's own test + * infrastructure (runtime-tags create-browser.ts), as does @tanstack/marko-query. + */ + +if (typeof window !== 'undefined') { + // requestAnimationFrame: batch callbacks, fire on the next macrotask. + let queue: Array | undefined + ;(window as any).requestAnimationFrame = function requestAnimationFrame( + fn: FrameRequestCallback, + ) { + if (queue) { + queue.push(fn) + } else { + queue = [fn] + setTimeout(() => { + const timestamp = performance.now() + const batch = queue! + queue = undefined + for (const cb of batch) cb(timestamp) + }) + } + return 0 + } + ;(window as any).cancelAnimationFrame = () => {} + + // MessageChannel: postMessage -> setImmediate -> queueMicrotask. + ;(window as any).MessageChannel = class MessageChannel { + port1: any + port2: any + constructor() { + this.port1 = { onmessage() {} } + this.port2 = { + postMessage: () => { + setImmediate(() => { + window.queueMicrotask(this.port1.onmessage) + }) + }, + } + } + } +} diff --git a/packages/marko-store/tests/ssr.test.ts b/packages/marko-store/tests/ssr.test.ts new file mode 100644 index 00000000..a17f98b6 --- /dev/null +++ b/packages/marko-store/tests/ssr.test.ts @@ -0,0 +1,97 @@ +// @vitest-environment node +// +// Server-render guard. The node environment is deliberate: vitest then uses +// @marko/vite's HTML (server) transform, so Marko's serializer actually runs -- +// the path the jsdom client-mount tests never touch, and where a non-serializable +// value would crash on resume. +// +// The positive cases prove the thunk channel (from={() => store}) survives SSR: +// the tag closes over the registered thunk function, so the store stays inside +// the closure and is never walked by the serializer. The SSR fixtures define the +// thunk INLINE (compiler-registered) over a module-singleton store -- a thunk +// created in this .ts file would itself be an unregistered function and fail to +// serialize. +// +// The keystone guard proves the failure mode the thunk channel exists to avoid: +// a store passed as a bare OBJECT attribute is captured in a resumable closure, +// the attribute value is walked, and serialization rejects. + +import { describe, expect, it } from 'vitest' +import SsrSelector from './fixtures/ssr-selector.marko' +import SsrAtom from './fixtures/ssr-atom.marko' +import SsrObjectAntipattern from './fixtures/ssr-object-antipattern.marko' +import SsrPayloadHost from './fixtures/ssr-payload-host.marko' +import { countAtom, counterStore } from './fixtures/ssr-store' + +async function renderToString( + template: any, + input: Record, +): Promise { + let out = '' + for await (const chunk of template.render(input)) out += String(chunk) + return out +} + +const cell = (html: string, id: string): string | null => { + const m = html.match(new RegExp(`data-testid=["']?${id}["']?>([^<]*)`)) + return m?.[1] ?? null +} + +describe('SSR render — thunk channel', () => { + it(' renders server-side and seeds the selected value', async () => { + await expect(renderToString(SsrSelector, {})).resolves.toBeTypeOf('string') + const html = await renderToString(SsrSelector, {}) + expect(cell(html, 'count')).toBe('5') + }) + + it(' renders server-side and seeds the value', async () => { + const html = await renderToString(SsrAtom, {}) + expect(cell(html, 'value')).toBe(String(countAtom.get())) + }) +}) + +describe('SSR keystone guard — why sources must be thunks', () => { + it('rejects when a store is passed as a bare object attribute', async () => { + await expect( + renderToString(SsrObjectAntipattern, { store: counterStore }), + ).rejects.toThrow(/serialize/i) + }) +}) + +describe('SSR serialization boundary — what a resumable payload can carry', () => { + // The Marko serializer (runtime-tags/src/html/serializer.ts) supports plain + // objects, arrays, Date, Map, Set, RegExp, typed arrays, URL and more; custom + // class instances are not serializable and fail loudly in debug builds + // (production silently drops them — never rely on prod to catch this). The + // fixture forces serialization by capturing the payload in a resumable closure. + it('carries strings, nested arrays of objects, Date, Map and Set', async () => { + const html = await renderToString(SsrPayloadHost, { + payload: { + title: 'quarterly report', + rows: [ + { id: 1, tags: ['a', 'b'] }, + { id: 2, tags: [] }, + ], + when: new Date('2026-07-14T00:00:00.000Z'), + tags: new Map([['a', 'alpha']]), + ids: new Set([1, 2, 3]), + }, + }) + expect(cell(html, 'title')).toBe('quarterly report') + expect(cell(html, 'date')).toBe('2026-07-14T00:00:00.000Z') + expect(cell(html, 'map')).toBe('alpha') + expect(cell(html, 'set')).toBe('true') + expect(cell(html, 'rows')).toContain('"id":1') + }) + + it('rejects a custom class instance in the payload', async () => { + class Cart { + items: Array<{ id: number }> = [] + } + await expect( + renderToString(SsrPayloadHost, { + payload: { title: 'bad', cart: new Cart() }, + }), + ).rejects.toThrow(/serialize/i) + }) +}) diff --git a/packages/marko-store/tests/store-bus.test.ts b/packages/marko-store/tests/store-bus.test.ts new file mode 100644 index 00000000..6bcb11a9 --- /dev/null +++ b/packages/marko-store/tests/store-bus.test.ts @@ -0,0 +1,37 @@ +/** The bell: a tiny client-side pub/sub the provider rings on mount so empty-shelf + * readers re-resolve on resume. Unit-level so the recovery primitive is pinned + * independent of a full resume (the resume round-trip itself is the e2e's job). */ +import { describe, expect, test, vi } from 'vitest' +import { publishStore, subscribeStorePublish } from '../src/tags/store-bus' + +describe('store-bus', () => { + test('publish notifies all current subscribers', () => { + const a = vi.fn() + const b = vi.fn() + const offA = subscribeStorePublish(a) + const offB = subscribeStorePublish(b) + publishStore() + expect(a).toHaveBeenCalledTimes(1) + expect(b).toHaveBeenCalledTimes(1) + offA() + offB() + }) + test('unsubscribe stops further notifications', () => { + const a = vi.fn() + const off = subscribeStorePublish(a) + off() + publishStore() + expect(a).not.toHaveBeenCalled() + }) + test('a throwing subscriber does not break the others', () => { + const good = vi.fn() + const offBad = subscribeStorePublish(() => { + throw new Error('boom') + }) + const offGood = subscribeStorePublish(good) + expect(() => publishStore()).not.toThrow() + expect(good).toHaveBeenCalledTimes(1) + offBad() + offGood() + }) +}) diff --git a/packages/marko-store/tests/store-context-ssr.test.ts b/packages/marko-store/tests/store-context-ssr.test.ts new file mode 100644 index 00000000..478d4c32 --- /dev/null +++ b/packages/marko-store/tests/store-context-ssr.test.ts @@ -0,0 +1,35 @@ +// @vitest-environment node +// +// Server-render guard for the context path. The node environment runs @marko/vite's +// HTML (server) transform, so Marko's serializer actually executes -- a live store +// reaching the payload would crash here. Mirrors the package's ssr.test.ts. +import { describe, expect, it } from 'vitest' +import SsrHost from './fixtures/ctx-ssr-host.marko' +import MissingProviderHost from './fixtures/ctx-missing-provider-host.marko' +import DupProviderHost from './fixtures/ctx-dup-provider-host.marko' +import { counterStore } from './fixtures/ctx-store' + +async function renderToString(t: any, input: Record): Promise { + let out = '' + for await (const c of t.render(input)) out += String(c) + return out +} +const cell = (html: string, id: string): string | null => + html.match(new RegExp(`data-testid=["']?${id}["']?>([^<]*)`))?.[1] ?? null + +describe('SSR — + ', () => { + it('seeds the selected value server-side via the context picker (and does not crash the serializer)', async () => { + const html = await renderToString(SsrHost as any, {}) + expect(cell(html, 'count')).toBe(String(counterStore.get().count)) + }) + + it(' used without a provider throws at server render', async () => { + await expect(renderToString(MissingProviderHost as any, {})).rejects.toThrow( + /store-provider/, + ) + }) + + it('a duplicate for the same key throws at server render', async () => { + await expect(renderToString(DupProviderHost as any, {})).rejects.toThrow(/Duplicate/) + }) +}) diff --git a/packages/marko-store/tests/store-context-tags.test.ts b/packages/marko-store/tests/store-context-tags.test.ts new file mode 100644 index 00000000..6d7807a0 --- /dev/null +++ b/packages/marko-store/tests/store-context-tags.test.ts @@ -0,0 +1,132 @@ +/** + * Client (jsdom) tests for the context delivery path: + the + * picker + write handle. Mounts via + * Template.mount (the Marko 6 DOM API), same as the package's tags.test.ts. Each test + * passes a FRESH store via input; client mounts never serialize, so that is safe. + */ +import { afterEach, describe, expect, test } from 'vitest' +import { waitFor } from '@testing-library/dom' +import { createStore } from '../src/index' +import SelectorHostTemplate from './fixtures/ctx-selector-host.marko' +import WriteHostTemplate from './fixtures/ctx-write-host.marko' +import NoProviderHostTemplate from './fixtures/ctx-no-provider-host.marko' +import MultiHostTemplate from './fixtures/ctx-multi-host.marko' +import NestedHostTemplate from './fixtures/ctx-nested-host.marko' +import CustomKeyHostTemplate from './fixtures/ctx-customkey-host.marko' +import GetterFedHostTemplate from './fixtures/ctx-getter-fed-host.marko' + +const SelectorHost = SelectorHostTemplate as any +const WriteHost = WriteHostTemplate as any +const NoProviderHost = NoProviderHostTemplate as any +const MultiHost = MultiHostTemplate as any +const NestedHost = NestedHostTemplate as any +const CustomKeyHost = CustomKeyHostTemplate as any +const GetterFedHost = GetterFedHostTemplate as any + +const instances: Array<{ destroy: () => void }> = [] +function mount(T: any, input: Record = {}) { + const c = document.createElement('div') + document.body.appendChild(c) + instances.push(T.mount(input, c)) + return c +} +afterEach(() => { + instances.forEach((i) => i.destroy()) + instances.length = 0 + document.body.innerHTML = '' +}) +const cell = (el: Element, id: string) => + el.querySelector(`[data-testid='${id}']`)?.textContent ?? null + +describe(' read path', () => { + test('seeds from the bundle member with no blank, and reacts to external mutation', async () => { + const store = createStore({ count: 5 }) + const el = mount(SelectorHost, { store }) + expect(cell(el, 'count')).toBe('5') // seeded at render via the picker — no blank + store.setState(() => ({ count: 6 })) + await waitFor(() => expect(cell(el, 'count')).toBe('6')) // live subscription + }) + + test('context-mode with no provider mounts without crashing (empty shelf tolerated)', () => { + const el = mount(NoProviderHost, {}) // would throw here if the guard were missing + expect(cell(el, 'count')).toBe('') + }) +}) + +describe(' write path', () => { + test('grabs the bundle in a handler and setState propagates to the selector', async () => { + const store = createStore({ count: 5 }) + const el = mount(WriteHost, { store }) + expect(cell(el, 'count')).toBe('5') + ;(el.querySelector("[data-testid='btn']") as HTMLButtonElement).click() + await waitFor(() => expect(cell(el, 'count')).toBe('99')) + }) +}) + +describe(' with multiple stores in one provider', () => { + test('each selector reads its own store, they update independently, and a context write targets one', async () => { + const a = createStore({ count: 1 }) + const b = createStore({ count: 100 }) + const el = mount(MultiHost, { storeA: a, storeB: b }) + expect(cell(el, 'count-a')).toBe('1') + expect(cell(el, 'count-b')).toBe('100') + + a.setState(() => ({ count: 2 })) + await waitFor(() => expect(cell(el, 'count-a')).toBe('2')) + expect(cell(el, 'count-b')).toBe('100') // b untouched by a's change + + b.setState(() => ({ count: 101 })) + await waitFor(() => expect(cell(el, 'count-b')).toBe('101')) + expect(cell(el, 'count-a')).toBe('2') // a untouched by b's change + + ;(el.querySelector("[data-testid='write-a']") as HTMLButtonElement).click() + await waitFor(() => expect(cell(el, 'count-a')).toBe('9')) + expect(cell(el, 'count-b')).toBe('101') // the write hit a only + }) +}) + +describe('nested with distinct keys', () => { + test('an inner provider nests under an outer one; a child reads either bundle by key, independently', async () => { + const outer = createStore({ count: 1 }) + const inner = createStore({ count: 50 }) + const el = mount(NestedHost, { outer, inner }) + expect(cell(el, 'app')).toBe('1') // outer bundle, key "outer" + expect(cell(el, 'section')).toBe('50') // inner bundle, key "inner" + + outer.setState(() => ({ count: 2 })) + await waitFor(() => expect(cell(el, 'app')).toBe('2')) + expect(cell(el, 'section')).toBe('50') // inner untouched by outer's change + + inner.setState(() => ({ count: 51 })) + await waitFor(() => expect(cell(el, 'section')).toBe('51')) + expect(cell(el, 'app')).toBe('2') // outer untouched by inner's change + }) +}) + +describe(' + with a custom key', () => { + test('store-context reads the matching custom-keyed provider, and a write through it propagates', async () => { + // The provider parks the bundle under key "custom"; the selector reads it back under + // the same key, and must grab THAT bundle so a write lands. + const store = createStore({ count: 5 }) + const el = mount(CustomKeyHost, { store }) + expect(cell(el, 'count')).toBe('5') // selector read the custom-keyed bundle + ;(el.querySelector("[data-testid='btn']") as HTMLButtonElement).click() + await waitFor(() => expect(cell(el, 'count')).toBe('77')) // store-context key="custom" found it + }) +}) + +describe(' fed by ’s getter', () => { + test('seeds correctly and stays live in jsdom (the documented first-paint blank is real-browser-only)', async () => { + // Feeding a from-mode selector from ctx() is the path that shows a one-frame blank on a + // real browser's FIRST paint (cross-tag timing; server-rendered pages are unaffected). In + // jsdom the provider parks before the selector seeds, so ctx() already resolves and there + // is no blank — this test pins that jsdom behavior (correct seed + live updates). The + // transient blank is environment-specific to a real browser and is not observable here; + // capturing it would need a real-browser harness able to sample the very first frame. + const store = createStore({ count: 5 }) + const el = mount(GetterFedHost, { store }) + expect(cell(el, 'count')).toBe('5') // no blank in jsdom + store.setState(() => ({ count: 6 })) + await waitFor(() => expect(cell(el, 'count')).toBe('6')) // subscription is live + }) +}) diff --git a/packages/marko-store/tests/stream-store-provider-client.test.ts b/packages/marko-store/tests/stream-store-provider-client.test.ts new file mode 100644 index 00000000..3e0578d7 --- /dev/null +++ b/packages/marko-store/tests/stream-store-provider-client.test.ts @@ -0,0 +1,76 @@ +/** + * Client (jsdom) tests for mounted client-only (no SSR, no resume). + * Mounts via Template.mount, same pattern as the package's store-context-tags.test.ts. These + * close two gaps the SSR+resume suite does not cover directly: + * 1. client-only single build: when BOTH the render-time const and the onMount run on the same + * (client) side, the build-if-absent guard must produce exactly one store, not two. + * 2. many stores in ONE helper bundle: a single whose value() returns + * a bundle of two stores, read independently and written via . + * Distinct keys per test; onDestroy (afterEach) clears the shelf + marker between mounts. + */ +import { afterEach, describe, expect, test } from 'vitest' +import { waitFor } from '@testing-library/dom' +import { createStore } from '../src/index' +import BuildHostTemplate from './fixtures/csr-stream-build-host.marko' +import MultiHostTemplate from './fixtures/csr-stream-multi-host.marko' + +const BuildHost = BuildHostTemplate as any +const MultiHost = MultiHostTemplate as any + +const instances: Array<{ destroy: () => void }> = [] +function mount(T: any, input: Record = {}) { + const c = document.createElement('div') + document.body.appendChild(c) + instances.push(T.mount(input, c)) + return c +} +afterEach(() => { + instances.forEach((i) => i.destroy()) + instances.length = 0 + document.body.innerHTML = '' +}) +const cell = (el: Element, id: string) => + el.querySelector(`[data-testid='${id}']`)?.textContent ?? null + +describe(' mounted client-only', () => { + test('builds the store exactly once even though the const and onMount both run on the client', async () => { + let builds = 0 + const store = createStore({ count: 7 }) + const el = mount(BuildHost, { onBuild: () => { builds += 1 }, store }) + + // Seeded from the one built store. + expect(cell(el, 'count')).toBe('7') + // Let the onMount fire (it must hit the build-if-absent guard and skip, not rebuild). + await waitFor(() => expect(cell(el, 'count')).toBe('7')) + expect(builds).toBe(1) + + // And it is live: a write to that store updates the selector. + store.setState((s) => ({ count: s.count + 1 })) + await waitFor(() => expect(cell(el, 'count')).toBe('8')) + // Still one build after the update — no rebuild on reactive flush. + expect(builds).toBe(1) + }) +}) + +describe(' with multiple stores in one bundle', () => { + test('each selector reads its own store, they update independently, and a context write targets one', async () => { + const a = createStore({ count: 11 }) + const b = createStore({ count: 22 }) + const el = mount(MultiHost, { storeA: a, storeB: b }) + + expect(cell(el, 'count-a')).toBe('11') + expect(cell(el, 'count-b')).toBe('22') + + a.setState(() => ({ count: 12 })) + await waitFor(() => expect(cell(el, 'count-a')).toBe('12')) + expect(cell(el, 'count-b')).toBe('22') // b untouched by a + + b.setState(() => ({ count: 23 })) + await waitFor(() => expect(cell(el, 'count-b')).toBe('23')) + expect(cell(el, 'count-a')).toBe('12') // a untouched by b + + ;(el.querySelector("[data-testid='write-a']") as HTMLButtonElement).click() + await waitFor(() => expect(cell(el, 'count-a')).toBe('9')) + expect(cell(el, 'count-b')).toBe('23') // the write hit a only + }) +}) diff --git a/packages/marko-store/tests/stream-store-provider.test.ts b/packages/marko-store/tests/stream-store-provider.test.ts new file mode 100644 index 00000000..6cee9302 --- /dev/null +++ b/packages/marko-store/tests/stream-store-provider.test.ts @@ -0,0 +1,83 @@ +// @vitest-environment node +// +// Server-render guards for -- the streamed born-with-data helper. +// The node environment is deliberate: vitest then uses @marko/vite's HTML (server) transform, +// so Marko's serializer actually runs -- the path where a non-serializable value crashes on +// resume, which the jsdom client-mount tests never touch. +// +// What lives HERE (server-only, no browser needed): +// - the store is BORN WITH the awaited value on the server (the selection seeds it), +// - concurrent requests stay isolated when each gets its own $global, +// - a duplicate key throws at render, +// - above the helper throws at render, +// - and the KEYSTONE: a store created in the await still rejects if it is held in a block +// , which is exactly why the helper parks it on $global instead. +// +// What does NOT live here: crossing the awaited value to the browser, no-flash, staying live +// after resume, and effect order. Those need a real browser (jsdom cannot honestly resume a +// streamed page) and are covered by the e2e suite (streaming-helper-*.spec.ts). + +import { describe, expect, it } from 'vitest' +import StreamHelper from './fixtures/ssr-stream-helper.marko' +import StreamConstAntipattern from './fixtures/ssr-stream-const-antipattern.marko' +import StreamDup from './fixtures/ssr-stream-dup.marko' +import StreamCtxAbove from './fixtures/ssr-stream-ctx-above.marko' + +async function renderToString( + template: any, + input: Record, +): Promise { + let out = '' + for await (const chunk of template.render(input)) out += String(chunk) + return out +} + +const cell = (html: string, id: string): string | null => { + const m = html.match(new RegExp(`data-testid=["']?${id}["']?>([^<]*)`)) + return m?.[1] ?? null +} + +describe(' SSR — born with data', () => { + it('builds the store from the awaited value on the server and seeds the selection', async () => { + const html = await renderToString(StreamHelper, { value: 77, $global: {} }) + expect(cell(html, 'count')).toBe('77') + }) + + it('renders server-side with no serialization crash (the store lives on $global, not the block)', async () => { + await expect( + renderToString(StreamHelper, { value: 77, $global: {} }), + ).resolves.toBeTypeOf('string') + }) + + it('isolates concurrent requests when each gets its own $global', async () => { + const [a, b] = await Promise.all([ + renderToString(StreamHelper, { value: 11, $global: {} }), + renderToString(StreamHelper, { value: 22, $global: {} }), + ]) + expect(cell(a, 'count')).toBe('11') + expect(cell(b, 'count')).toBe('22') + }) +}) + +describe(' SSR — keystone guards', () => { + // The reason the helper parks the store on $global instead of holding it in the block: a store + // created in the await and kept in a is captured in the block's serialized state, and a + // live store cannot be serialized. This fixture does exactly that and must still reject. + it('a store held in a block const inside the await still rejects — why the helper uses the shelf', async () => { + await expect( + renderToString(StreamConstAntipattern, { value: 77, $global: {} }), + ).rejects.toThrow(/serialize/i) + }) + + it('a duplicate key throws at render', async () => { + await expect( + renderToString(StreamDup, { a: 111, b: 222, $global: {} }), + ).rejects.toThrow(/distinct key/i) + }) + + it(' above the helper throws at render', async () => { + await expect( + renderToString(StreamCtxAbove, { value: 88, $global: {} }), + ).rejects.toThrow(/no bundle on the shelf at render/i) + }) +}) diff --git a/packages/marko-store/tests/tags.test.ts b/packages/marko-store/tests/tags.test.ts new file mode 100644 index 00000000..1b0fe53b --- /dev/null +++ b/packages/marko-store/tests/tags.test.ts @@ -0,0 +1,354 @@ +/** + * Tag-level integration tests for and . + * + * We call template.mount() directly (the Marko 6 DOM API) instead of using + * @marko/testing-library: its v6 detection (`!template.renderSync`) misfires on + * Marko 6 browser templates — which DO have renderSync — and takes the Marko + * 3/4 path, calling a render() that browser templates don't expose. The same + * workaround is used by @tanstack/marko-virtual. @marko/vite compiles the .marko + * fixtures to browser templates in jsdom, and waitFor() retries until Marko's + * rAF-based reactive flush completes. + * + * Each test creates a FRESH store/atom and passes it as a thunk (from: () => x). + * Client mounts never serialize, so a test-created thunk is fine here; the SSR + * tests (ssr.test.ts) cover the resume/serialization path separately. + */ + +import { afterEach, describe, expect, test } from 'vitest' +import { waitFor } from '@testing-library/dom' +import { batch, createAtom, createStore, shallow } from '../src/index' +import { publishStore } from '../src/tags/store-bus' +import SelectorHostTemplate from './fixtures/selector-host.marko' +import RenderCountHostTemplate from './fixtures/selector-render-count-host.marko' +import AtomLateContextHostTemplate from './fixtures/atom-late-context-host.marko' +import AtomHostTemplate from './fixtures/atom-host.marko' +import SourceSwapHostTemplate from './fixtures/selector-source-swap-host.marko' +import SelectorSwapHostTemplate from './fixtures/selector-selector-swap-host.marko' +import NoSelectorHostTemplate from './fixtures/selector-no-selector-host.marko' +import CustomCompareHostTemplate from './fixtures/selector-custom-compare-host.marko' +import NoSelectorSwapHostTemplate from './fixtures/selector-no-selector-swap-host.marko' +import SelectorSwapCompareHostTemplate from './fixtures/selector-swap-compare-host.marko' +import SelectorBothHostTemplate from './fixtures/selector-both-host.marko' + +const SelectorHost = SelectorHostTemplate as any +const AtomHost = AtomHostTemplate as any +const SourceSwapHost = SourceSwapHostTemplate as any +const SelectorSwapHost = SelectorSwapHostTemplate as any +const NoSelectorHost = NoSelectorHostTemplate as any +const CustomCompareHost = CustomCompareHostTemplate as any +const NoSelectorSwapHost = NoSelectorSwapHostTemplate as any +const SelectorSwapCompareHost = SelectorSwapCompareHostTemplate as any +const SelectorBothHost = SelectorBothHostTemplate as any +const RenderCountHost = RenderCountHostTemplate as any +const AtomLateContextHost = AtomLateContextHostTemplate as any + +const instances: Array<{ destroy: () => void }> = [] + +function mount(Template: any, input: Record = {}) { + const container = document.createElement('div') + document.body.appendChild(container) + instances.push(Template.mount(input, container)) + return container +} + +afterEach(() => { + instances.forEach((i) => i.destroy()) + instances.length = 0 + document.body.innerHTML = '' +}) + +const cell = (el: Element, id: string) => + el.querySelector(`[data-testid='${id}']`)?.textContent ?? null + +describe(' read path', () => { + test('seeds from the store and updates when the selected slice changes', async () => { + const store = createStore({ count: 5, other: 0 }) + const el = mount(SelectorHost, { + from: () => store, + selector: (s: { count: number }) => s.count, + }) + expect(cell(el, 'value')).toBe('5') + + store.setState((s) => ({ ...s, count: s.count + 1 })) + await waitFor(() => expect(cell(el, 'value')).toBe('6')) + }) + + test('dedupes when an unrelated field changes', async () => { + const store = createStore({ count: 5, other: 0 }) + const el = mount(SelectorHost, { + from: () => store, + selector: (s: { count: number }) => s.count, + }) + await waitFor(() => expect(cell(el, 'value')).toBe('5')) + + // The selected slice (count) is unchanged, so the tag must not re-render. + store.setState((s) => ({ ...s, other: s.other + 99 })) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(cell(el, 'value')).toBe('5') + }) + + test('with no selector, passes the whole snapshot through and updates', async () => { + const store = createStore({ count: 5, other: 0 }) + const el = mount(NoSelectorHost, { from: () => store }) + expect(cell(el, 'value')).toBe(JSON.stringify({ count: 5, other: 0 })) + + // No selector => identity, so any field change produces a new snapshot and + // the default === compare sees a different object, so the value updates. + store.setState((s) => ({ ...s, count: 6 })) + await waitFor(() => + expect(cell(el, 'value')).toBe(JSON.stringify({ count: 6, other: 0 })), + ) + }) + + test('uses a supplied compare to decide whether the value updates', async () => { + const store = createStore({ count: 0 }) + const el = mount(CustomCompareHost, { from: () => store }) + expect(cell(el, 'value')).toBe('0') + + // Within tolerance (|0 - 1| < 10): the custom compare reports equal, so the + // tag must not re-render even though the selected slice changed. + store.setState(() => ({ count: 1 })) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(cell(el, 'value')).toBe('0') + + // Outside tolerance (|0 - 100| >= 10): not equal, so the value updates. + store.setState(() => ({ count: 100 })) + await waitFor(() => expect(cell(el, 'value')).toBe('100')) + }) +}) + +describe(' write path', () => { + test('echoes a write back through valueChange without looping', async () => { + const atom = createAtom(7) + const el = mount(AtomHost, { from: () => atom }) + expect(cell(el, 'value')).toBe('7') + ;(el.querySelector("[data-testid='inc']") as HTMLElement).dispatchEvent( + new MouseEvent('click', { bubbles: true }), + ) + + await waitFor(() => expect(cell(el, 'value')).toBe('8')) + expect(atom.get()).toBe(8) + }) + + test('reflects an external atom mutation, not just local writes', async () => { + const atom = createAtom(7) + const el = mount(AtomHost, { from: () => atom }) + expect(cell(el, 'value')).toBe('7') + + // Mutate from outside the tag; the tag's subscription drives the update. + atom.set(42) + await waitFor(() => expect(cell(el, 'value')).toBe('42')) + }) + + test('rejects a Store with a clear error at render, not on first write (point 9)', () => { + // A Store has setState but no set(); writes via set(), so without an + // early guard it would seed and read fine and only fail on the first valueChange. + const store = createStore({ count: 1 }) + expect(() => mount(AtomHost, { from: () => store })).toThrow( + / expects an atom/, + ) + }) +}) + +describe(' onUpdate', () => { + test('resubscribes and re-seeds when the source store changes', async () => { + const storeA = createStore({ count: 1, other: 0 }) + const storeB = createStore({ count: 100, other: 0 }) + const el = mount(SourceSwapHost, { storeA, storeB }) + expect(cell(el, 'value')).toBe('1') + + // Swap the source: a new resolved store reaches onUpdate, which unsubscribes + // from the old one, re-seeds from the new one, and subscribes to it. + ;(el.querySelector("[data-testid='swap']") as HTMLElement).dispatchEvent( + new MouseEvent('click', { bubbles: true }), + ) + await waitFor(() => expect(cell(el, 'value')).toBe('100')) + + // Updates to the new source now flow through. + storeB.setState((s) => ({ ...s, count: 101 })) + await waitFor(() => expect(cell(el, 'value')).toBe('101')) + + // The old source is no longer observed. + storeA.setState((s) => ({ ...s, count: 999 })) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(cell(el, 'value')).toBe('101') + }) + + test('re-seeds when the selector changes against the same source', async () => { + const store = createStore({ count: 5, other: 42 }) + const el = mount(SelectorSwapHost, { from: () => store }) + expect(cell(el, 'value')).toBe('5') + + // Swap the selector: same source, so onUpdate re-seeds with the new selection. + ;( + el.querySelector("[data-testid='swap-selector']") as HTMLElement + ).dispatchEvent(new MouseEvent('click', { bubbles: true })) + await waitFor(() => expect(cell(el, 'value')).toBe('42')) + + // The live subscription reflects the new selector on the next change. + store.setState((s) => ({ ...s, other: 43 })) + await waitFor(() => expect(cell(el, 'value')).toBe('43')) + }) + + test('with no selector, resubscribes to a swapped source', async () => { + const storeA = createStore({ count: 1 }) + const storeB = createStore({ count: 100 }) + const el = mount(NoSelectorSwapHost, { storeA, storeB }) + expect(cell(el, 'value')).toBe(JSON.stringify({ count: 1 })) + + // Swap the source with no selector in play: onUpdate takes the resubscribe + // branch and re-seeds via the identity passthrough. + ;(el.querySelector("[data-testid='swap']") as HTMLElement).dispatchEvent( + new MouseEvent('click', { bubbles: true }), + ) + await waitFor(() => + expect(cell(el, 'value')).toBe(JSON.stringify({ count: 100 })), + ) + + // The new source is now the observed one. + storeB.setState(() => ({ count: 101 })) + await waitFor(() => + expect(cell(el, 'value')).toBe(JSON.stringify({ count: 101 })), + ) + }) + + test('a selector swap publishes the new slice even when compare calls it equal (point 8)', async () => { + // a=1 and b=2 are within the host's tolerance compare (|1-2| < 10). A compare-gated + // swap would treat the new slice as "equal" and keep showing 1; the swap must win. + const store = createStore({ a: 1, b: 2 }) + const el = mount(SelectorSwapCompareHost, { from: () => store }) + expect(cell(el, 'value')).toBe('1') + + ;( + el.querySelector("[data-testid='swap-selector']") as HTMLElement + ).dispatchEvent(new MouseEvent('click', { bubbles: true })) + await waitFor(() => expect(cell(el, 'value')).toBe('2')) + }) +}) + +describe(' input guards', () => { + test('throws at render when given both `from` and `context`', () => { + // The two source modes are mutually exclusive; the render-time guard must reject + // the ambiguous combination rather than silently preferring one. + const store = createStore({ count: 5 }) + expect(() => mount(SelectorBothHost, { store })).toThrow( + 'takes either `from` or `context`, not both', + ) + }) +}) + +describe(' object/array slices', () => { + // An object (or array) slice is a FRESH REFERENCE on every store change, so the + // default `===` compare never reports equal — the classic selector footgun. The + // first test documents the over-render; the second proves `shallow` (re-exported + // from the core) is the fix. `track` runs from the consuming interpolation, so + // its call count IS the consumer re-render count (see the fixture header). + test('over-renders under the default compare when an unrelated field changes', async () => { + const store = createStore({ id: 1, name: 'Ada', other: 0 }) + let renders = 0 + const el = mount(RenderCountHost, { + from: () => store, + selector: (s: any) => ({ id: s.id, name: s.name }), + track: (v: any) => { + renders++ + return JSON.stringify(v) + }, + }) + expect(cell(el, 'value')).toBe(JSON.stringify({ id: 1, name: 'Ada' })) + const before = renders + + // The slice's CONTENTS are unchanged, but it's a new object, so `===` sees a + // change and the consumer re-renders — same text, wasted work. + store.setState((s) => ({ ...s, other: s.other + 1 })) + await waitFor(() => expect(renders).toBeGreaterThan(before)) + expect(cell(el, 'value')).toBe(JSON.stringify({ id: 1, name: 'Ada' })) + }) + + test('compare=shallow suppresses the over-render and still passes real changes', async () => { + const store = createStore({ id: 1, name: 'Ada', other: 0 }) + let renders = 0 + const el = mount(RenderCountHost, { + from: () => store, + selector: (s: any) => ({ id: s.id, name: s.name }), + compare: shallow, + track: (v: any) => { + renders++ + return JSON.stringify(v) + }, + }) + expect(cell(el, 'value')).toBe(JSON.stringify({ id: 1, name: 'Ada' })) + const before = renders + + // Unrelated change: shallow sees equal contents, no reassignment, no re-render. + store.setState((s) => ({ ...s, other: s.other + 1 })) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(renders).toBe(before) + + // Real change to the slice: shallow sees a difference and the value updates. + store.setState((s) => ({ ...s, name: 'Grace' })) + await waitFor(() => + expect(cell(el, 'value')).toBe(JSON.stringify({ id: 1, name: 'Grace' })), + ) + }) +}) + +describe('write batching', () => { + // Verified against the core directly: an unbatched double `setState` in one tick + // notifies subscribers twice; `batch()` collapses it to a single notification + // carrying the final state. The selector runs once per notification, so its call + // count observes exactly that at the tag level. + test('batch() collapses two same-tick writes into one selector update', async () => { + const store = createStore({ count: 0 }) + let selectorCalls = 0 + const el = mount(SelectorHost, { + from: () => store, + selector: (s: any) => { + selectorCalls++ + return s.count + }, + }) + expect(cell(el, 'value')).toBe('0') + + const beforeUnbatched = selectorCalls + store.setState(() => ({ count: 1 })) + store.setState(() => ({ count: 2 })) + await waitFor(() => expect(cell(el, 'value')).toBe('2')) + expect(selectorCalls - beforeUnbatched).toBe(2) + + const beforeBatched = selectorCalls + batch(() => { + store.setState(() => ({ count: 3 })) + store.setState(() => ({ count: 4 })) + }) + await waitFor(() => expect(cell(el, 'value')).toBe('4')) + expect(selectorCalls - beforeBatched).toBe(1) + }) +}) + +describe(' late-source recovery', () => { + // The gap the store-context example exposed under SSR: a getter-fed atom whose + // source is empty at mount used to crash the resume effect chain and take every + // other tag on the page down with it. The tag now tolerates the empty beat and + // attaches via the publish bus when a provider parks — mirroring the selector. + test('tolerates an empty source at mount and attaches when the bundle is published', async () => { + const atom = createAtom(7) + let source: any = null + const el = mount(AtomLateContextHost, { from: () => source }) + // Empty beat: no crash, blank value. + expect(cell(el, 'value')).toBe('') + + // The provider parks and rings the bus; the tag attaches and seeds. + source = atom + publishStore() + await waitFor(() => expect(cell(el, 'value')).toBe('7')) + + // Fully live both ways after the late attach. + atom.set(8) + await waitFor(() => expect(cell(el, 'value')).toBe('8')) + ;(el.querySelector("[data-testid='inc']") as HTMLElement).dispatchEvent( + new MouseEvent('click', { bubbles: true }), + ) + await waitFor(() => expect(cell(el, 'value')).toBe('9')) + expect(atom.get()).toBe(9) + }) +}) diff --git a/packages/marko-store/tsconfig.build.json b/packages/marko-store/tsconfig.build.json new file mode 100644 index 00000000..89dadab5 --- /dev/null +++ b/packages/marko-store/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": {} + }, + "include": ["src"] +} diff --git a/packages/marko-store/tsconfig.json b/packages/marko-store/tsconfig.json new file mode 100644 index 00000000..1083ed63 --- /dev/null +++ b/packages/marko-store/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/store": ["../store/src"] + } + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "e2e/**/*.ts", "eslint.config.js", "vitest.config.ts"] +} diff --git a/packages/marko-store/tsconfig.marko.json b/packages/marko-store/tsconfig.marko.json new file mode 100644 index 00000000..c46554c8 --- /dev/null +++ b/packages/marko-store/tsconfig.marko.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "noEmit": true, + "paths": {}, + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.marko", + "tests/**/*.ts", + "tests/**/*.marko" + ] +} diff --git a/packages/marko-store/tsconfig.tags.json b/packages/marko-store/tsconfig.tags.json new file mode 100644 index 00000000..e8a75dca --- /dev/null +++ b/packages/marko-store/tsconfig.tags.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "paths": {}, + "outDir": "dist", + "rootDir": "src", + "noEmit": false, + "declaration": true, + "tsBuildInfoFile": "./dist/tsconfig.tags.tsbuildinfo", + "types": ["node"] + }, + "include": ["src/tags/**/*.marko", "src/tags/**/*.ts"] +} diff --git a/packages/marko-store/tsdown.config.ts b/packages/marko-store/tsdown.config.ts new file mode 100644 index 00000000..b9f53dbf --- /dev/null +++ b/packages/marko-store/tsdown.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: ['./src/index.ts'], + format: ['esm', 'cjs'], + unbundle: true, + dts: true, + sourcemap: true, + clean: true, + minify: false, + fixedExtension: false, + // NOTE: unlike react-store, `exports: true` is intentionally NOT set here. + // tsdown's `exports: true` rewrites package.json "exports" from the built + // entries only, which would wipe the hand-authored "./marko.json" subpath. + // The Marko tags are built separately by `marko-type-check` (see the "build" + // script) into dist/tags. Tags resolve by NAME via marko.json's "exports" + // field; package.json "exports" additionally carries "./dist/tags/*" because + // the consumer's compiler emits each discovered tag as a bare specifier + // through the package and the consumer's BUNDLER resolves it via this map — + // without the subpath every real consumer build fails. We maintain + // package.json "exports" by hand. + // publint runs as `test:build` AFTER the full build, not inline here: tsdown's + // `clean` wipes dist and an inline publint would run before marko-type-check + // emits dist/tags, structurally failing the "./dist/tags/*" exports check. +}) diff --git a/packages/marko-store/vitest.config.ts b/packages/marko-store/vitest.config.ts new file mode 100644 index 00000000..d7e035bc --- /dev/null +++ b/packages/marko-store/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config' +import marko from '@marko/vite' +import packageJson from './package.json' + +// @marko/vite is an app plugin (it sets up SSR + browser transforms) and is +// used by vitest only — never by the library build (tsdown handles that). +export default defineConfig({ + plugins: [marko() as any], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, + typecheck: { enabled: true }, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5cfedcb..53ad8be5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@marko/compiler': 5.39.66 + importers: .: @@ -59,6 +62,9 @@ importers: prettier: specifier: ^3.8.2 version: 3.8.2 + prettier-plugin-marko: + specifier: ^4.0.9 + version: 4.0.9 prettier-plugin-svelte: specifier: ^3.5.1 version: 3.5.1(prettier@3.8.2)(svelte@5.55.7(@typescript-eslint/types@8.58.1)) @@ -351,6 +357,206 @@ importers: specifier: ^7.2.2 version: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + examples/marko/atoms: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/simple: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/ssr-per-request-multi: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/ssr-resume: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/store-actions: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/store-context: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/stores: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + examples/marko/streaming: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/marko-store': + specifier: ^0.11.0 + version: link:../../../packages/marko-store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + vite: + specifier: ^8.0.8 + version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + examples/preact/atoms: dependencies: '@tanstack/preact-store': @@ -969,7 +1175,7 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ^2.4.5 - version: 2.4.5(cd1678fbfdd77b23b6bdbf3f09017a19) + version: 2.4.5(981d0923643984e7df52ebd265f97d33) '@angular/common': specifier: ^21.2.8 version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) @@ -999,6 +1205,56 @@ importers: specifier: ^3.3.1 version: 3.3.2 + packages/marko-store: + dependencies: + '@tanstack/store': + specifier: workspace:* + version: link:../store + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@marko/vite': + specifier: ^6 + version: 6.1.0(@marko/compiler@5.39.66)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.1 + marko: + specifier: ^6 + version: 6.1.18 + + packages/marko-store/e2e: + dependencies: + '@marko/run': + specifier: ^0.10.0 + version: 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@tanstack/store': + specifier: workspace:* + version: link:../../store + marko: + specifier: ^6 + version: 6.1.18 + devDependencies: + '@marko/language-tools': + specifier: ^2.6.0 + version: 2.6.0 + '@marko/type-check': + specifier: ^3.1.0 + version: 3.1.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.61.1 + typescript: + specifier: ^5.6.3 + version: 5.8.3 + vite: + specifier: ^6.4.2 + version: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + packages/preact-store: dependencies: '@tanstack/store': @@ -1022,7 +1278,7 @@ importers: version: 10.29.1 typescript-eslint: specifier: ^8.58.1 - version: 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.2) + version: 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) vitest: specifier: ^4.1.4 version: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) @@ -1087,7 +1343,7 @@ importers: version: 1.9.12 vue: specifier: ^3.5.32 - version: 3.5.32(typescript@6.0.2) + version: 3.5.32(typescript@6.0.3) packages/svelte-store: dependencies: @@ -1097,7 +1353,7 @@ importers: devDependencies: '@sveltejs/package': specifier: ^2.5.7 - version: 2.5.7(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.2) + version: 2.5.7(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.58.1))(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) @@ -1112,7 +1368,7 @@ importers: version: 5.55.7(@typescript-eslint/types@8.58.1) svelte-check: specifier: ^4.4.6 - version: 4.4.6(picomatch@4.0.4)(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.2) + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3) packages/vue-store: dependencies: @@ -1121,20 +1377,20 @@ importers: version: link:../store vue-demi: specifier: ^0.14.10 - version: 0.14.10(@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.2)))(vue@3.5.32(typescript@6.0.2)) + version: 0.14.10(@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.3)))(vue@3.5.32(typescript@6.0.3)) devDependencies: '@testing-library/vue': specifier: ^8.1.0 - version: 8.1.0(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.2)) + version: 8.1.0(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.3)) '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) + version: 6.0.5(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.3)) '@vue/composition-api': specifier: ^1.7.2 - version: 1.7.2(vue@3.5.32(typescript@6.0.2)) + version: 1.7.2(vue@3.5.32(typescript@6.0.3)) vue: specifier: ^3.5.32 - version: 3.5.32(typescript@6.0.2) + version: 3.5.32(typescript@6.0.3) vue2: specifier: npm:vue@2.6 version: vue@2.6.14 @@ -2051,6 +2307,14 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@chialab/estransform@0.19.1': + resolution: {integrity: sha512-Op0TvQxnzdcnBriFUIjgg3V3MpOB9Cfs4S7TvIuypPegFOSvuFAOcPl5V02NJ9dyGoOc8W6ORbSldc5PYKhOCQ==} + engines: {node: '>=18'} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -2100,6 +2364,12 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -2112,6 +2382,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} @@ -2124,6 +2400,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} @@ -2136,6 +2418,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} @@ -2148,6 +2436,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} @@ -2160,6 +2454,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} @@ -2172,6 +2472,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} @@ -2184,6 +2490,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} @@ -2196,6 +2508,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} @@ -2208,6 +2526,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} @@ -2220,6 +2544,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} @@ -2232,6 +2562,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} @@ -2244,6 +2580,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} @@ -2256,6 +2598,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} @@ -2268,6 +2616,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} @@ -2280,6 +2634,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} @@ -2292,6 +2652,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -2304,6 +2670,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} @@ -2316,6 +2688,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -2328,6 +2706,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} @@ -2340,6 +2724,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -2352,6 +2742,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} @@ -2364,6 +2760,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} @@ -2376,6 +2778,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} @@ -2388,6 +2796,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} @@ -2400,6 +2814,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -2890,12 +3310,44 @@ packages: cpu: [x64] os: [win32] + '@luxass/strip-json-comments@1.4.0': + resolution: {integrity: sha512-Zl343to4u/t8jz1q7R/1UY6hLX+344cwPLEXsIYthVwdU5zyjuVBGcpf2E24+QZkwFfRfmnHTcscreQzWn3hiA==} + engines: {node: '>=18'} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@marko/compiler@5.39.66': + resolution: {integrity: sha512-Xky1vqj1wh8o0Cps/RRgo5p4i8DL+YOSFMvViFfRCTXbMMFNMFH6gUtk1S/oA9yIouhL1iULpxp3F87oWa2ZbA==} + engines: {node: 18 || 20 || >=22} + + '@marko/language-tools@2.6.0': + resolution: {integrity: sha512-Ut35zMYlDXqxrhcF+I24XLngYKptAposGBADt3Ba6YO+fP13B5hZsMjCQLBJFzHjAkRutCmVT7/kaqAc4f88TQ==} + + '@marko/run-explorer@2.0.4': + resolution: {integrity: sha512-E/wgGs8oo0j8XCR+wbxWuMkr1HpmAW+KydTuZXWc2haFRf+sE/7SXXQbgSCpCuRbfcGiVTTMXSfaL4lHrRLxAQ==} + peerDependencies: + '@marko/run': ^0.7||^0.8||^0.9||^0.10||^0.11 + + '@marko/run@0.10.0': + resolution: {integrity: sha512-ehM+NBEuErVmFMJ85HpycpiWlgW49ZHdRkT3T8Dsj6tmgzGiZzMblcKBkds+ixV2M4JKvWZaM+Tu5YWMZTQOQQ==} + hasBin: true + peerDependencies: + marko: 5 - 6 + + '@marko/type-check@3.1.0': + resolution: {integrity: sha512-vPNhfN0RboRIflegKJK0mvs/jnZcntp684R5q+qrSAkUDn1Z3PkDfNc1SIl3uPcvJ746ZNqylXILLMjF39uUyA==} + hasBin: true + + '@marko/vite@6.1.0': + resolution: {integrity: sha512-aZrtaDehP4FebYm5GQIqgzyWNvEOeSPDufD1JF6UvN0AhQqyPVLgM25644dxvQSCbNoaOFNb+GQj1V/9Nn9PBw==} + peerDependencies: + '@marko/compiler': 5.39.66 + vite: ^8 + '@mdn/browser-compat-data@5.7.6': resolution: {integrity: sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==} @@ -2942,6 +3394,88 @@ packages: cpu: [x64] os: [win32] + '@napi-rs/magic-string-android-arm-eabi@0.3.4': + resolution: {integrity: sha512-sszAYxqtzzJ4FDerDNHcqL9NhqPhj8W4DNiOanXYy50mA5oojlRtaAFPiB5ZMrWDBM32v5Q30LrmxQ4eTtu2Dg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/magic-string-android-arm64@0.3.4': + resolution: {integrity: sha512-jdQ6HuO0X5rkX4MauTcWR4HWdgjakTOmmzqXg8L26+jOHVVG1LZE+Su5qvV4bP8vMb2h+vPE+JsnwqSmWymu3Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/magic-string-darwin-arm64@0.3.4': + resolution: {integrity: sha512-6NmMtvURce9/oq09XBZmuIeI6lPLGtEJ2ZPO/QzL3nLZa6wygiCnO/sFACKYNg5/73ET5HMMTeuogE1JI+r2Lw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/magic-string-darwin-x64@0.3.4': + resolution: {integrity: sha512-f9LmfMiUAKDOtl0meOuLYeVb6OERrgGzrTg1Tn3R3fTAShM2kxRbfAuPE9ljuXxIFzOv/uqRNLSl/LqCJwpREA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/magic-string-freebsd-x64@0.3.4': + resolution: {integrity: sha512-rqduQ4odiDK4QdM45xHWRTU4wtFIfpp8g8QGpz+3qqg7ivldDqbbNOrBaf6Oeu77uuEvWggnkyuChotfKgJdJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/magic-string-linux-arm-gnueabihf@0.3.4': + resolution: {integrity: sha512-pVaJEdEpiPqIfq3M4+yMAATS7Z9muDcWYn8H7GFH1ygh8GwgLgKfy/n/lG2M6zp18Mwd0x7E2E/qg9GgCyUzoQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/magic-string-linux-arm64-gnu@0.3.4': + resolution: {integrity: sha512-9FwoAih/0tzEZx0BjYYIxWkSRMjonIn91RFM3q3MBs/evmThXUYXUqLNa1PPIkK1JoksswtDi48qWWLt8nGflQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/magic-string-linux-arm64-musl@0.3.4': + resolution: {integrity: sha512-wCR7R+WPOcAKmVQc1s6h6HwfwW1vL9pM8BjUY9Ljkdb8wt1LmZEmV2Sgfc1SfbRQzbyl+pKeufP6adRRQVzYDA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/magic-string-linux-x64-gnu@0.3.4': + resolution: {integrity: sha512-sbxFDpYnt5WFbxQ1xozwOvh5A7IftqSI0WnE9O7KsQIOi0ej2dvFbfOW4tmFkvH/YP8KJELo5AhP2+kEq1DpYA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/magic-string-linux-x64-musl@0.3.4': + resolution: {integrity: sha512-jN4h/7e2Ul8v3UK5IZu38NXLMdzVWhY4uEDlnwuUAhwRh26wBQ1/pLD97Uy/Z3dFNBQPcsv60XS9fOM1YDNT6w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/magic-string-win32-arm64-msvc@0.3.4': + resolution: {integrity: sha512-gMUyTRHLWpzX2ntJFCbW2Gnla9Y/WUmbkZuW5SBAo/Jo8QojHn76Y4PNgnoXdzcsV9b/45RBxurYKAfFg9WTyg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/magic-string-win32-ia32-msvc@0.3.4': + resolution: {integrity: sha512-QIMauMOvEHgL00K9np/c9CT/CRtLOz3mRTQqcZ9XGzSoAMrpxH71KSpDJrKl7h7Ro6TZ+hJ0C3T+JVuTCZNv4A==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/magic-string-win32-x64-msvc@0.3.4': + resolution: {integrity: sha512-V8FMSf828MzOI3P6/765MR7zHU6CUZqiyPhmAnwYoKFNxfv7oCviN/G6NcENeCdcYOvNgh5fYzaNLB96ndId5A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/magic-string@0.3.4': + resolution: {integrity: sha512-DEWl/B99RQsyMT3F9bvrXuhL01/eIQp/dtNSE3G1jQ4mTGRcP4iHWxoPZ577WrbjUinrNgvRA5+08g8fkPgimQ==} + engines: {node: '>= 10'} + '@napi-rs/nice-android-arm-eabi@1.1.1': resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} engines: {node: '>= 10'} @@ -3206,12 +3740,22 @@ packages: cpu: [arm64] os: [darwin] + '@oxc-parser/binding-darwin-arm64@0.8.0': + resolution: {integrity: sha512-3Dws5Wzj9efojjqvhS4ZF+Abh0EoiI5ciOE2kdLifMzSg4fnmYAIOktoUnPEo87TNIb4SiFJ5JgPBgEyq42Eow==} + cpu: [arm64] + os: [darwin] + '@oxc-parser/binding-darwin-x64@0.121.0': resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxc-parser/binding-darwin-x64@0.8.0': + resolution: {integrity: sha512-DAUJ/mfq0Jn2VDYn69bhHTsIWj+aZ/viamexFwaLL7ntkIFmGpzAJZUlWofpY1IRJynKWW+P5AOLYXMllw4qUw==} + cpu: [x64] + os: [darwin] + '@oxc-parser/binding-freebsd-x64@0.121.0': resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3237,9 +3781,21 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.121.0': - resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@oxc-parser/binding-linux-arm64-gnu@0.8.0': + resolution: {integrity: sha512-ZHQVey/O4K3zTIKtpfsbtJIE8MPTRHRxgY3dejaoeFQGf9C3HasgF132Yp4zN/jOUx+x8czKPVa/Af40ViyhGQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.121.0': + resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-arm64-musl@0.8.0': + resolution: {integrity: sha512-Diw+Tnf5v+zAYXzDoSKCZsMaroU6GoqZMS7smfDtFnZYTHWZrsTmPBLUQe7AFiG7O7tkhsCdcWjOYgbVkrSVOA==} cpu: [arm64] os: [linux] libc: [musl] @@ -3279,6 +3835,12 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.8.0': + resolution: {integrity: sha512-WloqcRrtQUVEP/Sy8ZeEgF0HgBKQjOv3zLFZqbC5ipkerKriGcVbsq3fOIMOi/55AM6/UhIAjeZGnoeco72JjQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-musl@0.121.0': resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3286,6 +3848,12 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-x64-musl@0.8.0': + resolution: {integrity: sha512-2j7BD9szwSXTvSj0Q8VE98UHGYvrgZzdLy4EyB0FilhQnopEfz+YV674rWGY2Il1VYxHJwGctrTJHvARolu37g==} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-openharmony-arm64@0.121.0': resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3303,6 +3871,12 @@ packages: cpu: [arm64] os: [win32] + '@oxc-parser/binding-win32-arm64-msvc@0.8.0': + resolution: {integrity: sha512-mcomr1og17yCmnwn8Q7CRzrH9Va0HccWe4Ld3/u/elBsw0SEzYGVvECRzCyRglYAbKTtusz7as9Jee0RiMOMmg==} + cpu: [arm64] + os: [win32] + libc: [null] + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3315,6 +3889,12 @@ packages: cpu: [x64] os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.8.0': + resolution: {integrity: sha512-nIBkc1KZOVYUaHT3+U+gM354P3byMAIXMvlmLMbs0kWVRcI4vrzL8qwWpC6QdBQxWKZGqPEqGolv8H4dDYA9nQ==} + cpu: [x64] + os: [win32] + libc: [null] + '@oxc-project/types@0.113.0': resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==} @@ -3438,6 +4018,10 @@ packages: '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} + '@parcel/source-map@2.1.1': + resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==} + engines: {node: ^12.18.3 || >=14} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -3564,6 +4148,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@preact/preset-vite@2.10.5': resolution: {integrity: sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==} peerDependencies: @@ -4805,6 +5394,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -5111,6 +5703,9 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -5127,6 +5722,10 @@ packages: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-truncate@5.2.0: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} @@ -5186,6 +5785,9 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + complain@1.6.1: + resolution: {integrity: sha512-WsiVAbAPcPSwyC5k8g/PLceaW0MXJUw61KqBd9uiyDOSuDIC6Ho/EbyhdFjeV6wq44R2g/b8IFpvV/1ZyXiqUQ==} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -5413,6 +6015,11 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -5466,6 +6073,13 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + draftlog@1.0.13: + resolution: {integrity: sha512-GeMWOpXERBpfVDK6v7m0x1hPg8+g8ZsZWqJl2T17wHqrm4h8fnjiZmXcnCrmwogAc6R3YTxFXax15wezfuyCUw==} + dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -5567,6 +6181,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -5586,6 +6203,9 @@ packages: resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -5605,11 +6225,23 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-plugin-browserslist@2.0.0: + resolution: {integrity: sha512-gm8EITyyfS3h5I+f/+6C+TFXI23PWi80vHtoccIA17GIoZDSSjHYZw+MINXrlQZ7DZ57myL0JAuDsKbUZlWQgw==} + engines: {node: ^20.19.0 || ^22.12.0 || >23.0.0} + peerDependencies: + browserslist: ^4.28.0 + esbuild: ~0.27.0 + esbuild-wasm@0.27.3: resolution: {integrity: sha512-AUXuOxZ145/5Az+lIqk6TdJbxKTyDGkXMJpTExmBdbnHR6n6qAFx+F4oG9ORpVYJ9dQYeQAqzv51TO4DFKsbXw==} engines: {node: '>=18'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -5906,6 +6538,10 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -6023,6 +6659,11 @@ packages: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6195,6 +6836,9 @@ packages: html-link-extractor@1.0.5: resolution: {integrity: sha512-ADd49pudM157uWHwHQPUSX4ssMsvR/yHIswOR5CUfBdK9g9ZYGMhVSE6KZVHJ6kCkR0gH4htsfzU6zECDNVwyw==} + htmljs-parser@5.11.0: + resolution: {integrity: sha512-YXdNqYB/FChJMpCG+UfVS/q/zfFErui92zIIduCFT71C3F6+lZWE4K6l5nGfFWQ2w4GCkgH/eEWQ1nWayBXIDQ==} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -6240,6 +6884,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-format@1.2.1: + resolution: {integrity: sha512-o5Ldz62VWR5lYUZ8aVQaLKiN37NsHnmk3xjMoUjza3mGkk8MvMofgZT0T6HKSCKSJIir+AWk9Dx8KhxvZAUgCg==} + engines: {node: '>=4'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -6691,6 +7339,12 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lasso-caching-fs@1.0.2: + resolution: {integrity: sha512-mudop0s8U3tLm3Fn9lhiZsiELpLeJToEo6RlDLdph7vWRxL9Sz0o+9WUw1IwlpCYXv/P0CLsMYWFgPwIKWEYvg==} + + lasso-package-root@1.0.1: + resolution: {integrity: sha512-j6LnauNCldqSDvOxoKpD6sTzudPGMiwcZQbySoF9KvJ0lD9Dp2t6QZF8kC0jbUDHuQPiAo5RuQ/mC3AGXscUYA==} + launch-editor@2.13.2: resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==} @@ -6924,6 +7578,10 @@ packages: engines: {node: '>= 20'} hasBin: true + marko@6.1.18: + resolution: {integrity: sha512-gBYHZwsGINVz+2QEhV+mreocTF9JDa/cSndJafp+yGH1v9pudOtYLdLuPMP4ZPnaWvJwuAfmfUF1Xul4s3UEuQ==} + engines: {node: '>=22'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -7325,6 +7983,9 @@ packages: resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} engines: {node: ^20.19.0 || >=22.12.0} + oxc-parser@0.8.0: + resolution: {integrity: sha512-ObPeMkbDX7igb7NyyAC8CbVC3fY+YmlMsxsRQ2oyFBkpQtI5tjoyqSDKbS9A9EcJvt2q89C4UoC+HjVBdLYYJg==} + oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} @@ -7386,6 +8047,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-node-args@1.1.3: + resolution: {integrity: sha512-Dp9KVLLq7g9TfGh3Uo8bv+0zW+YEKis+9J3d6KFp/GHkbqS5jd5xveavErhrOP2hsAmbCwdKFBTxBlNxXMK6vg==} + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -7474,6 +8138,16 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -7573,6 +8247,9 @@ packages: engines: {node: '>=6'} hasBin: true + prettier-plugin-marko@4.0.9: + resolution: {integrity: sha512-njqNk0oF6jolUMXNUOSlvRe6Dd50DN4NvQk3EY/6hUrU4tJw/HSwvhDF1uTYVs5KjOY5lH+x5C13C1Av9jajWA==} + prettier-plugin-svelte@3.5.1: resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} peerDependencies: @@ -7666,6 +8343,15 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raptor-async@1.1.3: + resolution: {integrity: sha512-VZCxygWMjW9lKqnApK9D2QbfyzRn7ehiTqnXWwMCLBXANSy+xbnYfbX/5f8YX3bZXu+g+JESmqWPchIQrZj2ig==} + + raptor-regexp@1.0.1: + resolution: {integrity: sha512-DqC7ViHJUs3jLIxJI1/HVvCu3yPJaP8CM7PGsHvjimg7yJ3lLOdCBxlPE0G2Q8OJgUA8Pe7nvhm6lcQ3hZepow==} + + raptor-util@3.2.0: + resolution: {integrity: sha512-uEDMMkBCJvjTqYMBnJNxn+neiS6a0rhybQNA9RaexGor1uvKjwyHA5VcbZMZEuqXhKUWbL+WNS7PhuZVZNB7pw==} + raw-body@2.5.3: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} @@ -7751,6 +8437,9 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true + relative-import-path@1.0.0: + resolution: {integrity: sha512-ZvbtoduKQmD4PZeJPfH6Ql21qUWhaMxiHkIsH+FUnZqKDwNIXBtGg5zRZyHWomiGYk8n5+KMBPK7Mi4D0XWfNg==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -7939,6 +8628,10 @@ packages: select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + self-closing-tags@1.0.1: + resolution: {integrity: sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA==} + engines: {node: '>=0.12.0'} + selfsigned@5.5.0: resolution: {integrity: sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==} engines: {node: '>=18'} @@ -8195,6 +8888,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -8278,6 +8974,10 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8564,6 +9264,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -8675,6 +9380,46 @@ packages: peerDependencies: vite: 5.x || 6.x || 7.x || 8.x + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + 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.3.2: resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -8860,6 +9605,9 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + warp10@2.1.0: + resolution: {integrity: sha512-krhkqzJdUxAZv2Cx0Gz6dN1r7TTrG9RDewkDHBbJQIqbNTCdB5ZUHVh7VkA4DgrKW4ZXPPUQKCwmI/3btDse9A==} + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -9206,13 +9954,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@analogjs/vite-plugin-angular@2.4.5(cd1678fbfdd77b23b6bdbf3f09017a19)': + '@analogjs/vite-plugin-angular@2.4.5(981d0923643984e7df52ebd265f97d33)': dependencies: tinyglobby: 0.2.16 ts-morph: 21.0.1 optionalDependencies: - '@angular-devkit/build-angular': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) - '@angular/build': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.14)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) + '@angular-devkit/build-angular': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3) + '@angular/build': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.14)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3) '@angular-devkit/architect@0.2102.7(chokidar@5.0.0)': dependencies: @@ -9276,6 +10024,93 @@ snapshots: typescript: 6.0.2 webpack: 5.105.2(esbuild@0.27.7) webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.7)) + webpack-merge: 6.0.1 + webpack-subresource-integrity: 5.1.0(webpack@5.105.2(esbuild@0.27.3)) + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + esbuild: 0.27.3 + transitivePeerDependencies: + - '@angular/compiler' + - '@emnapi/core' + - '@emnapi/runtime' + - '@rspack/core' + - '@swc/core' + - '@types/node' + - bufferutil + - chokidar + - debug + - html-webpack-plugin + - jiti + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - tsx + - uglify-js + - utf-8-validate + - vitest + - webpack-cli + - yaml + + '@angular-devkit/build-angular@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)))(webpack@5.105.2(esbuild@0.27.3)) + '@angular-devkit/core': 21.2.7(chokidar@5.0.0) + '@angular/build': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.6)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3) + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3) + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.0(@babel/core@7.29.0) + '@babel/runtime': 7.28.6 + '@discoveryjs/json-ext': 0.6.3 + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(typescript@6.0.3)(webpack@5.105.2(esbuild@0.27.3)) + ansi-colors: 4.1.3 + autoprefixer: 10.4.27(postcss@8.5.6) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(esbuild@0.27.3)) + browserslist: 4.28.2 + copy-webpack-plugin: 14.0.0(webpack@5.105.2(esbuild@0.27.3)) + css-loader: 7.1.3(webpack@5.105.2(esbuild@0.27.3)) + esbuild-wasm: 0.27.3 + http-proxy-middleware: 3.0.5 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + karma-source-map-support: 1.4.0 + less: 4.4.2 + less-loader: 12.3.1(less@4.4.2)(webpack@5.105.2(esbuild@0.27.3)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(esbuild@0.27.3)) + loader-utils: 3.3.1 + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(esbuild@0.27.3)) + open: 11.0.0 + ora: 9.3.0 + picomatch: 4.0.4 + piscina: 5.1.4 + postcss: 8.5.6 + postcss-loader: 8.2.0(postcss@8.5.6)(typescript@6.0.3)(webpack@5.105.2(esbuild@0.27.3)) + resolve-url-loader: 5.0.0 + rxjs: 7.8.2 + sass: 1.97.3 + sass-loader: 16.0.7(sass@1.97.3)(webpack@5.105.2(esbuild@0.27.3)) + semver: 7.7.4 + source-map-loader: 5.0.0(webpack@5.105.2(esbuild@0.27.3)) + source-map-support: 0.5.21 + terser: 5.46.0 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + tslib: 2.8.1 + typescript: 6.0.3 + webpack: 5.105.2(esbuild@0.27.3) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.105.2(esbuild@0.27.3)) @@ -9307,12 +10142,13 @@ snapshots: - vitest - webpack-cli - yaml + optional: true '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)))(webpack@5.105.2(esbuild@0.27.3))': dependencies: '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) rxjs: 7.8.2 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) transitivePeerDependencies: - chokidar @@ -9343,7 +10179,7 @@ snapshots: '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - '@angular/build@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.14)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3)': + '@angular/build@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.6)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) @@ -9376,6 +10212,61 @@ snapshots: undici: 7.24.4 vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) watchpack: 2.5.1 + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + less: 4.4.2 + lmdb: 3.5.1 + postcss: 8.5.6 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@angular/build@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.14)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular/compiler': 21.2.8 + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3) + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.21(@types/node@25.6.0) + '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + beasties: 0.4.1 + browserslist: 4.28.2 + esbuild: 0.27.3 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.4 + rolldown: 1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + sass: 1.97.3 + semver: 7.7.4 + source-map-support: 0.5.21 + tinyglobby: 0.2.15 + tslib: 2.8.1 + typescript: 6.0.3 + undici: 7.24.4 + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + watchpack: 2.5.1 optionalDependencies: '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) @@ -9399,12 +10290,12 @@ snapshots: - yaml optional: true - '@angular/build@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.6)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3)': + '@angular/build@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(postcss@8.5.6)(terser@5.46.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@4.1.4)(yaml@2.8.3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) '@angular/compiler': 21.2.8 - '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3) '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 @@ -9428,7 +10319,7 @@ snapshots: source-map-support: 0.5.21 tinyglobby: 0.2.15 tslib: 2.8.1 - typescript: 6.0.2 + typescript: 6.0.3 undici: 7.24.4 vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) watchpack: 2.5.1 @@ -9438,7 +10329,7 @@ snapshots: less: 4.4.2 lmdb: 3.5.1 postcss: 8.5.6 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -9453,6 +10344,7 @@ snapshots: - terser - tsx - yaml + optional: true '@angular/cli@21.2.7(@types/node@25.6.0)(chokidar@5.0.0)': dependencies: @@ -9502,6 +10394,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3)': + dependencies: + '@angular/compiler': 21.2.8 + '@babel/core': 7.29.0 + '@jridgewell/sourcemap-codec': 1.5.5 + chokidar: 5.0.0 + convert-source-map: 1.9.0 + reflect-metadata: 0.2.2 + semver: 7.7.4 + tslib: 2.8.1 + yargs: 18.0.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + '@angular/compiler@21.2.8': dependencies: tslib: 2.8.1 @@ -9575,7 +10484,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -9644,7 +10553,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) lodash.debounce: 4.0.8 resolve: 1.22.11 transitivePeerDependencies: @@ -10274,7 +11183,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -10442,6 +11351,17 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@chialab/estransform@0.19.1': + dependencies: + '@napi-rs/magic-string': 0.3.4 + '@parcel/source-map': 2.1.1 + cjs-module-lexer: 1.4.3 + es-module-lexer: 1.7.0 + oxc-parser: 0.8.0 + + '@colors/colors@1.5.0': + optional: true + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -10481,156 +11401,234 @@ snapshots: dependencies: tslib: 2.8.1 + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.27.3': optional: true '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true @@ -10728,7 +11726,7 @@ snapshots: '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -11125,6 +12123,8 @@ snapshots: '@lmdb/lmdb-win32-x64@3.5.1': optional: true + '@luxass/strip-json-comments@1.4.0': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -11141,6 +12141,93 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@marko/compiler@5.39.66': + dependencies: + '@luxass/strip-json-comments': 1.4.0 + complain: 1.6.1 + he: 1.2.0 + htmljs-parser: 5.11.0 + jsesc: 3.1.0 + kleur: 4.1.5 + lasso-package-root: 1.0.1 + raptor-regexp: 1.0.1 + raptor-util: 3.2.0 + relative-import-path: 1.0.0 + resolve-from: 5.0.0 + self-closing-tags: 1.0.1 + source-map-support: 0.5.21 + + '@marko/language-tools@2.6.0': + dependencies: + '@luxass/strip-json-comments': 1.4.0 + '@marko/compiler': 5.39.66 + htmljs-parser: 5.11.0 + relative-import-path: 1.0.0 + + '@marko/run-explorer@2.0.4(@marko/run@0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))': + dependencies: + '@marko/run': 0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + + '@marko/run@0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)': + dependencies: + '@marko/run-explorer': 2.0.4(@marko/run@0.10.0(@marko/compiler@5.39.66)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(marko@6.1.18)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + '@marko/vite': 6.1.0(@marko/compiler@5.39.66)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) + browserslist: 4.28.2 + cli-table3: 0.6.5 + compression: 1.8.1(supports-color@10.2.2) + debug: 4.4.3(supports-color@10.2.2) + dotenv: 17.4.2 + draftlog: 1.0.13 + esbuild-plugin-browserslist: 2.0.0(browserslist@4.28.2)(esbuild@0.27.7)(supports-color@10.2.2) + glob: 13.0.6 + human-format: 1.2.1 + kleur: 4.1.5 + marko: 6.1.18 + parse-node-args: 1.1.3 + sade: 1.8.1 + serve-static: 2.2.1(supports-color@10.2.2) + supports-color: 10.2.2 + vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + warp10: 2.1.0 + transitivePeerDependencies: + - '@marko/compiler' + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@marko/type-check@3.1.0': + dependencies: + '@luxass/strip-json-comments': 1.4.0 + '@marko/compiler': 5.39.66 + '@marko/language-tools': 2.6.0 + arg: 5.0.2 + kleur: 4.1.5 + typescript: 6.0.3 + + '@marko/vite@6.1.0(@marko/compiler@5.39.66)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))': + dependencies: + '@chialab/estransform': 0.19.1 + '@marko/compiler': 5.39.66 + anymatch: 3.1.3 + domelementtype: 2.3.0 + domhandler: 5.0.3 + fast-glob: 3.3.3 + htmljs-parser: 5.11.0 + htmlparser2: 10.1.0 + relative-import-path: 1.0.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@mdn/browser-compat-data@5.7.6': {} '@mdn/browser-compat-data@6.1.5': {} @@ -11185,6 +12272,61 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@napi-rs/magic-string-android-arm-eabi@0.3.4': + optional: true + + '@napi-rs/magic-string-android-arm64@0.3.4': + optional: true + + '@napi-rs/magic-string-darwin-arm64@0.3.4': + optional: true + + '@napi-rs/magic-string-darwin-x64@0.3.4': + optional: true + + '@napi-rs/magic-string-freebsd-x64@0.3.4': + optional: true + + '@napi-rs/magic-string-linux-arm-gnueabihf@0.3.4': + optional: true + + '@napi-rs/magic-string-linux-arm64-gnu@0.3.4': + optional: true + + '@napi-rs/magic-string-linux-arm64-musl@0.3.4': + optional: true + + '@napi-rs/magic-string-linux-x64-gnu@0.3.4': + optional: true + + '@napi-rs/magic-string-linux-x64-musl@0.3.4': + optional: true + + '@napi-rs/magic-string-win32-arm64-msvc@0.3.4': + optional: true + + '@napi-rs/magic-string-win32-ia32-msvc@0.3.4': + optional: true + + '@napi-rs/magic-string-win32-x64-msvc@0.3.4': + optional: true + + '@napi-rs/magic-string@0.3.4': + optionalDependencies: + '@napi-rs/magic-string-android-arm-eabi': 0.3.4 + '@napi-rs/magic-string-android-arm64': 0.3.4 + '@napi-rs/magic-string-darwin-arm64': 0.3.4 + '@napi-rs/magic-string-darwin-x64': 0.3.4 + '@napi-rs/magic-string-freebsd-x64': 0.3.4 + '@napi-rs/magic-string-linux-arm-gnueabihf': 0.3.4 + '@napi-rs/magic-string-linux-arm64-gnu': 0.3.4 + '@napi-rs/magic-string-linux-arm64-musl': 0.3.4 + '@napi-rs/magic-string-linux-x64-gnu': 0.3.4 + '@napi-rs/magic-string-linux-x64-musl': 0.3.4 + '@napi-rs/magic-string-win32-arm64-msvc': 0.3.4 + '@napi-rs/magic-string-win32-ia32-msvc': 0.3.4 + '@napi-rs/magic-string-win32-x64-msvc': 0.3.4 + '@napi-rs/nice-android-arm-eabi@1.1.1': optional: true @@ -11283,6 +12425,13 @@ snapshots: typescript: 6.0.2 webpack: 5.105.2(esbuild@0.27.7) + '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3))(typescript@6.0.3)(webpack@5.105.2(esbuild@0.27.3))': + dependencies: + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.3) + typescript: 6.0.3 + webpack: 5.105.2(esbuild@0.27.3) + optional: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -11400,9 +12549,15 @@ snapshots: '@oxc-parser/binding-darwin-arm64@0.121.0': optional: true + '@oxc-parser/binding-darwin-arm64@0.8.0': + optional: true + '@oxc-parser/binding-darwin-x64@0.121.0': optional: true + '@oxc-parser/binding-darwin-x64@0.8.0': + optional: true + '@oxc-parser/binding-freebsd-x64@0.121.0': optional: true @@ -11415,9 +12570,15 @@ snapshots: '@oxc-parser/binding-linux-arm64-gnu@0.121.0': optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.8.0': + optional: true + '@oxc-parser/binding-linux-arm64-musl@0.121.0': optional: true + '@oxc-parser/binding-linux-arm64-musl@0.8.0': + optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': optional: true @@ -11433,9 +12594,15 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu@0.121.0': optional: true + '@oxc-parser/binding-linux-x64-gnu@0.8.0': + optional: true + '@oxc-parser/binding-linux-x64-musl@0.121.0': optional: true + '@oxc-parser/binding-linux-x64-musl@0.8.0': + optional: true + '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true @@ -11450,12 +12617,18 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.121.0': optional: true + '@oxc-parser/binding-win32-arm64-msvc@0.8.0': + optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': optional: true '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true + '@oxc-parser/binding-win32-x64-msvc@0.8.0': + optional: true + '@oxc-project/types@0.113.0': {} '@oxc-project/types@0.121.0': {} @@ -11531,6 +12704,10 @@ snapshots: '@package-json/types@0.0.12': {} + '@parcel/source-map@2.1.1': + dependencies: + detect-libc: 1.0.3 + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -11685,6 +12862,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.4)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 @@ -11693,7 +12874,7 @@ snapshots: '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.4) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) magic-string: 0.30.21 picocolors: 1.1.1 vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) @@ -12061,14 +13242,14 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/package@2.5.7(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.2)': + '@sveltejs/package@2.5.7(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3)': dependencies: chokidar: 5.0.0 kleur: 4.1.5 sade: 1.8.1 semver: 7.7.4 svelte: 5.55.7(@typescript-eslint/types@8.58.1) - svelte2tsx: 0.7.53(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.2) + svelte2tsx: 0.7.53(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3) transitivePeerDependencies: - typescript @@ -12186,12 +13367,12 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.2))': + '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.3))': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 9.3.4 '@vue/test-utils': 2.4.6 - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) optionalDependencies: '@vue/compiler-sfc': 3.5.32 @@ -12383,15 +13564,43 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 10.3.0(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/scope-manager': 8.58.1 '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.3.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 10.3.0(jiti@2.6.1) - typescript: 6.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -12399,11 +13608,20 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) '@typescript-eslint/types': 8.58.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: 6.0.2 transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.58.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.3) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3(supports-color@10.2.2) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 @@ -12413,18 +13631,34 @@ snapshots: dependencies: typescript: 6.0.2 + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + '@typescript-eslint/type-utils@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/types': 8.58.1 '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) '@typescript-eslint/utils': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.2) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 10.3.0(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.3.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.58.1': {} '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2)': @@ -12433,7 +13667,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) '@typescript-eslint/types': 8.58.1 '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.16 @@ -12442,6 +13676,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.3) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1)) @@ -12453,6 +13702,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.3) + eslint: 10.3.0(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 @@ -12532,6 +13792,12 @@ snapshots: vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) vue: 3.5.32(typescript@6.0.2) + '@vitejs/plugin-vue@6.0.5(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + vue: 3.5.32(typescript@6.0.3) + '@vitest/coverage-istanbul@4.1.4(vitest@4.1.4)': dependencies: '@babel/core': 7.29.0 @@ -12648,9 +13914,9 @@ snapshots: '@vue/compiler-dom': 3.5.32 '@vue/shared': 3.5.32 - '@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.2))': + '@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.3))': dependencies: - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) '@vue/language-core@3.2.6': dependencies: @@ -12684,6 +13950,12 @@ snapshots: '@vue/shared': 3.5.32 vue: 3.5.32(typescript@6.0.2) + '@vue/server-renderer@3.5.32(vue@3.5.32(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.32 + '@vue/shared': 3.5.32 + vue: 3.5.32(typescript@6.0.3) + '@vue/shared@3.5.32': {} '@vue/test-utils@2.4.6': @@ -12897,6 +14169,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -13025,7 +14299,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.29.0): dependencies: @@ -13127,7 +14401,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -13144,7 +14418,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -13295,6 +14569,8 @@ snapshots: chrome-trace-event@1.0.4: {} + cjs-module-lexer@1.4.3: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -13307,6 +14583,12 @@ snapshots: cli-spinners@3.4.0: {} + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-truncate@5.2.0: dependencies: slice-ansi: 8.0.0 @@ -13358,15 +14640,19 @@ snapshots: compare-versions@6.1.1: {} + complain@1.6.1: + dependencies: + error-stack-parser: 2.1.4 + compressible@2.0.18: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@10.2.2): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -13412,7 +14698,7 @@ snapshots: schema-utils: 4.3.3 serialize-javascript: 7.0.5 tinyglobby: 0.2.16 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) core-js-compat@3.49.0: dependencies: @@ -13434,6 +14720,16 @@ snapshots: optionalDependencies: typescript: 6.0.2 + cosmiconfig@9.0.1(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -13451,7 +14747,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) css-select@5.2.2: dependencies: @@ -13511,13 +14807,17 @@ snapshots: dataloader@1.4.0: {} - debug@2.6.9: + debug@2.6.9(supports-color@10.2.2): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 10.2.2 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 decimal.js@10.6.0: {} @@ -13589,6 +14889,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@1.0.3: {} + detect-libc@2.1.2: {} detect-node@2.1.0: {} @@ -13637,6 +14939,10 @@ snapshots: dotenv@16.6.1: {} + dotenv@17.4.2: {} + + draftlog@1.0.13: {} + dts-resolver@2.1.3(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)): optionalDependencies: oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -13718,6 +15024,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -13811,6 +15121,8 @@ snapshots: math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -13834,8 +15146,46 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-plugin-browserslist@2.0.0(browserslist@4.28.2)(esbuild@0.27.7)(supports-color@10.2.2): + dependencies: + browserslist: 4.28.2 + debug: 4.4.3(supports-color@10.2.2) + esbuild: 0.27.7 + zod: 4.3.6 + transitivePeerDependencies: + - supports-color + esbuild-wasm@0.27.3: {} + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -13952,7 +15302,7 @@ snapshots: '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.58.1 comment-parser: 1.4.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 10.3.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 @@ -14177,7 +15527,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -14274,7 +15624,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -14309,7 +15659,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -14326,8 +15676,8 @@ snapshots: qs: 6.15.0 range-parser: 1.2.1 router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + send: 1.2.1(supports-color@10.2.2) + serve-static: 2.2.1(supports-color@10.2.2) statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -14352,6 +15702,8 @@ snapshots: fast-uri@3.1.2: {} + fastest-levenshtein@1.0.16: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -14382,7 +15734,7 @@ snapshots: finalhandler@1.3.2: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -14394,7 +15746,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -14424,7 +15776,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) for-each@0.3.5: dependencies: @@ -14477,6 +15829,9 @@ snapshots: dependencies: minipass: 7.1.3 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -14649,6 +16004,8 @@ snapshots: dependencies: cheerio: 1.2.0 + htmljs-parser@5.11.0: {} + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -14681,7 +16038,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -14700,7 +16057,7 @@ snapshots: http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.17 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -14719,10 +16076,12 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color + human-format@1.2.1: {} + human-id@4.1.3: {} hyperdyperid@1.2.0: {} @@ -15150,6 +16509,14 @@ snapshots: kolorist@1.8.0: {} + lasso-caching-fs@1.0.2: + dependencies: + raptor-async: 1.1.3 + + lasso-package-root@1.0.1: + dependencies: + lasso-caching-fs: 1.0.2 + launch-editor@2.13.2: dependencies: picocolors: 1.1.1 @@ -15159,7 +16526,7 @@ snapshots: dependencies: less: 4.4.2 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) less@4.4.2: dependencies: @@ -15184,7 +16551,7 @@ snapshots: dependencies: webpack-sources: 3.3.4 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) lightningcss-android-arm64@1.32.0: optional: true @@ -15401,6 +16768,13 @@ snapshots: marked@17.0.5: {} + marko@6.1.18: + dependencies: + '@marko/compiler': 5.39.66 + csstype: 3.2.3 + fastest-levenshtein: 1.0.16 + magic-string: 0.30.21 + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} @@ -15471,7 +16845,7 @@ snapshots: dependencies: schema-utils: 4.3.3 tapable: 2.3.2 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) minimalistic-assert@1.0.1: {} @@ -15894,6 +17268,17 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + oxc-parser@0.8.0: + optionalDependencies: + '@oxc-parser/binding-darwin-arm64': 0.8.0 + '@oxc-parser/binding-darwin-x64': 0.8.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.8.0 + '@oxc-parser/binding-linux-arm64-musl': 0.8.0 + '@oxc-parser/binding-linux-x64-gnu': 0.8.0 + '@oxc-parser/binding-linux-x64-musl': 0.8.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.8.0 + '@oxc-parser/binding-win32-x64-msvc': 0.8.0 + oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 @@ -15993,6 +17378,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-node-args@1.1.3: + dependencies: + '@babel/runtime': 7.29.2 + parse-node-version@1.0.1: {} parse5-html-rewriting-stream@8.0.0: @@ -16073,6 +17462,14 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss-load-config@3.1.4(postcss@8.5.14): @@ -16093,6 +17490,18 @@ snapshots: transitivePeerDependencies: - typescript + postcss-loader@8.2.0(postcss@8.5.6)(typescript@6.0.3)(webpack@5.105.2(esbuild@0.27.3)): + dependencies: + cosmiconfig: 9.0.1(typescript@6.0.3) + jiti: 2.6.1 + postcss: 8.5.6 + semver: 7.7.4 + optionalDependencies: + webpack: 5.105.2(esbuild@0.27.3) + transitivePeerDependencies: + - typescript + optional: true + postcss-media-query-parser@0.2.3: {} postcss-modules-extract-imports@3.1.0(postcss@8.5.14): @@ -16151,6 +17560,10 @@ snapshots: premove@4.0.0: {} + prettier-plugin-marko@4.0.9: + dependencies: + htmljs-parser: 5.11.0 + prettier-plugin-svelte@3.5.1(prettier@3.8.2)(svelte@5.55.7(@typescript-eslint/types@8.58.1)): dependencies: prettier: 3.8.2 @@ -16232,6 +17645,12 @@ snapshots: range-parser@1.2.1: {} + raptor-async@1.1.3: {} + + raptor-regexp@1.0.1: {} + + raptor-util@3.2.0: {} + raw-body@2.5.3: dependencies: bytes: 3.1.2 @@ -16340,6 +17759,8 @@ snapshots: dependencies: jsesc: 3.1.0 + relative-import-path@1.0.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -16514,7 +17935,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -16566,7 +17987,7 @@ snapshots: neo-async: 2.6.2 optionalDependencies: sass: 1.97.3 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) sass@1.97.3: dependencies: @@ -16596,6 +18017,8 @@ snapshots: select-hose@2.0.0: {} + self-closing-tags@1.0.1: {} + selfsigned@5.5.0: dependencies: '@peculiar/x509': 1.14.3 @@ -16610,7 +18033,7 @@ snapshots: send@0.19.2: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -16626,9 +18049,9 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: + send@1.2.1(supports-color@10.2.2): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -16654,7 +18077,7 @@ snapshots: dependencies: accepts: 1.3.8 batch: 0.6.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@10.2.2) escape-html: 1.0.3 http-errors: 1.8.1 mime-types: 2.1.35 @@ -16671,12 +18094,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@10.2.2): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -16825,7 +18248,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -16856,7 +18279,7 @@ snapshots: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) source-map-support@0.5.21: dependencies: @@ -16883,7 +18306,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -16894,7 +18317,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -16914,6 +18337,8 @@ snapshots: stackback@0.0.2: {} + stackframe@1.3.4: {} + statuses@1.5.0: {} statuses@2.0.2: {} @@ -17020,6 +18445,8 @@ snapshots: strip-json-comments@5.0.3: {} + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -17042,6 +18469,18 @@ snapshots: transitivePeerDependencies: - picomatch + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.7(@typescript-eslint/types@8.58.1) + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + svelte-eslint-parser@1.6.0(svelte@5.55.7(@typescript-eslint/types@8.58.1)): dependencies: eslint-scope: 8.4.0 @@ -17054,12 +18493,12 @@ snapshots: optionalDependencies: svelte: 5.55.7(@typescript-eslint/types@8.58.1) - svelte2tsx@0.7.53(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.2): + svelte2tsx@0.7.53(svelte@5.55.7(@typescript-eslint/types@8.58.1))(typescript@6.0.3): dependencies: dedent-js: 1.0.1 scule: 1.3.0 svelte: 5.55.7(@typescript-eslint/types@8.58.1) - typescript: 6.0.2 + typescript: 6.0.3 svelte@5.55.7(@typescript-eslint/types@8.58.1): dependencies: @@ -17104,6 +18543,16 @@ snapshots: term-size@2.2.1: {} + terser-webpack-plugin@5.4.0(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.0 + webpack: 5.105.2(esbuild@0.27.3) + optionalDependencies: + esbuild: 0.27.3 + terser-webpack-plugin@5.4.0(esbuild@0.27.7)(webpack@5.105.2(esbuild@0.27.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -17177,6 +18626,10 @@ snapshots: dependencies: typescript: 6.0.2 + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + ts-declaration-location@1.0.7(typescript@6.0.2): dependencies: picomatch: 4.0.4 @@ -17236,7 +18689,7 @@ snapshots: tuf-js@4.1.0: dependencies: '@tufjs/models': 4.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) make-fetch-happen: 15.0.5 transitivePeerDependencies: - supports-color @@ -17320,6 +18773,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + typescript@5.6.3: {} typescript@5.7.3: {} @@ -17328,6 +18792,8 @@ snapshots: typescript@6.0.2: {} + typescript@6.0.3: {} + uc.micro@2.1.0: {} unbash@2.2.0: {} @@ -17445,6 +18911,24 @@ snapshots: stack-trace: 1.0.0-pre2 vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + lightningcss: 1.32.0 + sass: 1.97.3 + terser: 5.46.0 + yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3): dependencies: esbuild: 0.27.7 @@ -17547,15 +19031,15 @@ snapshots: vue-component-type-helpers@2.2.12: {} - vue-demi@0.14.10(@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.2)))(vue@3.5.32(typescript@6.0.2)): + vue-demi@0.14.10(@vue/composition-api@1.7.2(vue@3.5.32(typescript@6.0.3)))(vue@3.5.32(typescript@6.0.3)): dependencies: - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) optionalDependencies: - '@vue/composition-api': 1.7.2(vue@3.5.32(typescript@6.0.2)) + '@vue/composition-api': 1.7.2(vue@3.5.32(typescript@6.0.3)) vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 10.3.0(jiti@2.6.1) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -17588,12 +19072,24 @@ snapshots: optionalDependencies: typescript: 6.0.2 + vue@3.5.32(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.32 + '@vue/compiler-sfc': 3.5.32 + '@vue/runtime-dom': 3.5.32 + '@vue/server-renderer': 3.5.32(vue@3.5.32(typescript@6.0.3)) + '@vue/shared': 3.5.32 + optionalDependencies: + typescript: 6.0.3 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 walk-up-path@4.0.0: {} + warp10@2.1.0: {} + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -17615,6 +19111,19 @@ snapshots: webidl-conversions@8.0.1: {} webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)): + dependencies: + colorette: 2.0.20 + memfs: 4.57.1(tslib@2.8.1) + mime-types: 3.0.2 + on-finished: 2.4.1 + range-parser: 1.2.1 + schema-utils: 4.3.3 + optionalDependencies: + webpack: 5.105.2(esbuild@0.27.3) + transitivePeerDependencies: + - tslib + + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.7)): dependencies: colorette: 2.0.20 memfs: 4.57.1(tslib@2.8.1) @@ -17641,7 +19150,7 @@ snapshots: bonjour-service: 1.3.0 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.8.1 + compression: 1.8.1(supports-color@10.2.2) connect-history-api-fallback: 2.0.0 express: 4.22.1 graceful-fs: 4.2.11 @@ -17657,6 +19166,45 @@ snapshots: spdy: 4.0.2 webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) ws: 8.20.0 + optionalDependencies: + webpack: 5.105.2(esbuild@0.27.3) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - tslib + - utf-8-validate + + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.7)): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/express-serve-static-core': 4.19.8 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1(supports-color@10.2.2) + connect-history-api-fallback: 2.0.0 + express: 4.22.1 + graceful-fs: 4.2.11 + http-proxy-middleware: 2.0.9(@types/express@4.17.25) + ipaddr.js: 2.3.0 + launch-editor: 2.13.2 + open: 10.2.0 + p-retry: 6.2.1 + schema-utils: 4.3.3 + selfsigned: 5.5.0 + serve-index: 1.9.2 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.7)) + ws: 8.20.0 optionalDependencies: webpack: 5.105.2(esbuild@0.27.7) transitivePeerDependencies: @@ -17677,7 +19225,39 @@ snapshots: webpack-subresource-integrity@5.1.0(webpack@5.105.2(esbuild@0.27.3)): dependencies: typed-assert: 1.0.9 - webpack: 5.105.2(esbuild@0.27.7) + webpack: 5.105.2(esbuild@0.27.3) + + webpack@5.105.2(esbuild@0.27.3): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js webpack@5.105.2(esbuild@0.27.7): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bd664ffb..38b3ba78 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,14 @@ preferWorkspacePackages: true blockExoticSubdeps: true trustPolicy: 'no-downgrade' +overrides: + # Exactly one @marko/compiler in the graph. marko, @marko/vite, @marko/run, + # and @marko/language-tools (via @marko/type-check) all accept ^5.39/^5.40 + # ranges; letting the lockfile resolve them independently can split versions + # across entries, and two compiler instances in one process fail every .marko + # compile with "Unable to access Marko File outside of a compilation". + '@marko/compiler': 5.39.66 + peerDependencyRules: allowedVersions: '@vue/composition-api>vue': '>= 2.5' @@ -17,6 +25,7 @@ packages: - 'examples/svelte/**' - 'examples/preact/**' - 'examples/lit/**' + - 'examples/marko/**' allowBuilds: # root dependency