From 082d94314cb9e29bb90bb6db3273701e9d1546c5 Mon Sep 17 00:00:00 2001 From: danwood <118035379+Floating-Y@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:28:53 +0800 Subject: [PATCH] Defer block source loading until its Code panel opens Signed-off-by: danwood <118035379+Floating-Y@users.noreply.github.com> --- .github/workflows/checks.yml | 2 +- scripts/block-sources.test.mjs | 116 +++++++++++++++++++++++++++++++++ src/App.tsx | 2 +- src/blocks/index.ts | 77 ++++------------------ src/blocks/source.ts | 71 ++++++++++++++++++++ src/pages/blocks.tsx | 83 ++++++++++++++++------- 6 files changed, 261 insertions(+), 90 deletions(-) create mode 100644 scripts/block-sources.test.mjs create mode 100644 src/blocks/source.ts diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 39def38..6f919a4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,7 +26,7 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: node --test scripts/block-metadata.test.mjs + - run: node --test scripts/block-metadata.test.mjs scripts/block-sources.test.mjs # the documentation and the registry are generated: the types of what renders them depend on them - run: npm run docs - name: The registry is as committed diff --git a/scripts/block-sources.test.mjs b/scripts/block-sources.test.mjs new file mode 100644 index 0000000..5ef5225 --- /dev/null +++ b/scripts/block-sources.test.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import test from "node:test" +import vm from "node:vm" +import ts from "typescript" + +// Replace only Vite's glob boundary; run the real registry and source loader without a browser. +function loadModule(path, glob, require = () => { throw new Error("Unexpected module import") }) { + const source = readFileSync(new URL(path, import.meta.url), "utf8") + const { outputText } = ts.transpileModule(source.replaceAll("import.meta.glob", "glob"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + }) + const exports = {} + vm.runInNewContext(outputText, { exports, glob, require }, { filename: path }) + return exports +} + +function sourceModule(fixtures) { + const calls = [] + const sources = Object.fromEntries(Object.entries(fixtures).map(([path, source]) => [path, async () => { + calls.push(path) + return typeof source === "function" ? source() : source + }])) + const module = loadModule("../src/blocks/source.ts", (_patterns, options) => { + assert.equal(options.query, "?raw") + assert.notEqual(options.eager, true) + return sources + }) + return { ...module, calls } +} + +test("initializing the registry and loading a preview never imports raw sources", async () => { + const previewCalls = [] + let sourceImports = 0 + const { blocks } = loadModule("../src/blocks/index.ts", (_patterns, options) => { + assert.notEqual(options?.query, "?raw") + return new Proxy({}, { get: (_target, path) => async () => { + previewCalls.push(path) + return { default: () => null } + } }) + }, (name) => { + if (name === "react") return { lazy: (load) => ({ load }) } + if (name === "./source") { + sourceImports++ + return { loadBlockSource: async (name) => ({ files: [{ path: `${name}.tsx`, source: "code" }] }) } + } + throw new Error(`Unexpected module import: ${name}`) + }) + assert.equal(sourceImports, 0) + assert.deepEqual(previewCalls, []) + const selected = blocks.find((block) => block.name === "sidebar-07") + await selected.component.load() + assert.deepEqual(previewCalls, ["/src/blocks/sidebar-07/page.tsx"]) + assert.equal(sourceImports, 0) + const code = await selected.loadSource() + assert.equal(sourceImports, 1) + assert.equal(code.files[0].path, "sidebar-07.tsx") +}) + +test("Code loads only its block and overview, preserving file order and copied paths", async () => { + const { loadBlockSource, calls } = sourceModule({ + "/src/blocks/sidebar-07/components/z.tsx": 'import A from "@/blocks/sidebar-07/components/a"', + "/src/blocks/sidebar-07/page.tsx": 'export const iframeHeight = "800px"\n\nexport const description = "Preview"\n\nimport Overview from "@/blocks/shared/overview"\nimport Sidebar from "@/blocks/sidebar-07/components/a"\nexport default Sidebar', + "/src/blocks/sidebar-07/components/a.tsx": "export const description = 'Keep component exports'\n", + "/src/blocks/sidebar-07/data.json": '{"rows": []}', + "/src/blocks/shared/overview.tsx": 'export default function Overview() { return "overview" }', + "/src/blocks/login-04/page.tsx": "Must not load", + "/src/blocks/agent-workspace.tsx": "Must not load", + }) + assert.deepEqual(calls, []) + const result = await loadBlockSource("sidebar-07") + assert.deepEqual(Array.from(result.files, (file) => file.path), [ + "page.tsx", "components/a.tsx", "components/z.tsx", "data.json", "shared/overview.tsx", + ]) + assert.equal(result.files[0].source, 'import Overview from "@/shared/overview"\nimport Sidebar from "@/components/a"\nexport default Sidebar') + assert.equal(result.files[1].source, "export const description = 'Keep component exports'\n") + assert.equal(result.files[2].source, 'import A from "@/components/a"') + assert.equal(result.files[3].source, '{"rows": []}') + assert.match(result.note, /empty boxes.*/) + assert.equal(calls.length, 5) + assert.ok(calls.every((path) => path.startsWith("/src/blocks/sidebar-07/") || path === "/src/blocks/shared/overview.tsx")) + assert.strictEqual(await loadBlockSource("sidebar-07"), result) + assert.equal(calls.length, 5) +}) + +test("own blocks retain their install path and successful concurrent loads are reused", async () => { + const original = 'import Panel from "@/components/ui/panel"\nexport default Panel' + const { loadBlockSource, calls } = sourceModule({ + "/src/blocks/agent-workspace.tsx": original, + "/src/blocks/login-04/page.tsx": "Must not load", + }) + const [first, second] = await Promise.all([ + loadBlockSource("agent-workspace", true), loadBlockSource("agent-workspace", true), + ]) + assert.strictEqual(first, second) + assert.deepEqual(Array.from(first.files, (file) => file.path), ["components/agent-workspace.tsx"]) + assert.equal(first.files[0].source, original) + assert.equal(first.note, undefined) + assert.deepEqual(calls, ["/src/blocks/agent-workspace.tsx"]) +}) + +test("failed source loads reject and allow a later retry", async () => { + let attempts = 0 + const { loadBlockSource, calls } = sourceModule({ + "/src/blocks/login-04/page.tsx": () => { + if (++attempts === 1) throw new Error("Network unavailable") + return "export default function Login() {}" + }, + "/src/blocks/sidebar-07/page.tsx": "Must not load", + }) + await assert.rejects(loadBlockSource("login-04"), /Network unavailable/) + const result = await loadBlockSource("login-04") + assert.equal(result.files[0].source, "export default function Login() {}") + assert.equal(result.note, undefined) + assert.deepEqual(calls, ["/src/blocks/login-04/page.tsx", "/src/blocks/login-04/page.tsx"]) +}) diff --git a/src/App.tsx b/src/App.tsx index 3d75d39..80849d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,7 @@ const DocPage = React.lazy(() => import("@/site/docs").then((page) => ({ default const DevExamples = import.meta.env.DEV ? React.lazy(() => import("@/pages/dev-examples").then((page) => ({ default: page.DevExamples }))) : null -// The blocks carry their own source to show it: fetched with the Blocks page, not with every page. +// Preview components and Code sources are fetched separately, when each is opened. const Blocks = React.lazy(() => import("@/pages/blocks").then((page) => ({ default: page.Blocks }))) const BlockView = React.lazy(() => import("@/pages/block-view")) diff --git a/src/blocks/index.ts b/src/blocks/index.ts index f3cdfa9..a34b5ab 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import type { BlockSource } from "./source"; /** * The blocks: whole screens made of the components. A block is shown on the Blocks page in a frame @@ -19,78 +20,26 @@ export type Block = { own?: boolean; /** How tall a window it is shown in. */ height?: number; - /** Said beside the code, where what is shown is not quite what the command installs. */ - note?: string; - files: { path: string; source: string }[]; + loadSource: () => Promise; component: React.LazyExoticComponent; }; -const sources = import.meta.glob( - ["/src/blocks/**/*.tsx", "/src/blocks/**/*.json"], - { query: "?raw", import: "default", eager: true }, -); const pages = import.meta.glob<{ default: React.ComponentType }>([ "/src/blocks/*.tsx", "/src/blocks/*/page.tsx", ]); -/** The files a block is made of, the page first. */ -function filesOf(name: string, own?: boolean) { - if (own) - return [ - { - path: `components/${name}.tsx`, - source: sources[`/src/blocks/${name}.tsx`], - }, - ]; - const prefix = `/src/blocks/${name}/`; - const files = Object.keys(sources) - .filter((path) => path.startsWith(prefix)) - .map((path) => ({ path: path.slice(prefix.length), source: sources[path] })) - .sort( - (a, b) => - Number(b.path === "page.tsx") - Number(a.path === "page.tsx") || - a.path.localeCompare(b.path), - ); - // The page that fills a sidebar block's empty boxes is shared by them, and shown with each. - if (files[0]?.source.includes(OVERVIEW)) - files.push({ - path: "shared/overview.tsx", - source: sources["/src/blocks/shared/overview.tsx"], - }); - // Previews use the site's block folders; copied files live at the paths shown above. - return files.map((file) => ({ - ...file, - // Upstream preview metadata is not a supported Next App Router page export. - source: (file.path === "page.tsx" - ? file.source.replace(/^export const (?:iframeHeight|description) = [^\n]*\n\n?/gm, "") - : file.source) - .replaceAll(`@/blocks/${name}/`, "@/") - .replaceAll("@/blocks/shared/", "@/shared/"), - })); -} - -const OVERVIEW = "@/blocks/shared/overview"; -const FILLED = - "From shadcn/ui the page comes with empty boxes where this one has : a page of Solid Glass's own, shown with the block's files."; - -const block = (entry: Omit): Block => { - const files = filesOf(entry.name, entry.own); - return { - ...entry, - note: files.some((file) => file.path === "shared/overview.tsx") - ? FILLED - : undefined, - files, - component: React.lazy( - pages[ - entry.own - ? `/src/blocks/${entry.name}.tsx` - : `/src/blocks/${entry.name}/page.tsx` - ], - ), - }; -}; +const block = (entry: Omit): Block => ({ + ...entry, + loadSource: () => import("./source").then((module) => module.loadBlockSource(entry.name, entry.own)), + component: React.lazy( + pages[ + entry.own + ? `/src/blocks/${entry.name}.tsx` + : `/src/blocks/${entry.name}/page.tsx` + ], + ), +}); export const blocks: Block[] = [ block({ diff --git a/src/blocks/source.ts b/src/blocks/source.ts new file mode 100644 index 0000000..a5ae5db --- /dev/null +++ b/src/blocks/source.ts @@ -0,0 +1,71 @@ +export type BlockSource = { + files: { path: string; source: string }[]; + /** Said beside the code, where what is shown is not quite what the command installs. */ + note?: string; +}; + +const sources = import.meta.glob( + ["/src/blocks/**/*.tsx", "/src/blocks/**/*.json"], + { query: "?raw", import: "default" }, +); +const cache = new Map>(); +const OVERVIEW = "@/blocks/shared/overview"; +const FILLED = + "From shadcn/ui the page comes with empty boxes where this one has : a page of Solid Glass's own, shown with the block's files."; + +/** The files a block is made of, fetched only when its Code panel opens. */ +async function filesOf(name: string, own?: boolean): Promise { + if (own) + return { + files: [{ + path: `components/${name}.tsx`, + source: await sources[`/src/blocks/${name}.tsx`](), + }], + }; + + const prefix = `/src/blocks/${name}/`; + const paths = Object.keys(sources) + .filter((path) => path.startsWith(prefix)) + .sort( + (a, b) => + Number(b === `${prefix}page.tsx`) - Number(a === `${prefix}page.tsx`) || + a.localeCompare(b), + ); + const files = await Promise.all(paths.map(async (path) => ({ + path: path.slice(prefix.length), + source: await sources[path](), + }))); + // The page that fills a sidebar block's empty boxes is shared by them, and shown with each. + const filled = files[0]?.source.includes(OVERVIEW); + if (filled) + files.push({ + path: "shared/overview.tsx", + source: await sources["/src/blocks/shared/overview.tsx"](), + }); + + return { + note: filled ? FILLED : undefined, + // Previews use the site's block folders; copied files live at the paths shown here. + files: files.map((file) => ({ + ...file, + // Upstream preview metadata is not a supported Next App Router page export. + source: (file.path === "page.tsx" + ? file.source.replace(/^export const (?:iframeHeight|description) = [^\n]*\n\n?/gm, "") + : file.source) + .replaceAll(`@/blocks/${name}/`, "@/") + .replaceAll("@/blocks/shared/", "@/shared/"), + })), + }; +} + +export function loadBlockSource(name: string, own?: boolean): Promise { + const cached = cache.get(name); + if (cached) return cached; + const pending = filesOf(name, own).catch((error: unknown) => { + // A failed request must not poison the next attempt. + cache.delete(name); + throw error; + }); + cache.set(name, pending); + return pending; +} diff --git a/src/pages/blocks.tsx b/src/pages/blocks.tsx index fa2059d..79879ec 100644 --- a/src/pages/blocks.tsx +++ b/src/pages/blocks.tsx @@ -4,6 +4,7 @@ import { Link } from "react-router-dom" import { cn } from "@/lib/utils" import { blocks, type Block } from "@/blocks" +import type { BlockSource } from "@/blocks/source" import { Button, buttonVariants } from "@/components/ui/button" import { StatusGlyph } from "@/components/ui/status-glyph" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" @@ -19,6 +20,63 @@ const WIDTHS = [ { name: "Mobile", icon: SmartphoneIcon, width: "390px" }, ] +/** Mounted only while Code is visible; leaving the panel ignores any pending result. */ +function BlockCode({ block, file, setFile }: { + block: Block + file: number + setFile: (index: number) => void +}) { + const [source, setSource] = React.useState() + const [failed, setFailed] = React.useState(false) + const [attempt, setAttempt] = React.useState(0) + + React.useEffect(() => { + let active = true + setSource(undefined) + setFailed(false) + block.loadSource().then( + (loaded) => { if (active) setSource(loaded) }, + () => { if (active) setFailed(true) }, + ) + return () => { active = false } + }, [block, attempt]) + + if (failed) return ( +
+

Could not load this block's source.

+ +
+ ) + if (!source) return

Loading source…

+ + return ( +
+ {source.note &&

{source.note}

} + {source.files.length > 1 && ( +
+ {source.files.map((entry, index) => ( + + ))} +
+ )} + +
+ ) +} + /** One block: a bar to switch between looking at it and reading it, then the block in a frame of its own. */ function BlockViewer({ block }: { block: Block }) { const { theme } = useTheme() @@ -101,30 +159,7 @@ function BlockViewer({ block }: { block: Block }) { /> ) : ( -
- {block.note &&

{block.note}

} - {block.files.length > 1 && ( -
- {block.files.map((entry, index) => ( - - ))} -
- )} - -
+ )} )