Skip to content
Merged
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
5 changes: 3 additions & 2 deletions web/src/app/community/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ export default async function CommunityLayout({ children }: { children: React.Re
{session ? (
<Link
href="/pattern-lab"
prefetch={false}
className={styles.makeCta}
title="Open Pattern Lab — make a pattern and publish it here"
>
Make<span className={styles.ctaRest}> &amp; publish — Pattern Lab</span> ↗
</Link>
) : (
<Link href="/pattern-lab" title="Open Pattern Lab editor">
<Link href="/pattern-lab" prefetch={false} title="Open Pattern Lab editor">
Pattern Lab ↗
</Link>
)}
Expand All @@ -130,7 +131,7 @@ export default async function CommunityLayout({ children }: { children: React.Re
<div className={styles.pageBody}>{children}</div>

<footer className={styles.pageFooter}>
<Link href="/terms">Terms &amp; Privacy</Link>
<Link href="/terms" prefetch={false}>Terms &amp; Privacy</Link>
<span>·</span>
<a href="mailto:contact@patternflow.work">Report something</a>
{/* The atlas, kept read-only since 26 October 2026. Nothing else
Expand Down
35 changes: 35 additions & 0 deletions web/src/app/community/linkPrefetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { links } from "../../../test/linkTags";

// A <Link> in view prefetches the page it points to, and that page's client
// chunks come with it. On 26 October, at 390px and 1440px, /community
// fetched 20 scripts (392.8 KB gzip), and 9 of them (218.7 KB) arrived after
// the load event. 210.5 KB was Pattern Lab, prefetched by the header's
// "Pattern Lab ↗", and 5.0 KB was /editions, from the firmware notice. With
// prefetch={false} on the links that leave the community, nothing arrived
// after load. A page outside the community loads when its link is followed.

const HREF = /\bhref=(?:"([^"]*)"|\{["`]([^"`]*)["`]\})/;

/** The links whose href is written out and goes somewhere other than
* /community. An href computed from a variable is not read. */
const leaving = links("app/community", "components/community").filter(({ tag }) => {
const m = tag.match(HREF);
const href = m?.[1] ?? m?.[2];
return href !== undefined && href.startsWith("/") && !/^\/community(?:[/?#]|$)/.test(href);
});

describe("links that leave the community", () => {
it("are still found", () => {
const files = leaving.map((l) => l.rel);
expect(files).toContain("app/community/layout.tsx");
expect(files).toContain("components/community/FirmwareNotice.tsx");
});

it("skip the prefetch", () => {
const prefetching = leaving
.filter(({ tag }) => !/\bprefetch=\{false\}/.test(tag))
.map(({ rel, tag }) => `${rel}: ${tag.match(HREF)?.[0]}`);
expect(prefetching).toEqual([]);
});
});
4 changes: 2 additions & 2 deletions web/src/app/community/u/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default async function CommunityUserPage(props: RouteParams) {
</div>
{isSelf && (
<div className={styles.profileSelfActions}>
<Link href="/pattern-lab" className={styles.btnAccent}>
<Link href="/pattern-lab" prefetch={false} className={styles.btnAccent}>
Make &amp; publish ↗
</Link>
{/* Phone only — the header carries it everywhere else. */}
Expand All @@ -104,7 +104,7 @@ export default async function CommunityUserPage(props: RouteParams) {
<span className={styles.emptyBody}>
Open Pattern Lab, make something, publish it here — it takes one button from there.
</span>
<Link href="/pattern-lab" className={styles.emptyCta}>
<Link href="/pattern-lab" prefetch={false} className={styles.emptyCta}>
Make &amp; publish — Pattern Lab ↗
</Link>
</div>
Expand Down
36 changes: 2 additions & 34 deletions web/src/app/homeLinkPrefetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { links } from '../../test/linkTags';

// A <Link href="/"> in view prefetches the home page, and the home page's
// client chunks come with it: three.js (237,537 bytes gzip) and the
Expand All @@ -12,7 +11,6 @@ import { describe, expect, it } from 'vitest';
// breadboard guide, journal, editions and compliance. They are left out
// below until they are done the same way.

const SRC = path.resolve(__dirname, '..');
const LEFT_OUT = [
'components/guide',
'app/build/breadboard',
Expand All @@ -22,37 +20,7 @@ const LEFT_OUT = [
'app/compliance',
];

function sources(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return sources(full);
return entry.name.endsWith('.tsx') && !entry.name.includes('.test.') ? [full] : [];
});
}

/** Each `<Link …>` opening tag, read to the `>` outside any braces. */
function linkTags(source: string): string[] {
const tags: string[] = [];
for (const m of source.matchAll(/<Link\b/g)) {
let depth = 0;
let end = m.index + m[0].length;
for (; end < source.length; end++) {
const c = source[end];
if (c === '{') depth++;
else if (c === '}') depth--;
else if (c === '>' && depth === 0) break;
}
tags.push(source.slice(m.index, end + 1));
}
return tags;
}

const homeLinks = sources(SRC).flatMap((file) => {
const rel = path.relative(SRC, file).split(path.sep).join('/');
return linkTags(fs.readFileSync(file, 'utf8'))
.filter((tag) => /\bhref="\/"/.test(tag))
.map((tag) => ({ rel, tag }));
});
const homeLinks = links('').filter(({ tag }) => /\bhref="\/"/.test(tag));

describe('links to the home page', () => {
it('are still found', () => {
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/community/AddHeaderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ export default function AddHeaderModal({
<p className={styles.formNote}>
The author has no header for this pattern yet — if you ran it on your own board,
finish the job for them. Generate the port with <strong>To hardware</strong> in{" "}
<Link href="/pattern-lab">Pattern Lab</Link>, verify it on hardware, and paste it
<Link href="/pattern-lab" prefetch={false}>
Pattern Lab
</Link>, verify it on hardware, and paste it
here. It goes live immediately, credited to you; the author can pin or replace it,
and the port carries the pattern&apos;s own licence — the work stays theirs.
</p>
Expand All @@ -176,7 +178,9 @@ export default function AddHeaderModal({
<p className={styles.formNote}>
The board runs C++, so a pattern needs a <code>.h</code> port before anyone can
flash it. Generate one with <strong>To hardware</strong> in{" "}
<Link href="/pattern-lab">Pattern Lab</Link>, build it on your own board, and paste
<Link href="/pattern-lab" prefetch={false}>
Pattern Lab
</Link>, build it on your own board, and paste
the working header here. Your pattern then shows as <strong>hardware ready</strong>{" "}
and can be filtered for in the feed.
</p>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/community/FirmwareNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function FirmwareNotice() {
<strong>{LATEST}</strong> is out — the firmware split into editions: the core instrument,
Audio, and Performance. One-click install, switch any time, your settings stay.
</span>
<Link href="/editions" className={styles.fwNoticeLink}>
<Link href="/editions" prefetch={false} className={styles.fwNoticeLink}>
Pick an edition →
</Link>
<button
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/community/NotificationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default function NotificationsList({ items }: { items: NotificationView[]
Alerts arrive when someone comments on, forks, ports, or decks your work. Publish
something and give them a reason.
</span>
<Link href="/pattern-lab" className={styles.emptyCta}>
<Link href="/pattern-lab" prefetch={false} className={styles.emptyCta}>
Make &amp; publish — Pattern Lab ↗
</Link>
</div>
Expand Down
45 changes: 45 additions & 0 deletions web/test/linkTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// What the link prefetch tests read: the `.tsx` files under a folder and the
// `<Link …>` opening tags in each.

import fs from "node:fs";
import path from "node:path";

/** `web/src/`. */
export const src = path.resolve(__dirname, "../src");

/** Every `.tsx` under `dir`, tests left out. */
export function sources(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return sources(full);
return entry.name.endsWith(".tsx") && !entry.name.includes(".test.") ? [full] : [];
});
}

/** Each `<Link …>` opening tag, read to the `>` outside any braces. */
export function linkTags(source: string): string[] {
const tags: string[] = [];
for (const m of source.matchAll(/<Link\b/g)) {
let depth = 0;
let end = m.index + m[0].length;
for (; end < source.length; end++) {
const c = source[end];
if (c === "{") depth++;
else if (c === "}") depth--;
else if (c === ">" && depth === 0) break;
}
tags.push(source.slice(m.index, end + 1));
}
return tags;
}

/** Each `<Link>` in the files under `dirs` (relative to `src/`), with the file
* it is in, relative to `src/` with forward slashes. */
export function links(...dirs: string[]): { rel: string; tag: string }[] {
return dirs.flatMap((dir) =>
sources(path.join(src, dir)).flatMap((file) => {
const rel = path.relative(src, file).split(path.sep).join("/");
return linkTags(fs.readFileSync(file, "utf8")).map((tag) => ({ rel, tag }));
}),
);
}
Loading