From 7297ff08b57b86839107028d86997314b5c9a8b1 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 9 Nov 2026 08:53:38 +0100 Subject: [PATCH] feat(docs): share playground links through `@vite-hub/ui/playground` --- docs/app/pages/play/gallery.vue | 2 +- docs/app/types/tools-data.d.ts | 2 +- docs/gallery.ts | 68 +++++++++++-------- docs/pages/mermaid/index.md | 2 +- docs/play-link.ts | 56 ++++++++-------- docs/play-links.ts | 38 +++++------ docs/play.ts | 104 ++++++++++++++++-------------- docs/studio.ts | 9 +-- docs/tools.ts | 23 +++++-- packages/carousel/docs/index.md | 2 +- packages/code/docs/index.md | 2 +- packages/magic-move/docs/index.md | 2 +- packages/tabs/docs/index.md | 2 +- packages/zoom/docs/index.md | 2 +- pnpm-lock.yaml | 24 +++---- pnpm-workspace.yaml | 5 +- test/links.test.ts | 8 ++- 17 files changed, 187 insertions(+), 164 deletions(-) diff --git a/docs/app/pages/play/gallery.vue b/docs/app/pages/play/gallery.vue index fedc74ef9..6371136f0 100644 --- a/docs/app/pages/play/gallery.vue +++ b/docs/app/pages/play/gallery.vue @@ -191,7 +191,7 @@ onBeforeUnmount(() => { void { let saving: ReturnType | undefined; let saveVersion = 0; - function savedState() { - const edits = Object.fromEntries( - [...sources].filter(([id, source]) => data.cells[id] && data.cells[id].source !== source), - ); - return Object.keys(edits).length || added.length - ? { - edits, - added: added.map((cell) => ({ ...cell, source: sources.get(cell.id) ?? cell.source })), - } - : undefined; - } + // As files, each cell's Markdown is `.md`, and `added.json` places the copies. The examples' own Markdown is the + // base, so a link holds only the cells that changed. + const fileOf = (id: string) => `${id}.md`; + const examplesFiles: PlaygroundFiles = Object.fromEntries( + Object.entries(data.cells).map(([id, cell]) => [fileOf(id), cell.source]), + ); + const edited = () => + added.length > 0 || [...sources].some(([id, source]) => data.cells[id]?.source !== source); async function saveNow(): Promise { clearTimeout(saving); const version = saveVersion; - const state = savedState(); - const code = state ? await pack(JSON.stringify(state)) : undefined; + const files: PlaygroundFiles = Object.fromEntries( + [...sources].map(([id, source]) => [fileOf(id), source]), + ); + if (added.length) + files["added.json"] = JSON.stringify( + added.map(({ id, after, label }) => ({ id, after, label })), + ); + const code = edited() ? await encodePlaygroundLink({ files }, examplesFiles) : undefined; // Compression can finish after another edit or reset. Only the current snapshot may reach the URL. if (version !== saveVersion) return saveNow(); const url = new URL(location.href); - if (code) url.searchParams.set("edit", code); - else url.searchParams.delete("edit"); + if (code) url.searchParams.set(playgroundHashKey, code); + else url.searchParams.delete(playgroundHashKey); if (!signal.aborted) history.replaceState(history.state, "", url); return url.href; } function save() { saveVersion++; - clear.hidden = share.hidden = !savedState(); + clear.hidden = share.hidden = !edited(); clearTimeout(saving); saving = setTimeout(() => void saveNow().catch((error) => reportSharing(error)), 250); } @@ -472,7 +481,7 @@ export function startGallery(root: HTMLElement, data: GalleryData): () => void { // A cell's Playground link, and the inspector's while it's open, take the cell's Markdown as it is now. async function link(id: string): Promise { const source = sources.get(id) ?? ""; - const href = `/play#md=${await pack(source)}`; + const href = await playLink(source); if (source !== (sources.get(id) ?? "")) return link(id); const own = article(id)?.querySelector('[data-action="play"]'); if (own) own.href = href; @@ -1092,19 +1101,22 @@ export function startGallery(root: HTMLElement, data: GalleryData): () => void { }); // A link's changes render as the page opens. - const edit = new URLSearchParams(location.search).get("edit"); - if (edit) { + const code = new URLSearchParams(location.search).get(playgroundHashKey); + if (code) { pending.add(""); gallery.dataset.busy = ""; - void unpack(edit) - .then((text) => { - const saved: { edits?: Record; added?: Added[] } = JSON.parse(text); - for (const [id, source] of Object.entries(saved.edits ?? {})) - if (data.cells[id]) change(id, source); - for (const cell of saved.added ?? []) { + void decodePlaygroundLink(code) + .then((state) => { + if (!state) throw new Error("The link is cut off or changed."); + const copiesOf: Omit[] = JSON.parse(state.files["added.json"] ?? "[]"); + for (const id of Object.keys(data.cells)) { + const source = state.files[fileOf(id)]; + if (source !== undefined) change(id, source); + } + for (const cell of copiesOf) { const number = Number(cell.id.split("--")[1]); if (Number.isFinite(number)) copies = Math.max(copies, number); - addCell(cell.after, cell.label, cell.source, cell.id); + addCell(cell.after, cell.label, state.files[fileOf(cell.id)] ?? "", cell.id); } hold(); keep(); diff --git a/docs/pages/mermaid/index.md b/docs/pages/mermaid/index.md index 30efd8d64..4ef8826ea 100644 --- a/docs/pages/mermaid/index.md +++ b/docs/pages/mermaid/index.md @@ -124,7 +124,7 @@ pele({ languages: ["mermaid"] }); // only `mermaid` fences, not `pele` ones ### Check a diagram before you publish -A diagram Pele can't draw shows its source as code, with the error in the wrapper's `data-error`. Nothing goes in `meta.issues`, so try it in the [playground](/play/#preset=mermaid-flowchart) first. +A diagram Pele can't draw shows its source as code, with the error in the wrapper's `data-error`. Nothing goes in `meta.issues`, so try it in the [playground](/play#code=q1YqUbJSyk0tyk3MTNFNy8kvT85ILCpRqgUA) first. ### Comark's own mermaid plugin diff --git a/docs/play-link.ts b/docs/play-link.ts index bbf3102a6..a6aa46dc2 100644 --- a/docs/play-link.ts +++ b/docs/play-link.ts @@ -1,35 +1,31 @@ -// A playground link carries its Markdown in the hash, deflated, so nothing is stored anywhere. The pages' examples, -// the playground, and the gallery all write it, in the browser or on the server, so it uses the web's streams. -export const sharingUnsupported = - "Sharing is unavailable in this browser (raw deflate is not supported)."; +// The playground's state is files: the Markdown in `content.md`, and `config.json` and `theme.json` when they aren't +// the defaults. A link carries them in the hash through @vite-hub/ui/playground, so nothing is stored anywhere. A +// preset is a template, so a link to it holds only its id and what changed. The pages' examples, the playground, and +// the gallery all write links, in the browser or on the server. +import { + encodePlaygroundLink, + playgroundUrl, + type PlaygroundTemplate, +} from "@vite-hub/ui/playground"; -function compressionStream(kind: "compress" | "decompress") { - const Stream = - kind === "compress" ? globalThis.CompressionStream : globalThis.DecompressionStream; - if (typeof Stream !== "function") throw new Error(sharingUnsupported); - try { - return new Stream("deflate-raw"); - } catch { - throw new Error(sharingUnsupported); - } -} - -export async function pack(text: string) { - const stream = new Blob([text]).stream().pipeThrough(compressionStream("compress")); - const bytes = new Uint8Array(await new Response(stream).arrayBuffer()); - let binary = ""; - for (const byte of bytes) binary += String.fromCharCode(byte); - return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""); -} - -export async function unpack(code: string) { - const binary = atob(code.replaceAll("-", "+").replaceAll("_", "/")); - const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); - const stream = new Blob([bytes]).stream().pipeThrough(compressionStream("decompress")); - return new Response(stream).text(); -} +export const contentFile = "content.md"; +export const configFile = "config.json"; +export const themeFile = "theme.json"; /** An include reads files, so the playground can't run it. */ export const playable = (source: string) => !/^:+include\b/m.test(source); -export const playLink = async (source: string) => `/play#md=${await pack(source)}`; +export const templateOf = (preset: { id: string; source: string }): PlaygroundTemplate => ({ + id: preset.id, + files: { [contentFile]: preset.source }, +}); + +/** The code of a link that opens the preset as it is. */ +export const presetCode = (preset: { id: string; source: string }) => { + const { id, files } = templateOf(preset); + return encodePlaygroundLink({ template: id, files }, files); +}; + +/** A link that opens `source` in the playground, with the default config and theme. */ +export const playLink = async (source: string) => + playgroundUrl("/play", await encodePlaygroundLink({ files: { [contentFile]: source } })); diff --git a/docs/play-links.ts b/docs/play-links.ts index e5b8e4799..d62d80b06 100644 --- a/docs/play-links.ts +++ b/docs/play-links.ts @@ -1,11 +1,12 @@ -// Opens every playground link in the built docs the way the playground does. A link carries its state in the fragment: -// a preset by id, or Markdown packed by play-link.ts. The preset must exist, or the Markdown must unpack and parse with -// the playground's plugins and default config without issues. docs/links.ts checks the rest of each link. +// Opens every playground link in the built docs the way the playground does. A link carries its files in the fragment, +// `#code=…`, with a preset as its template. The code must decode against the presets, and the Markdown must parse with +// the playground's plugins and the link's config without issues. docs/links.ts checks the rest of each link. import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, relative } from "node:path"; import { parseMarkdown } from "comark"; import { defaultConfig, readConfig } from "./config.ts"; -import { playable, unpack } from "./play-link.ts"; +import { decodePlaygroundLink, readPlaygroundHash } from "@vite-hub/ui/playground"; +import { configFile, contentFile, playable, templateOf } from "./play-link.ts"; import { browserPlugins } from "./plugins.ts"; const root = new URL("./dist/public/", import.meta.url).pathname; @@ -20,26 +21,27 @@ const pages = (dir: string): string[] => : [], ); -const presets = new Set( - (JSON.parse(readFileSync(join(root, "play/presets.json"), "utf8")) as { id: string }[]).map( - (preset) => preset.id, - ), -); -const { config } = readConfig(defaultConfig); +const templates = ( + JSON.parse(readFileSync(join(root, "play/presets.json"), "utf8")) as { + id: string; + source: string; + }[] +).map(templateOf); async function problemOf(hash: string) { - const state = new URLSearchParams(hash.slice(1)); - const preset = state.get("preset"); - if (preset) return presets.has(preset) ? undefined : `the preset "${preset}" doesn't exist`; - const packed = state.get("md"); - if (!packed) return; - const source = await unpack(packed).catch(() => undefined); - if (!source) return "its Markdown doesn't unpack"; + const code = readPlaygroundHash(hash); + if (!code) return "it has no `#code=`"; + const state = await decodePlaygroundLink(code, templates); + if (!state) return "its code doesn't decode, or names a preset that doesn't exist"; + const source = state.files[contentFile]; + if (source === undefined) return `it has no ${contentFile}`; if (!playable(source)) return "its Markdown includes files, which the playground can't read"; + const { config, issues } = readConfig(state.files[configFile] ?? defaultConfig); const { meta } = await parseMarkdown(source, { plugins: browserPlugins(config, { download: false }), }); - return meta.issues?.length ? `the playground reports: ${meta.issues.join(" ")}` : undefined; + const all = [...issues, ...(meta.issues ?? [])]; + return all.length ? `the playground reports: ${all.join(" ")}` : undefined; } const checked = new Map>(); diff --git a/docs/play.ts b/docs/play.ts index e8c140d7a..7facddbaa 100644 --- a/docs/play.ts +++ b/docs/play.ts @@ -7,7 +7,14 @@ import { defaultConfig, readConfig } from "./config.ts"; import { defaultTheme, readTheme, themeCss, themePresets, type Theme } from "./play-theme.ts"; import { browserPlugins, components } from "./plugins.ts"; import { previewDocument } from "./preview.ts"; -import { pack, sharingUnsupported, unpack } from "./play-link.ts"; +import { + decodePlaygroundLink, + encodePlaygroundLink, + playgroundUrl, + readPlaygroundHash, + type PlaygroundFiles, +} from "@vite-hub/ui/playground"; +import { configFile, contentFile, presetCode, templateOf, themeFile } from "./play-link.ts"; import { errorText } from "../shared/text.ts"; export interface Preset { @@ -423,14 +430,15 @@ export async function startPlayground( showIssues(problems); } + const templates = presets.map(templateOf); async function link() { - const config = - configEditor.value === defaultConfig ? "" : `&config=${await pack(configEditor.value)}`; - const themed = - themeCss(theme) || theme.preset !== "shadcn" || theme.scheme - ? `&theme=${await pack(JSON.stringify(theme))}` - : ""; - return `#md=${await pack(editor.value)}${config}${themed}`; + const files: PlaygroundFiles = { [contentFile]: editor.value }; + if (configEditor.value !== defaultConfig) files[configFile] = configEditor.value; + if (themeCss(theme) || theme.preset !== "shadcn" || theme.scheme) + files[themeFile] = JSON.stringify(theme); + const template = base && templateOf(base); + const code = await encodePlaygroundLink({ template: template?.id, files }, template?.files); + return playgroundUrl("", code); } let loadRun = 0; @@ -438,49 +446,44 @@ export async function startPlayground( if (signal.aborted) return; const run = ++loadRun; const navigationIssues: string[] = []; - const hash = new URLSearchParams(location.hash.slice(1)); - const code = hash.get("md"); - const config = hash.get("config"); - const sharedTheme = hash.get("theme"); - // A preset keeps whatever config is in the tab, so one config can be tried across them. - if (config !== null) { - const decodedConfig = await unpack(config).catch(() => { - navigationIssues.push("The link's config is cut off or changed; using the default config."); - return defaultConfig; - }); - if (run !== loadRun) return; - configEditor.value = decodedConfig; - repaint(configEditor); - } else if (code !== null) configEditor.value = defaultConfig; - // The theme rides along like the config. - let nextTheme: Theme | undefined; - if (sharedTheme !== null) - nextTheme = await unpack(sharedTheme) - .then((text) => readTheme(JSON.parse(text))) - .catch(() => { - navigationIssues.push("The link's theme is cut off or changed; using the default theme."); - return defaultTheme(); - }); - else if (code !== null) nextTheme = defaultTheme(); - if (run !== loadRun) return; - if (nextTheme) setTheme(nextTheme); - const preset = - presets.find((preset) => preset.id === hash.get("preset")) ?? - (code === null ? presets[0] : undefined); - let markdown = preset?.source; - if (!preset && code !== null) - markdown = await unpack(code).catch((error) => - error instanceof Error && error.message === sharingUnsupported - ? sharingUnsupported - : "The link's Markdown is cut off or changed.", - ); + const code = readPlaygroundHash(location.hash); + const state = code === undefined ? undefined : await decodePlaygroundLink(code, templates); if (run !== loadRun) return; + const preset = state + ? presets.find((preset) => preset.id === state.template) + : code === undefined + ? presets[0] + : undefined; + const files = state?.files; + // A preset as it is keeps whatever config and theme are in the tab, so one config can be tried across them. + const asIs = + !!preset && + !!files && + Object.keys(files).length === 1 && + files[contentFile] === preset.source; + if (files && !asIs) { + configEditor.value = files[configFile] ?? defaultConfig; + const sharedTheme = files[themeFile]; + let nextTheme = defaultTheme(); + if (sharedTheme !== undefined) + try { + nextTheme = readTheme(JSON.parse(sharedTheme)); + } catch { + navigationIssues.push("The link's theme is not valid; using the default theme."); + } + setTheme(nextTheme); + } + const markdown = files + ? (files[contentFile] ?? "") + : code === undefined + ? preset?.source + : "The link's Markdown is cut off or changed."; loadIssues = navigationIssues; if (markdown !== undefined) editor.value = markdown; base = preset; - markPreset(preset?.id); + reset.hidden = !preset || editor.value === preset.source; + markPreset(reset.hidden ? preset?.id : undefined); revealPreset(preset?.id); - reset.hidden = true; pointNpmx(preset?.npm); repaint(editor); repaint(configEditor); @@ -521,8 +524,13 @@ export async function startPlayground( confirmReset.returnValue = ""; confirmReset.showModal(); }); - confirmReset.addEventListener("close", () => { - if (confirmReset.returnValue === "reset" && base) location.hash = `preset=${base.id}`; + confirmReset.addEventListener("close", async () => { + if (confirmReset.returnValue !== "reset" || !base) return; + try { + location.hash = playgroundUrl("", await presetCode(base)); + } catch (error) { + showIssues([errorText(error)]); + } }); copy.addEventListener("click", async () => { const shown = configEditor.closest(".play-code")?.hidden ? editor : configEditor; diff --git a/docs/studio.ts b/docs/studio.ts index cf9607298..12d7a0cfd 100644 --- a/docs/studio.ts +++ b/docs/studio.ts @@ -12,7 +12,7 @@ import { type Node, } from "comark"; import { textContent } from "comark/utils"; -import { site } from "./paths.ts"; +import { playLink } from "./play-link.ts"; const isElement = (node: Node): node is ElementNode => Array.isArray(node) && typeof node[0] === "string"; @@ -20,13 +20,6 @@ const isElement = (node: Node): node is ElementNode => const childrenOf = (node: ElementNode) => node.slice(2) as Node[]; const isPre = (node: Node): node is ElementNode => isElement(node) && node[0] === "pre"; -async function playLink(source: string) { - const stream = new Blob([source]).stream().pipeThrough(new CompressionStream("deflate-raw")); - const bytes = new Uint8Array(await new Response(stream).arrayBuffer()); - const base64 = btoa(String.fromCharCode(...bytes)); - return `${site("/play/")}#md=${base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "")}`; -} - async function studio(node: ElementNode, plugins: () => ComarkPlugin[]) { const children = childrenOf(node); const [draftPre, pre] = children.filter(isPre); diff --git a/docs/tools.ts b/docs/tools.ts index 3612bd942..5321014ce 100644 --- a/docs/tools.ts +++ b/docs/tools.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs"; import { renderHtmlFromDocument } from "@comark/html"; import { icons as lucide } from "@iconify-json/lucide"; +import { playgroundUrl } from "@vite-hub/ui/playground"; import { parseMarkdown } from "comark"; import { escapeHtml } from "comark/utils"; import { parse } from "yaml"; @@ -17,6 +18,7 @@ import { loadExamples, thirdParty } from "./examples.ts"; import { highlight } from "./highlight.ts"; import { components } from "./plugins.ts"; import { propsOf } from "./props.ts"; +import { presetCode } from "./play-link.ts"; import { cellDocument } from "./preview.ts"; import { packageOrder } from "./sections.ts"; @@ -58,12 +60,15 @@ const packages = packageOrder.flatMap((slug) => { const packageOf = (group: string) => packages.find((pkg) => pkg.slug === group); const examples = await loadExamples(packageOrder); -const presets = examples - .filter((example) => example.playable) - .map(({ id, label, group, source }) => { - const pkg = packageOf(group); - return { id, label, group: pkg?.label ?? "Recipes", npm: pkg?.npm ?? "", source }; - }); +const presets = await Promise.all( + examples + .filter((example) => example.playable) + .map(async ({ id, label, group, source }) => { + const pkg = packageOf(group); + const code = await presetCode({ id, source }); + return { id, label, group: pkg?.label ?? "Recipes", npm: pkg?.npm ?? "", source, code }; + }), +); // A still of a rendered document: no scripts, its plugins' styles collected into `styles` to go in the page once, // no ids, since a page holds many, and so no link to one, and a chart without its data table, so the still is all @@ -101,7 +106,7 @@ const column = [...new Set(presets.map((preset) => preset.group))] .filter((preset) => preset.group === group) .map( (preset) => - `
${escapeHtml(preset.label)}
`, + `
${escapeHtml(preset.label)}
`, ) .join(""); return `
${cards}
`; @@ -143,6 +148,9 @@ await Promise.all( }), ); const docsPath = (page: string) => `/${page}`; +const playLinks = new Map( + presets.map((preset) => [preset.id, playgroundUrl("/play", preset.code)]), +); const galleryGroups = [...new Set(examples.map((example) => example.group))].map((group) => { const pkg = packageOf(group); return { @@ -157,6 +165,7 @@ const galleryGroups = [...new Set(examples.map((example) => example.group))].map label: example.label || pkg?.label || example.page, page: docsPath(example.page), playable: example.playable, + play: playLinks.get(example.id), })), }; }); diff --git a/packages/carousel/docs/index.md b/packages/carousel/docs/index.md index 426cd68d3..56e4d7ad8 100644 --- a/packages/carousel/docs/index.md +++ b/packages/carousel/docs/index.md @@ -306,4 +306,4 @@ Wider gaps, no panels, and bigger arrows: } ``` -To try it, paste it into Extra CSS in the [playground's Theme pane](/play/#preset=carousel-images). +To try it, paste it into Extra CSS in the [playground's Theme pane](/play#code=q1YqUbJSSk4syi8tTs3RzcxNTE8tVqoFAA). diff --git a/packages/code/docs/index.md b/packages/code/docs/index.md index ed74a223a..db0b2dc59 100644 --- a/packages/code/docs/index.md +++ b/packages/code/docs/index.md @@ -460,4 +460,4 @@ Bigger buttons further from the corner, and a title in the code's font, lined up } ``` -To try it, paste it into Extra CSS in the [playground's Theme pane](/play/#preset=code-copy-and-wrap). +To try it, paste it into Extra CSS in the [playground's Theme pane](/play#code=q1YqUbJSSs5PSdVNzi-o1E3MS9EtL0osUKoFAA). diff --git a/packages/magic-move/docs/index.md b/packages/magic-move/docs/index.md index 0a3cd4562..e5b7d0991 100644 --- a/packages/magic-move/docs/index.md +++ b/packages/magic-move/docs/index.md @@ -258,4 +258,4 @@ A header that matches the code block: } ``` -To try it, paste it into Extra CSS in the [playground's Theme pane](/play/#preset=magic-move-refactor). +To try it, paste it into Extra CSS in the [playground's Theme pane](/play#code=q1YqUbJSyk1Mz0zWzc0vS9UtSk1LTC7JL1KqBQA). diff --git a/packages/tabs/docs/index.md b/packages/tabs/docs/index.md index 49124da7c..154b2de8b 100644 --- a/packages/tabs/docs/index.md +++ b/packages/tabs/docs/index.md @@ -418,4 +418,4 @@ An open tab in the page's brand color: } ``` -To try it, paste it into Extra CSS in the [playground's Theme pane](/play/#preset=tabs-tabs). +To try it, paste it into Extra CSS in the [playground's Theme pane](/play#code=q1YqUbJSKklMKtYFEUq1AA). diff --git a/packages/zoom/docs/index.md b/packages/zoom/docs/index.md index f6539001b..e608cc068 100644 --- a/packages/zoom/docs/index.md +++ b/packages/zoom/docs/index.md @@ -318,4 +318,4 @@ A dark backdrop that lets the page show through: } ``` -To try it, paste it into Extra CSS in the [playground's Theme pane](/play/#preset=zoom-image). +To try it, paste it into Extra CSS in the [playground's Theme pane](/play#code=q1YqUbJSqsrPz9XNzE1MT1WqBQA). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c68b77d3..885b67096 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,7 +208,7 @@ overrides: '@vite-hub/schedule': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/schedule@2707919d355e02959955d9607436b9e26bc09f3e '@vite-hub/shell': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e '@vite-hub/source': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/source@2707919d355e02959955d9607436b9e26bc09f3e - '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e + '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9 '@vite-hub/web-container': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/web-container@2707919d355e02959955d9607436b9e26bc09f3e '@vite-hub/workflow': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workflow@2707919d355e02959955d9607436b9e26bc09f3e '@vite-hub/workspace': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workspace@2707919d355e02959955d9607436b9e26bc09f3e @@ -367,8 +367,8 @@ importers: specifier: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e version: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(supports-color@10.2.2) '@vite-hub/ui': - specifier: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e - version: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e(92f6ef7a2d1832b8ad2fc810fe8768b8) + specifier: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9 + version: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9(92f6ef7a2d1832b8ad2fc810fe8768b8) '@vite-hub/workflow': specifier: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workflow@2707919d355e02959955d9607436b9e26bc09f3e version: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workflow@2707919d355e02959955d9607436b9e26bc09f3e(@aws-sdk/credential-provider-web-identity@3.972.49)(@vite-hub/database@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/database@2707919d355e02959955d9607436b9e26bc09f3e(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1))(ocache@0.3.1))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(typescript@6.0.3)(ws@8.22.0) @@ -422,7 +422,7 @@ importers: version: 1.5.0(typescript@6.0.3) vite-hub: specifier: https://pkg.pr.new/vite-hub/vitehub/vite-hub@2707919d355e02959955d9607436b9e26bc09f3e - version: https://pkg.pr.new/vite-hub/vitehub/vite-hub@2707919d355e02959955d9607436b9e26bc09f3e(04ab9dec9cacd6cddd0f214d2f11b97e) + version: https://pkg.pr.new/vite-hub/vitehub/vite-hub@2707919d355e02959955d9607436b9e26bc09f3e(4b36e15916eff5af251150df68a1e8b7) vue: specifier: 'catalog:' version: 3.5.44(typescript@6.0.3) @@ -6974,9 +6974,9 @@ packages: vue: optional: true - '@vite-hub/ui@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e': - resolution: {integrity: sha512-HSQ42E3PThvBz6CPf4QiiOFza32n96VOmvr5yZY1C6NJnGArxbFnV8NHD77/kuGdxVrOeFwWGhQBZLUvZg34Yw==, tarball: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e} - version: 0.0.0-preview-2707919 + '@vite-hub/ui@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9': + resolution: {integrity: sha512-c7LIY40jSRm3/uZysWf43MnzcjFbVUlLrUcTTshOk+PYY5U0Hm+3MixTdkbZyYWvC45BE4CocVQynZNWwH8K6A==, tarball: https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9} + version: 0.0.0-preview-0fe50ee engines: {node: '>=22.22.0'} peerDependencies: '@iconify-json/ph': ^1.2.2 @@ -6990,7 +6990,7 @@ packages: '@tiptap/vue-3': ^3.31.4 ai: ^6.0.0 || ^7.0.0-0 vite: '>=8.0.0' - vue: 3.5.44 + vue: ^3.5.0 peerDependenciesMeta: '@iconify-json/ph': optional: true @@ -18197,7 +18197,7 @@ snapshots: '@vite-hub/content': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/content@2707919d355e02959955d9607436b9e26bc09f3e(27c8ae7a0437292070e4bc3c3b3536fb) '@vite-hub/rate-limit': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/rate-limit@2707919d355e02959955d9607436b9e26bc09f3e(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(typescript@6.0.3) '@vite-hub/source': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/source@2707919d355e02959955d9607436b9e26bc09f3e(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1))(nitro@3.0.261102-rc)(ocache@0.3.1)(vue@3.5.44(typescript@6.0.3)) - '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e(92f6ef7a2d1832b8ad2fc810fe8768b8) + '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9(92f6ef7a2d1832b8ad2fc810fe8768b8) '@vite-hub/workspace': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workspace@2707919d355e02959955d9607436b9e26bc09f3e(@vite-hub/shell@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(supports-color@10.2.2))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(ai@7.0.153(zod@4.6.7))(crossws@0.4.17(srvx@1.0.8))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1))(nitro@3.0.261102-rc)(ocache@0.3.1)(oxc-parser@0.153.0)(rolldown@1.2.16)(rollup@4.64.12)(sharp@0.35.5(@types/node@24.19.2))(shiki@4.5.0)(ssh2@1.17.0)(supports-color@10.2.2)(typescript@6.0.3)(unstorage@2.0.0-beta.0)(vue@3.5.44(typescript@6.0.3)) '@vueuse/core': 14.4.0(vue@3.5.44(typescript@6.0.3)) comark: 0.9.0(katex@0.19.1)(shiki@4.5.0) @@ -18748,7 +18748,7 @@ snapshots: - crossws - ocache - '@vite-hub/ui@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e(92f6ef7a2d1832b8ad2fc810fe8768b8)': + '@vite-hub/ui@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9(92f6ef7a2d1832b8ad2fc810fe8768b8)': dependencies: '@codemirror/autocomplete': 6.20.4 '@codemirror/commands': 6.11.1 @@ -26173,7 +26173,7 @@ snapshots: vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1)' optional: true - vite-hub@https://pkg.pr.new/vite-hub/vitehub/vite-hub@2707919d355e02959955d9607436b9e26bc09f3e(04ab9dec9cacd6cddd0f214d2f11b97e): + vite-hub@https://pkg.pr.new/vite-hub/vitehub/vite-hub@2707919d355e02959955d9607436b9e26bc09f3e(4b36e15916eff5af251150df68a1e8b7): dependencies: '@cfworker/json-schema': 4.1.1 '@cloudflare/workers-types': 5.20261025.1 @@ -26233,7 +26233,7 @@ snapshots: '@vite-hub/realtime': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/realtime@2707919d355e02959955d9607436b9e26bc09f3e(45e28081b19a964ffa3cb0dc8a16e54a) '@vite-hub/schedule': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/schedule@2707919d355e02959955d9607436b9e26bc09f3e(@vite-hub/kv@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/kv@2707919d355e02959955d9607436b9e26bc09f3e(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(typescript@6.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(typescript@6.0.3) '@vite-hub/shell': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(supports-color@10.2.2) - '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e(92f6ef7a2d1832b8ad2fc810fe8768b8) + '@vite-hub/ui': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9(92f6ef7a2d1832b8ad2fc810fe8768b8) '@vite-hub/workspace': https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workspace@2707919d355e02959955d9607436b9e26bc09f3e(@vite-hub/shell@https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1)(supports-color@10.2.2))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@24.19.2)(@volar/typescript@2.4.28)(@vue/language-core@3.3.13)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.25)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vue-tsc@3.3.13(typescript@6.0.3))(yaml@2.9.1))(ai@7.0.153(zod@4.6.7))(crossws@0.4.17(srvx@1.0.8))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1))(nitro@3.0.261102-rc)(ocache@0.3.1)(oxc-parser@0.153.0)(rolldown@1.2.16)(rollup@4.64.12)(sharp@0.35.5(@types/node@24.19.2))(shiki@4.5.0)(ssh2@1.17.0)(supports-color@10.2.2)(typescript@6.0.3)(unstorage@2.0.0-beta.0)(vue@3.5.44(typescript@6.0.3)) better-auth: 1.7.15(@opentelemetry/api@1.10.0)(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1))(next@16.4.0(@babel/core@8.1.0)(@opentelemetry/api@1.10.0)(@playwright/test@1.64.1)(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(svelte@5.57.2(@typescript-eslint/types@8.71.1))(vitest@5.0.3)(vue@3.5.44(typescript@6.0.3)) drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261025.1)(@libsql/client@0.18.0)(@opentelemetry/api@1.10.0)(bun-types@1.4.4)(kysely@0.29.7)(sql.js@1.15.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index df5c2b465..7531a717e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -91,7 +91,8 @@ catalog: "@vite-hub/docs": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/docs@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/realtime": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/realtime@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/source": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/source@2707919d355e02959955d9607436b9e26bc09f3e - "@vite-hub/ui": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e + # @vite-hub/ui/playground (vite-hub/vitehub#3744) is not on the hosted Console stack yet. Move to it once #3744 merges. + "@vite-hub/ui": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9 "@vite-hub/workspace": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workspace@2707919d355e02959955d9607436b9e26bc09f3e drizzle-kit: ^0.31.10 drizzle-orm: 0.45.4 @@ -152,7 +153,7 @@ overrides: "@vite-hub/schedule": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/schedule@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/shell": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/shell@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/source": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/source@2707919d355e02959955d9607436b9e26bc09f3e - "@vite-hub/ui": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@2707919d355e02959955d9607436b9e26bc09f3e + "@vite-hub/ui": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/ui@0fe50ee248888266a9b59bfd8949fc3fdae329e9 "@vite-hub/web-container": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/web-container@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/workflow": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workflow@2707919d355e02959955d9607436b9e26bc09f3e "@vite-hub/workspace": https://pkg.pr.new/vite-hub/vitehub/@vite-hub/workspace@2707919d355e02959955d9607436b9e26bc09f3e diff --git a/test/links.test.ts b/test/links.test.ts index ed20bb0d8..6649a7e27 100644 --- a/test/links.test.ts +++ b/test/links.test.ts @@ -171,7 +171,7 @@ describe("built docs links", () => { it("reads the playground's fragment as its state, and checks the gallery's as ids", () => { const result = check( - `PlaygroundPreset + `PlaygroundPreset GalleryMissing`, { "play/index.html": "", "play/gallery/index.html": '
' }, ); @@ -181,8 +181,10 @@ describe("built docs links", () => { }); it("keeps playground links on the site and drops the old trailing slash elsewhere", () => { - expect(site("/play/#preset=code-copy-and-wrap")).toBe("/play#preset=code-copy-and-wrap"); - expect(site("/play#md=eJx-test_123")).toBe("/play#md=eJx-test_123"); + expect(site("/play/#code=q1YqUbJSSs5PSdVNzi-o1E3MS9EtL0osUKoFAA")).toBe( + "/play#code=q1YqUbJSSs5PSdVNzi-o1E3MS9EtL0osUKoFAA", + ); + expect(site("/play#code=eJx-test_123")).toBe("/play#code=eJx-test_123"); expect(site("/zoom/#theme")).toBe("/zoom#theme"); expect(site("/playwright/")).toBe("/playwright"); });