Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions scripts/block-sources.test.mjs
Original file line number Diff line number Diff line change
@@ -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.*<Overview \/>/)
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"])
})
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
77 changes: 13 additions & 64 deletions src/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<BlockSource>;
component: React.LazyExoticComponent<React.ComponentType>;
};

const sources = import.meta.glob<string>(
["/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 <Overview />: a page of Solid Glass's own, shown with the block's files.";

const block = (entry: Omit<Block, "files" | "component">): 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, "loadSource" | "component">): 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({
Expand Down
71 changes: 71 additions & 0 deletions src/blocks/source.ts
Original file line number Diff line number Diff line change
@@ -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<string>(
["/src/blocks/**/*.tsx", "/src/blocks/**/*.json"],
{ query: "?raw", import: "default" },
);
const cache = new Map<string, Promise<BlockSource>>();
const OVERVIEW = "@/blocks/shared/overview";
const FILLED =
"From shadcn/ui the page comes with empty boxes where this one has <Overview />: 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<BlockSource> {
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<BlockSource> {
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;
}
Loading
Loading