From 8eaa0decc9f6f98bc29eb6ea9d7a794d8e7aa921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20D=C3=ADez?= Date: Sun, 15 Dec 2024 00:23:41 +0100 Subject: [PATCH] feat(transfer-state): new library for managing state in an isolated way with server to client transfer (#4) * feat(transfer-state): new library for managing state in a n isolated way * feat(transfer-state): add dev toolbar integration to view the transferred state * ci: add validation workflow * ci: fix name of validate action --- .changeset/popular-wasps-sniff.md | 5 + .github/workflows/validate.yml | 37 ++++++ apps/docs/astro.config.mjs | 8 +- apps/docs/package.json | 1 + .../after-hydration/Example.astro | 16 +++ .../after-hydration/Example.svelte | 21 ++++ .../before-hydration/Example.astro | 16 +++ .../before-hydration/Example.svelte | 19 +++ .../_examples/transfer-state/setup.ts | 9 ++ .../transfer-state/transfer-state-toolbar.png | Bin 0 -> 22455 bytes .../docs/state-management/transfer-state.mdx | 115 ++++++++++++++++++ .../docs-utils/src/components/Output.scss | 8 ++ .../docs-utils/src/components/Output.svelte | 10 ++ packages/docs-utils/src/components/index.ts | 2 + packages/transfer-state/package.json | 51 ++++++++ packages/transfer-state/src/index.ts | 54 ++++++++ packages/transfer-state/src/middleware.ts | 20 +++ packages/transfer-state/src/state/client.ts | 21 ++++ packages/transfer-state/src/state/server.ts | 19 +++ packages/transfer-state/src/toolbar/icon.ts | 1 + packages/transfer-state/src/toolbar/index.ts | 56 +++++++++ .../src/transfer-state-storage.ts | 5 + packages/transfer-state/src/transfer-state.ts | 1 + .../transfer-state/src/with-transfer-state.ts | 11 ++ packages/transfer-state/tsconfig.json | 9 ++ packages/transfer-state/tsup.config.ts | 19 +++ pnpm-lock.yaml | 71 +++++++++++ 27 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 .changeset/popular-wasps-sniff.md create mode 100644 .github/workflows/validate.yml create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.astro create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.svelte create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.astro create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.svelte create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/setup.ts create mode 100644 apps/docs/src/content/docs/state-management/_examples/transfer-state/transfer-state-toolbar.png create mode 100644 apps/docs/src/content/docs/state-management/transfer-state.mdx create mode 100644 packages/docs-utils/src/components/Output.scss create mode 100644 packages/docs-utils/src/components/Output.svelte create mode 100644 packages/transfer-state/package.json create mode 100644 packages/transfer-state/src/index.ts create mode 100644 packages/transfer-state/src/middleware.ts create mode 100644 packages/transfer-state/src/state/client.ts create mode 100644 packages/transfer-state/src/state/server.ts create mode 100644 packages/transfer-state/src/toolbar/icon.ts create mode 100644 packages/transfer-state/src/toolbar/index.ts create mode 100644 packages/transfer-state/src/transfer-state-storage.ts create mode 100644 packages/transfer-state/src/transfer-state.ts create mode 100644 packages/transfer-state/src/with-transfer-state.ts create mode 100644 packages/transfer-state/tsconfig.json create mode 100644 packages/transfer-state/tsup.config.ts diff --git a/.changeset/popular-wasps-sniff.md b/.changeset/popular-wasps-sniff.md new file mode 100644 index 0000000..900ba2e --- /dev/null +++ b/.changeset/popular-wasps-sniff.md @@ -0,0 +1,5 @@ +--- +"@astro-tools/transfer-state": minor +--- + +Initial version of transfer-state package: a state management integration for having request isolated state transferred from server to client diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..21c5d70 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,37 @@ +name: Validate + +on: + pull_request: + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + validate: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Install dependencies + run: pnpm install + + - name: Build packages + run: pnpm exec turbo build --filter="./packages/*" diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 9c4702d..5d0df7b 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,6 +1,7 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import svelte from '@astrojs/svelte'; +import { transferState } from '@astro-tools/transfer-state'; import { onClientDirective } from '@astro-tools/client-directives/on'; import { eventClientDirective } from '@astro-tools/client-directives/event'; import { clickClientDirective } from '@astro-tools/client-directives/click'; @@ -19,13 +20,18 @@ export default defineConfig({ { label: 'Client directives', autogenerate: { directory: 'client-directives' }, - } + }, + { + label: 'State management', + autogenerate: { directory: 'state-management' }, + } ], customCss: [ './src/styles/theme.scss', ], }), svelte(), + transferState(), onClientDirective({ directives: [ { name: 'event', entrypoint: '@astro-tools/client-directives/event/directive' }, diff --git a/apps/docs/package.json b/apps/docs/package.json index 151a1b6..5d4335d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -19,6 +19,7 @@ "@astrojs/svelte": "^6.0.2", "@astro-tools/client-directives": "workspace:*", "@astro-tools/docs-utils": "workspace:*", + "@astro-tools/transfer-state": "workspace:*", "svelte": "^5.8.0", "sass-embedded": "^1.82.0" } diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.astro b/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.astro new file mode 100644 index 0000000..2beca5f --- /dev/null +++ b/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.astro @@ -0,0 +1,16 @@ +--- +import { setState } from '@astro-tools:transfer-state'; + +import ExampleComponent from './Example.svelte'; + +interface Props { + id: string; +} + +const { id } = Astro.props; + +setState('uuid-after-hydration', 'bed6fb83-0dd9-4566-a675-55052529f18e'); +--- + +
+ diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.svelte b/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.svelte new file mode 100644 index 0000000..57b1177 --- /dev/null +++ b/apps/docs/src/content/docs/state-management/_examples/transfer-state/after-hydration/Example.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.astro b/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.astro new file mode 100644 index 0000000..94771f5 --- /dev/null +++ b/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.astro @@ -0,0 +1,16 @@ +--- +import { setState } from '@astro-tools:transfer-state'; + +import ExampleComponent from './Example.svelte'; + +interface Props { + id: string; +} + +const { id } = Astro.props; + +setState('uuid', 'fc379108-c24e-47f5-b119-45db86e0e94a'); +--- + +
+ diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.svelte b/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.svelte new file mode 100644 index 0000000..9f55da6 --- /dev/null +++ b/apps/docs/src/content/docs/state-management/_examples/transfer-state/before-hydration/Example.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/setup.ts b/apps/docs/src/content/docs/state-management/_examples/transfer-state/setup.ts new file mode 100644 index 0000000..4f8671a --- /dev/null +++ b/apps/docs/src/content/docs/state-management/_examples/transfer-state/setup.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'astro/config'; + +import { transferState } from '@astro-tools/transfer-state'; + +export default defineConfig({ + integrations: [ + transferState(), + ], +}); diff --git a/apps/docs/src/content/docs/state-management/_examples/transfer-state/transfer-state-toolbar.png b/apps/docs/src/content/docs/state-management/_examples/transfer-state/transfer-state-toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..0814b629477fb7b8c1d33384c862afe15c9d8490 GIT binary patch literal 22455 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 this integration will disable stream rendering because the state must be available before executing any client script. + +## Setup + +For setting up the state management with request isolation and transfer state, include the integration in your Astro project. + + +1. Install the library using your preferred package manager: + ``` + npm i -D @astro-tools/transfer-state + ``` +2. Add the integration to your project configuration: + + + +## Use + +For using the state management, just use `setState` and `getState` methods: + +### setState +Use `setState` with a key to save the value (JSON-like) into the state. Keys can be removed using `null` as value. + +```typescript +import type { MyData } from './my-data'; + +const myData: MyData = { name: 'example' }; +setState('my-data', myData); +``` + +### getState +Use `getState` to get the current value of a key. When there is no value, `null` will be returned. + +```typescript +import type { MyData } from './my-data'; + +const myData = getState('my-data'); +``` + + + +## Debugging + +This integration adds a new option into the Astro Dev Toolbar which allows to easily check the transferred state from the server to the client. Find the icon highlighed in the image below and click it to toggle the state viewer: +![Transfer State Astro Dev Toolbar integration](./_examples/transfer-state/transfer-state-toolbar.png) + + + +## Examples + +The state can be used to render server-side UI framework components, like the one in this example. + +The `Example.svelte` is rendered in server-side using the `uuid` state and transferred to the client. The hydration process keeps the value is it is. + + + + + + + + + + + + + + + + + + +Also, the state value can be recovered at any moment, for example, after hydration or any logic you want. + + + + + + + + + + + + + + + + + + diff --git a/packages/docs-utils/src/components/Output.scss b/packages/docs-utils/src/components/Output.scss new file mode 100644 index 0000000..77f670a --- /dev/null +++ b/packages/docs-utils/src/components/Output.scss @@ -0,0 +1,8 @@ +.output { + color: var(--at-color-hydration-off); + transition: color 200ms ease; +} + +.output--hydrated { + color: var(--at-color-hydration-on); +} diff --git a/packages/docs-utils/src/components/Output.svelte b/packages/docs-utils/src/components/Output.svelte new file mode 100644 index 0000000..f6bb344 --- /dev/null +++ b/packages/docs-utils/src/components/Output.svelte @@ -0,0 +1,10 @@ + + +
{text}
+ + diff --git a/packages/docs-utils/src/components/index.ts b/packages/docs-utils/src/components/index.ts index fedacea..4073d24 100644 --- a/packages/docs-utils/src/components/index.ts +++ b/packages/docs-utils/src/components/index.ts @@ -1,5 +1,7 @@ import Square from './Square.svelte'; +import Output from './Output.svelte'; export { Square, + Output, } diff --git a/packages/transfer-state/package.json b/packages/transfer-state/package.json new file mode 100644 index 0000000..640dd00 --- /dev/null +++ b/packages/transfer-state/package.json @@ -0,0 +1,51 @@ +{ + "name": "@astro-tools/transfer-state", + "version": "0.0.0", + "description": "A state management integration for having request isolated state transferred from server to client", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/wishrd/astro-tools", + "directory": "packages/transfer-state" + }, + "homepage": "https://astro-tools.pages.dev", + "keywords": [ + "astro", + "withastro", + "transfer", + "state" + ], + "author": { + "name": "wishrd" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "tsup --watch", + "build": "tsup" + }, + "type": "module", + "peerDependencies": { + "astro": "^4.16.17", + "vite": "^5.4.11" + }, + "dependencies": { + "astro-integration-kit": "^0.17.0", + "@alenaksu/json-viewer": "^2.1.2" + }, + "devDependencies": { + "tsup": "^8.3.5", + "vite": "^5.4.11" + } +} diff --git a/packages/transfer-state/src/index.ts b/packages/transfer-state/src/index.ts new file mode 100644 index 0000000..f87e09b --- /dev/null +++ b/packages/transfer-state/src/index.ts @@ -0,0 +1,54 @@ +import { addVirtualImports, createResolver, defineIntegration } from 'astro-integration-kit'; + +import toolbarIcon from './toolbar/icon.ts'; + +const VIRTUAL_MODULE_ID = '@astro-tools:transfer-state'; + +export const transferState = defineIntegration({ + name: '@astro-tools/transfer-state', + setup: ({ name }) => { + const { resolve } = createResolver(import.meta.url); + + return { + hooks: { + 'astro:config:setup': (options) => { + const { addMiddleware, addDevToolbarApp } = options; + + addMiddleware({ + order: 'pre', + entrypoint: resolve('./middleware.js'), + }); + + addVirtualImports(options, { + name, + imports: [ + { + id: VIRTUAL_MODULE_ID, + content: `export * from '${resolve('./state/server.js')}'`, + context: 'server', + }, + { + id: VIRTUAL_MODULE_ID, + content: `export * from '${resolve('./state/client.js')}'`, + context: 'client', + } + ], + }); + + addDevToolbarApp({ + id: name, + name: 'Astro Tools - Transfer State', + icon: toolbarIcon, + entrypoint: resolve('./toolbar/index.js'), + }); + }, + 'astro:config:done': ({ injectTypes }) => { + injectTypes({ + filename: 'types.d.ts', + content: `declare module '@astro-tools:transfer-state' { export const getState: (key: string) => T; export const setState: (key: string, value: T) => void; }`, + }); + } + } + } + }, +}); diff --git a/packages/transfer-state/src/middleware.ts b/packages/transfer-state/src/middleware.ts new file mode 100644 index 0000000..3e9f432 --- /dev/null +++ b/packages/transfer-state/src/middleware.ts @@ -0,0 +1,20 @@ +import { defineMiddleware } from 'astro/middleware'; + +import { withTransferState } from './with-transfer-state.ts'; + +export const onRequest = defineMiddleware(async (_, next) => { + const { response, transferState } = await withTransferState(next); + const contentType = response.headers.get('Content-Type'); + if (!contentType?.includes('text/html')) { + return response; + } + + const content = await response.text(); + const bodyCloseIndex = content.indexOf(''); + + const newContent = content.slice(0, bodyCloseIndex) + + `` + + content.slice(bodyCloseIndex); + + return new Response(newContent, response); +}); diff --git a/packages/transfer-state/src/state/client.ts b/packages/transfer-state/src/state/client.ts new file mode 100644 index 0000000..e02357a --- /dev/null +++ b/packages/transfer-state/src/state/client.ts @@ -0,0 +1,21 @@ +import type { TransferState } from '../transfer-state.ts'; + +let store: TransferState; + +function getStore(): TransferState { + if (!store) { + const state = document.getElementById('astro-tools-transfer-state'); + const stateObject = JSON.parse(state !== null ? (state.textContent || '{}') : '{}'); + store = stateObject; + } + + return store; +} + +export function getState(key: string): T | null { + return getStore()[key] as T | undefined || null; +} + +export function setState(key: string, value: T | null): void { + getStore()[key] = value; +} diff --git a/packages/transfer-state/src/state/server.ts b/packages/transfer-state/src/state/server.ts new file mode 100644 index 0000000..f670041 --- /dev/null +++ b/packages/transfer-state/src/state/server.ts @@ -0,0 +1,19 @@ +import { transferStateStorage } from '../transfer-state-storage.ts'; +import type { TransferState } from '../transfer-state.ts'; + +function getStore(): TransferState { + const store = transferStateStorage.getStore(); + if (!store) { + throw new Error('Server store is not defined!'); + } + + return store; +} + +export function getState(key: string): T | null { + return getStore()[key] as T | undefined || null; +} + +export function setState(key: string, value: T | null): void { + getStore()[key] = value; +} diff --git a/packages/transfer-state/src/toolbar/icon.ts b/packages/transfer-state/src/toolbar/icon.ts new file mode 100644 index 0000000..fa1b464 --- /dev/null +++ b/packages/transfer-state/src/toolbar/icon.ts @@ -0,0 +1 @@ +export default '' diff --git a/packages/transfer-state/src/toolbar/index.ts b/packages/transfer-state/src/toolbar/index.ts new file mode 100644 index 0000000..fce7d71 --- /dev/null +++ b/packages/transfer-state/src/toolbar/index.ts @@ -0,0 +1,56 @@ +import { defineToolbarApp } from 'astro/toolbar'; + +function createHeader(text: string): HTMLElement { + const header = document.createElement('header'); + const title = document.createElement('div'); + const brand = document.createElement('div'); + brand.textContent = 'Astro Tools'; + title.textContent = text; + + header.appendChild(title); + header.appendChild(brand); + + header.style.display = 'flex'; + header.style.flexDirection = 'row'; + header.style.justifyContent = 'space-between'; + header.style.fontWeight = 'bold'; + header.style.marginBlockEnd = '1rem'; + + return header; +} + +export default defineToolbarApp({ + init: async (canvas) => { + const toolbarWindow = document.createElement('astro-dev-toolbar-window'); + toolbarWindow.appendChild(createHeader('Transfer state')); + + const transferState = document.getElementById('astro-tools-transfer-state'); + + if (!transferState || !transferState.textContent) { + toolbarWindow.textContent = 'Transfer state element not found'; + } else if (!transferState.textContent) { + toolbarWindow.textContent = 'Transfer state element is empty'; + } else { + let textContent: string | null = null; + + try { + textContent = JSON.parse(transferState.textContent); + } catch (err) { + console.error(err); + } + + if (!textContent) { + toolbarWindow.textContent = 'Transfer state content is not valid'; + } else { + await import('@alenaksu/json-viewer'); + + const jsonViewer = document.createElement('json-viewer'); + jsonViewer.style.padding = '0 1rem'; + jsonViewer.textContent = JSON.stringify(textContent); + toolbarWindow.appendChild(jsonViewer); + } + } + + canvas.appendChild(toolbarWindow); + }, +}); diff --git a/packages/transfer-state/src/transfer-state-storage.ts b/packages/transfer-state/src/transfer-state-storage.ts new file mode 100644 index 0000000..a00a073 --- /dev/null +++ b/packages/transfer-state/src/transfer-state-storage.ts @@ -0,0 +1,5 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +import type { TransferState } from './transfer-state.ts'; + +export const transferStateStorage = new AsyncLocalStorage(); diff --git a/packages/transfer-state/src/transfer-state.ts b/packages/transfer-state/src/transfer-state.ts new file mode 100644 index 0000000..2d57d92 --- /dev/null +++ b/packages/transfer-state/src/transfer-state.ts @@ -0,0 +1 @@ +export type TransferState = Record; diff --git a/packages/transfer-state/src/with-transfer-state.ts b/packages/transfer-state/src/with-transfer-state.ts new file mode 100644 index 0000000..ee4f486 --- /dev/null +++ b/packages/transfer-state/src/with-transfer-state.ts @@ -0,0 +1,11 @@ +import type { MiddlewareNext } from 'astro'; + +import type { TransferState } from './transfer-state.ts'; +import { transferStateStorage } from './transfer-state-storage.ts'; + +export function withTransferState(callback: MiddlewareNext): Promise<{ response: Response, transferState: TransferState }> { + return new Promise((resolve) => transferStateStorage.run({}, async () => resolve({ + response: await callback(), + transferState: transferStateStorage.getStore() || {}, + }))); +} diff --git a/packages/transfer-state/tsconfig.json b/packages/transfer-state/tsconfig.json new file mode 100644 index 0000000..2c36a00 --- /dev/null +++ b/packages/transfer-state/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strictest", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "jsx": "preserve" + }, + "exclude": ["dist"] +} diff --git a/packages/transfer-state/tsup.config.ts b/packages/transfer-state/tsup.config.ts new file mode 100644 index 0000000..3effc50 --- /dev/null +++ b/packages/transfer-state/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; +import { peerDependencies } from './package.json'; + +export default defineConfig((options) => { + const dev = !!options.watch; + return { + entry: ["src/**/*.ts"], + format: ["esm"], + target: "node18", + bundle: true, + dts: true, + sourcemap: true, + clean: true, + splitting: true, + minify: !dev, + external: [...Object.keys(peerDependencies)], + tsconfig: "tsconfig.json" + }; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15416ac..b63262b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: '@astro-tools/docs-utils': specifier: workspace:* version: link:../../packages/docs-utils + '@astro-tools/transfer-state': + specifier: workspace:* + version: link:../../packages/transfer-state '@astrojs/svelte': specifier: ^6.0.2 version: 6.0.2(astro@4.16.17(rollup@4.28.0)(sass-embedded@1.82.0)(sass@1.82.0)(typescript@5.7.2))(sass-embedded@1.82.0)(sass@1.82.0)(svelte@5.8.0)(typescript@5.7.2) @@ -75,8 +78,30 @@ importers: specifier: ^8.3.5 version: 8.3.5(postcss@8.4.49)(typescript@5.7.2) + packages/transfer-state: + dependencies: + '@alenaksu/json-viewer': + specifier: ^2.1.2 + version: 2.1.2 + astro: + specifier: ^4.16.17 + version: 4.16.17(rollup@4.28.0)(sass-embedded@1.82.0)(sass@1.82.0)(typescript@5.7.2) + astro-integration-kit: + specifier: ^0.17.0 + version: 0.17.0(astro@4.16.17(rollup@4.28.0)(sass-embedded@1.82.0)(sass@1.82.0)(typescript@5.7.2)) + devDependencies: + tsup: + specifier: ^8.3.5 + version: 8.3.5(postcss@8.4.49)(typescript@5.7.2) + vite: + specifier: ^5.4.11 + version: 5.4.11(sass-embedded@1.82.0)(sass@1.82.0) + packages: + '@alenaksu/json-viewer@2.1.2': + resolution: {integrity: sha512-hyR5EUc0GZMTjlSULJZlMzEYB896ICbCf1+5YLQRSAczGRWy42UWac5PcXM4spuyTkrEHHlOZBTVEq34f9+JVQ==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -693,6 +718,12 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lit-labs/ssr-dom-shim@1.2.1': + resolution: {integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==} + + '@lit/reactive-element@2.0.4': + resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1011,6 +1042,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1821,6 +1855,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lit-element@4.1.1: + resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==} + + lit-html@3.2.1: + resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==} + + lit@3.2.1: + resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3039,6 +3082,10 @@ packages: snapshots: + '@alenaksu/json-viewer@2.1.2': + dependencies: + lit: 3.2.1 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -3703,6 +3750,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lit-labs/ssr-dom-shim@1.2.1': {} + + '@lit/reactive-element@2.0.4': + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.1 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.26.0 @@ -4016,6 +4069,8 @@ snapshots: dependencies: '@types/node': 17.0.45 + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -4965,6 +5020,22 @@ snapshots: lines-and-columns@1.2.4: {} + lit-element@4.1.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.1 + '@lit/reactive-element': 2.0.4 + lit-html: 3.2.1 + + lit-html@3.2.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.2.1: + dependencies: + '@lit/reactive-element': 2.0.4 + lit-element: 4.1.1 + lit-html: 3.2.1 + load-tsconfig@0.2.5: {} load-yaml-file@0.2.0: