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
7 changes: 7 additions & 0 deletions .changeset/small-icons-load.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@emdash-cms/admin": patch
"@emdash-cms/blocks": patch
"emdash": patch
---

Reduces the admin's initial download by deferring uncommon plugin navigation icons and Block Kit chart code until they are displayed. Preserves Phosphor's exported icon aliases and keeps admin routes mounted if an icon or chart chunk fails to load.
13 changes: 9 additions & 4 deletions .github/workflows/query-counts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm build

- name: Regenerate snapshots
run: |
node scripts/query-counts.mjs --target sqlite --update
node scripts/query-counts.mjs --target d1 --update
- name: Regenerate SQLite snapshots
run: node scripts/query-counts.mjs --target sqlite --update

- name: Check admin client bundle
# Reuse the SQLite production build before the D1 run replaces dist/.
run: pnpm bundle:check:ci

- name: Regenerate D1 snapshots
run: node scripts/query-counts.mjs --target d1 --update

- name: Detect snapshot drift
id: drift
Expand Down
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"**/emdash-env.d.ts",
"**/worker-configuration.d.ts",
"packages/registry-lexicons/src/generated/**",
"packages/admin/src/generated/phosphor-icon-buckets/**",
"packages/plugin-cli/schemas/**",
"infra/emdash-bot/.flue/lib/machine.json",
"infra/emdash-bot/BOT_STATE_MACHINE.md"
Expand Down
5 changes: 5 additions & 0 deletions fixtures/perf-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

import { adminClientBundleMetadata } from "../../scripts/check-admin-client-bundle.mjs";

const target = process.env.EMDASH_FIXTURE_TARGET ?? "sqlite";

const sqliteIntegration = emdash({
Expand Down Expand Up @@ -33,4 +35,7 @@ export default defineConfig({
}),
integrations: [react(), target === "d1" ? d1Integration : sqliteIntegration],
devToolbar: { enabled: false },
vite: {
plugins: [adminClientBundleMetadata()],
},
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"knip": "knip --no-exit-code --exclude unlisted,unresolved,exports,types,duplicates",
"new": "create-emdash",
"screenshots": "node scripts/screenshot-all-templates.mjs",
"bundle:check": "node scripts/check-admin-client-bundle.mjs",
"bundle:check:ci": "node scripts/check-admin-client-bundle.mjs --ci-reuse-query-count-build",
"query-counts": "node scripts/query-counts.mjs",
"locale:extract": "pnpm --filter @emdash-cms/admin locale:extract",
"locale:compile": "pnpm --filter @emdash-cms/admin locale:compile"
Expand Down
8 changes: 5 additions & 3 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
"./locales/*": "./dist/locales/*"
},
"scripts": {
"build": "node --run locale:compile && tsdown && node --run locale:copy && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
"dev": "tsdown src/index.ts --format esm --dts --watch",
"build": "node --run icons:generate && node --run locale:compile && tsdown && node --run locale:copy && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
"dev": "node --run icons:generate && tsdown src/index.ts --format esm --dts --watch",
"prepublishOnly": "node --run build",
"check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --ignore-rules=no-resolution",
"test": "vitest",
"typecheck": "tsgo --noEmit",
"typecheck": "node --run icons:check && tsgo --noEmit",
"icons:generate": "node ./scripts/generate-phosphor-icon-buckets.js",
"icons:check": "node ./scripts/generate-phosphor-icon-buckets.js --check",
"locale:compile": "lingui compile --namespace es",
"locale:copy": "node ./scripts/copy-locales.js",
"locale:extract": "lingui extract --clean"
Expand Down
144 changes: 144 additions & 0 deletions packages/admin/scripts/generate-phosphor-icon-buckets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import ts from "typescript";

const BUCKET_COUNT = 32;
const ICON_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
const GENERATED_FILE_PATTERN = /^(?:bucket-\d{2}|module-aliases)\.ts$/;
const checkOnly = process.argv.includes("--check");

const scriptDir = dirname(fileURLToPath(import.meta.url));
const packageDir = join(scriptDir, "..");
const phosphorPackageDir = dirname(
fileURLToPath(import.meta.resolve("@phosphor-icons/react/package.json")),
);
const iconModulesDir = join(phosphorPackageDir, "dist", "csr");
const outputDir = join(packageDir, "src", "generated", "phosphor-icon-buckets");

function getBucket(name) {
let hash = 0x811c9dc5;
for (let index = 0; index < name.length; index++) {
hash = Math.imul(hash ^ name.charCodeAt(index), 0x01000193);
}
return (hash >>> 0) & (BUCKET_COUNT - 1);
}

function getModuleExportNames(fileName) {
const source = readFileSync(join(iconModulesDir, fileName), "utf8");
const sourceFile = ts.createSourceFile(
fileName,
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS,
);
const exportNames = [];
for (const statement of sourceFile.statements) {
if (
ts.isExportDeclaration(statement) &&
statement.exportClause &&
ts.isNamedExports(statement.exportClause)
) {
for (const element of statement.exportClause.elements) {
exportNames.push(element.name.text);
}
}
}
return exportNames.toSorted();
}

const iconModules = readdirSync(iconModulesDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".es.js"))
.map((entry) => ({
moduleName: entry.name.slice(0, -6),
exportNames: getModuleExportNames(entry.name),
}))
.toSorted((a, b) => a.moduleName.localeCompare(b.moduleName));

const exportOwners = new Map();
const moduleAliases = [];
for (const { moduleName, exportNames } of iconModules) {
if (!ICON_NAME_PATTERN.test(moduleName) || moduleName.endsWith("Icon")) {
throw new Error(`Unsupported Phosphor icon module name: ${moduleName}`);
}
if (!exportNames.includes(moduleName) || !exportNames.includes(`${moduleName}Icon`)) {
throw new Error(`Phosphor icon module ${moduleName} is missing its canonical exports`);
}
for (const exportName of exportNames) {
if (!ICON_NAME_PATTERN.test(exportName)) {
throw new Error(`Unsupported Phosphor icon export name: ${exportName}`);
}
const existingOwner = exportOwners.get(exportName);
if (existingOwner) {
throw new Error(
`Phosphor icon export ${exportName} is declared by both ${existingOwner} and ${moduleName}`,
);
}
exportOwners.set(exportName, moduleName);
if (exportName !== moduleName && exportName !== `${moduleName}Icon`) {
moduleAliases.push({ exportName, moduleName });
}
}
}

/** @type {{ moduleName: string; exportNames: string[] }[][]} */
const buckets = Array.from({ length: BUCKET_COUNT }, () => []);
for (const iconModule of iconModules) {
buckets[getBucket(iconModule.moduleName)].push(iconModule);
}

const expectedFiles = new Map(
buckets.map((modules, index) => {
const fileName = `bucket-${String(index).padStart(2, "0")}.ts`;
const exports = modules
.map(
({ moduleName, exportNames }) =>
`export { ${exportNames.join(", ")} } from "@phosphor-icons/react/${moduleName}";`,
)
.join("\n");
return [
fileName,
`// Generated by scripts/generate-phosphor-icon-buckets.js. Do not edit.\n\n${exports}\n`,
];
}),
);
const moduleAliasEntries = moduleAliases
.toSorted((a, b) => a.exportName.localeCompare(b.exportName))
.map(({ exportName, moduleName }) => `\t${exportName}: "${moduleName}",`)
.join("\n");
expectedFiles.set(
"module-aliases.ts",
`// Generated by scripts/generate-phosphor-icon-buckets.js. Do not edit.\n\nexport const PHOSPHOR_ICON_MODULE_ALIASES: Readonly<Record<string, string>> = {\n${moduleAliasEntries}\n};\n`,
);

const existingGeneratedFiles = existsSync(outputDir)
? readdirSync(outputDir).filter((fileName) => GENERATED_FILE_PATTERN.test(fileName))
: [];
const unexpectedFiles = existingGeneratedFiles.filter((fileName) => !expectedFiles.has(fileName));
const staleFiles = [...expectedFiles].flatMap(([fileName, expected]) => {
const filePath = join(outputDir, fileName);
return !existsSync(filePath) || readFileSync(filePath, "utf8") !== expected ? [fileName] : [];
});

if (checkOnly) {
const mismatches = [...staleFiles, ...unexpectedFiles];
if (mismatches.length > 0) {
console.error(
`Generated Phosphor icon buckets are stale: ${mismatches.join(", ")}. Run \`node --run icons:generate\`.`,
);
process.exitCode = 1;
}
} else {
mkdirSync(outputDir, { recursive: true });
for (const fileName of unexpectedFiles) {
rmSync(join(outputDir, fileName));
}
for (const [fileName, source] of expectedFiles) {
const filePath = join(outputDir, fileName);
if (!existsSync(filePath) || readFileSync(filePath, "utf8") !== source) {
writeFileSync(filePath, source);
}
}
}
5 changes: 3 additions & 2 deletions packages/admin/src/components/admin-navigation-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
import type { Icon } from "@phosphor-icons/react";
import * as React from "react";

import { loadPhosphorIcon } from "../lib/phosphor-icon-loader.js";

/** Shared icon vocabulary for first-party admin entities and navigation surfaces. */
export const ADMIN_NAV_ICONS = {
dashboard: SquaresFour,
Expand Down Expand Up @@ -143,8 +145,7 @@ export function resolveNavIcon(name?: string): React.ElementType {
let icon = lazyIconCache.get(componentName);
if (!icon) {
icon = React.lazy(async () => {
const mod = await import("@phosphor-icons/react");
const candidate: unknown = (mod as Record<string, unknown>)[componentName];
const candidate = await loadPhosphorIcon(componentName);
return { default: isIconComponent(candidate) ? candidate : ADMIN_NAV_ICONS.plugins };
});
lazyIconCache.set(componentName, icon);
Expand Down
49 changes: 49 additions & 0 deletions packages/admin/src/generated/phosphor-icon-buckets/bucket-00.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Generated by scripts/generate-phosphor-icon-buckets.js. Do not edit.

export { AppleLogo, AppleLogoIcon } from "@phosphor-icons/react/AppleLogo";
export { ArrowArcRight, ArrowArcRightIcon } from "@phosphor-icons/react/ArrowArcRight";
export { ArrowFatUp, ArrowFatUpIcon } from "@phosphor-icons/react/ArrowFatUp";
export { ArrowSquareLeft, ArrowSquareLeftIcon } from "@phosphor-icons/react/ArrowSquareLeft";
export { BellSimpleRinging, BellSimpleRingingIcon } from "@phosphor-icons/react/BellSimpleRinging";
export { Blueprint, BlueprintIcon } from "@phosphor-icons/react/Blueprint";
export { BookOpen, BookOpenIcon } from "@phosphor-icons/react/BookOpen";
export { CaretDoubleLeft, CaretDoubleLeftIcon } from "@phosphor-icons/react/CaretDoubleLeft";
export { CellSignalFull, CellSignalFullIcon } from "@phosphor-icons/react/CellSignalFull";
export { CodepenLogo, CodepenLogoIcon } from "@phosphor-icons/react/CodepenLogo";
export { Cube, CubeIcon } from "@phosphor-icons/react/Cube";
export { DeviceRotate, DeviceRotateIcon } from "@phosphor-icons/react/DeviceRotate";
export { Devices, DevicesIcon } from "@phosphor-icons/react/Devices";
export { DiamondsFour, DiamondsFourIcon } from "@phosphor-icons/react/DiamondsFour";
export { Dot, DotIcon } from "@phosphor-icons/react/Dot";
export { Faders, FadersIcon } from "@phosphor-icons/react/Faders";
export { FadersHorizontal, FadersHorizontalIcon } from "@phosphor-icons/react/FadersHorizontal";
export { FileC, FileCIcon } from "@phosphor-icons/react/FileC";
export { FileCpp, FileCppIcon } from "@phosphor-icons/react/FileCpp";
export { Flame, FlameIcon } from "@phosphor-icons/react/Flame";
export { GenderFemale, GenderFemaleIcon } from "@phosphor-icons/react/GenderFemale";
export { Hand, HandIcon } from "@phosphor-icons/react/Hand";
export { HandGrabbing, HandGrabbingIcon } from "@phosphor-icons/react/HandGrabbing";
export { LinkedinLogo, LinkedinLogoIcon } from "@phosphor-icons/react/LinkedinLogo";
export { MapPin, MapPinIcon } from "@phosphor-icons/react/MapPin";
export { MapPinPlus, MapPinPlusIcon } from "@phosphor-icons/react/MapPinPlus";
export { MicrosoftTeamsLogo, MicrosoftTeamsLogoIcon } from "@phosphor-icons/react/MicrosoftTeamsLogo";
export { Moon, MoonIcon } from "@phosphor-icons/react/Moon";
export { MusicNote, MusicNoteIcon } from "@phosphor-icons/react/MusicNote";
export { Notebook, NotebookIcon } from "@phosphor-icons/react/Notebook";
export { NumberSquareSeven, NumberSquareSevenIcon } from "@phosphor-icons/react/NumberSquareSeven";
export { Palette, PaletteIcon } from "@phosphor-icons/react/Palette";
export { Person, PersonIcon } from "@phosphor-icons/react/Person";
export { PhoneSlash, PhoneSlashIcon } from "@phosphor-icons/react/PhoneSlash";
export { PipeWrench, PipeWrenchIcon } from "@phosphor-icons/react/PipeWrench";
export { PokerChip, PokerChipIcon } from "@phosphor-icons/react/PokerChip";
export { QuestionMark, QuestionMarkIcon } from "@phosphor-icons/react/QuestionMark";
export { CircleWavyIcon, Seal, SealIcon } from "@phosphor-icons/react/Seal";
export { SmileyMeh, SmileyMehIcon } from "@phosphor-icons/react/SmileyMeh";
export { SmileyMelting, SmileyMeltingIcon } from "@phosphor-icons/react/SmileyMelting";
export { SpeakerHifi, SpeakerHifiIcon } from "@phosphor-icons/react/SpeakerHifi";
export { StarHalf, StarHalfIcon } from "@phosphor-icons/react/StarHalf";
export { Student, StudentIcon } from "@phosphor-icons/react/Student";
export { Tent, TentIcon } from "@phosphor-icons/react/Tent";
export { TextHTwo, TextHTwoIcon } from "@phosphor-icons/react/TextHTwo";
export { VectorTwo, VectorTwoIcon } from "@phosphor-icons/react/VectorTwo";
export { Visor, VisorIcon } from "@phosphor-icons/react/Visor";
42 changes: 42 additions & 0 deletions packages/admin/src/generated/phosphor-icon-buckets/bucket-01.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Generated by scripts/generate-phosphor-icon-buckets.js. Do not edit.

export { AirplaneTakeoff, AirplaneTakeoffIcon } from "@phosphor-icons/react/AirplaneTakeoff";
export { ArrowFatLinesUp, ArrowFatLinesUpIcon } from "@phosphor-icons/react/ArrowFatLinesUp";
export { ArrowsOut, ArrowsOutIcon } from "@phosphor-icons/react/ArrowsOut";
export { ArrowUDownRight, ArrowUDownRightIcon } from "@phosphor-icons/react/ArrowUDownRight";
export { ArrowURightDown, ArrowURightDownIcon } from "@phosphor-icons/react/ArrowURightDown";
export { Asterisk, AsteriskIcon } from "@phosphor-icons/react/Asterisk";
export { BellSlash, BellSlashIcon } from "@phosphor-icons/react/BellSlash";
export { BuildingApartment, BuildingApartmentIcon } from "@phosphor-icons/react/BuildingApartment";
export { BuildingOffice, BuildingOfficeIcon } from "@phosphor-icons/react/BuildingOffice";
export { Car, CarIcon } from "@phosphor-icons/react/Car";
export { CaretCircleUpDown, CaretCircleUpDownIcon } from "@phosphor-icons/react/CaretCircleUpDown";
export { ChartLine, ChartLineIcon } from "@phosphor-icons/react/ChartLine";
export { ChartPolar, ChartPolarIcon } from "@phosphor-icons/react/ChartPolar";
export { Coffee, CoffeeIcon } from "@phosphor-icons/react/Coffee";
export { CurrencyBtc, CurrencyBtcIcon } from "@phosphor-icons/react/CurrencyBtc";
export { DotsNine, DotsNineIcon } from "@phosphor-icons/react/DotsNine";
export { DotsThreeCircleVertical, DotsThreeCircleVerticalIcon } from "@phosphor-icons/react/DotsThreeCircleVertical";
export { FileX, FileXIcon } from "@phosphor-icons/react/FileX";
export { FolderNotchOpenIcon, FolderOpen, FolderOpenIcon } from "@phosphor-icons/react/FolderOpen";
export { Gif, GifIcon } from "@phosphor-icons/react/Gif";
export { Headset, HeadsetIcon } from "@phosphor-icons/react/Headset";
export { List, ListIcon } from "@phosphor-icons/react/List";
export { Megaphone, MegaphoneIcon } from "@phosphor-icons/react/Megaphone";
export { NumberCircleSeven, NumberCircleSevenIcon } from "@phosphor-icons/react/NumberCircleSeven";
export { NumberSquareZero, NumberSquareZeroIcon } from "@phosphor-icons/react/NumberSquareZero";
export { Pants, PantsIcon } from "@phosphor-icons/react/Pants";
export { PaperPlaneRight, PaperPlaneRightIcon } from "@phosphor-icons/react/PaperPlaneRight";
export { PixLogo, PixLogoIcon } from "@phosphor-icons/react/PixLogo";
export { ReceiptX, ReceiptXIcon } from "@phosphor-icons/react/ReceiptX";
export { Resize, ResizeIcon } from "@phosphor-icons/react/Resize";
export { RssSimple, RssSimpleIcon } from "@phosphor-icons/react/RssSimple";
export { SkipBack, SkipBackIcon } from "@phosphor-icons/react/SkipBack";
export { Subtract, SubtractIcon } from "@phosphor-icons/react/Subtract";
export { Sun, SunIcon } from "@phosphor-icons/react/Sun";
export { TagSimple, TagSimpleIcon } from "@phosphor-icons/react/TagSimple";
export { Usb, UsbIcon } from "@phosphor-icons/react/Usb";
export { UserSquare, UserSquareIcon } from "@phosphor-icons/react/UserSquare";
export { WaveSine, WaveSineIcon } from "@phosphor-icons/react/WaveSine";
export { Wheelchair, WheelchairIcon } from "@phosphor-icons/react/Wheelchair";
export { YoutubeLogo, YoutubeLogoIcon } from "@phosphor-icons/react/YoutubeLogo";
37 changes: 37 additions & 0 deletions packages/admin/src/generated/phosphor-icon-buckets/bucket-02.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Generated by scripts/generate-phosphor-icon-buckets.js. Do not edit.

export { AppStoreLogo, AppStoreLogoIcon } from "@phosphor-icons/react/AppStoreLogo";
export { ArrowUpLeft, ArrowUpLeftIcon } from "@phosphor-icons/react/ArrowUpLeft";
export { ArticleMedium, ArticleMediumIcon } from "@phosphor-icons/react/ArticleMedium";
export { ArticleNyTimes, ArticleNyTimesIcon } from "@phosphor-icons/react/ArticleNyTimes";
export { BatteryWarning, BatteryWarningIcon } from "@phosphor-icons/react/BatteryWarning";
export { CellSignalMedium, CellSignalMediumIcon } from "@phosphor-icons/react/CellSignalMedium";
export { Church, ChurchIcon } from "@phosphor-icons/react/Church";
export { City, CityIcon } from "@phosphor-icons/react/City";
export { ClipboardText, ClipboardTextIcon } from "@phosphor-icons/react/ClipboardText";
export { CubeTransparent, CubeTransparentIcon } from "@phosphor-icons/react/CubeTransparent";
export { DeviceMobileSpeaker, DeviceMobileSpeakerIcon } from "@phosphor-icons/react/DeviceMobileSpeaker";
export { GitCommit, GitCommitIcon } from "@phosphor-icons/react/GitCommit";
export { Globe, GlobeIcon } from "@phosphor-icons/react/Globe";
export { GrainsSlash, GrainsSlashIcon } from "@phosphor-icons/react/GrainsSlash";
export { LinuxLogo, LinuxLogoIcon } from "@phosphor-icons/react/LinuxLogo";
export { Lock, LockIcon } from "@phosphor-icons/react/Lock";
export { Needle, NeedleIcon } from "@phosphor-icons/react/Needle";
export { NumberZero, NumberZeroIcon } from "@phosphor-icons/react/NumberZero";
export { PaperPlaneTilt, PaperPlaneTiltIcon } from "@phosphor-icons/react/PaperPlaneTilt";
export { PersonSimpleCircle, PersonSimpleCircleIcon } from "@phosphor-icons/react/PersonSimpleCircle";
export { PianoKeys, PianoKeysIcon } from "@phosphor-icons/react/PianoKeys";
export { Pill, PillIcon } from "@phosphor-icons/react/Pill";
export { Racquet, RacquetIcon } from "@phosphor-icons/react/Racquet";
export { Sailboat, SailboatIcon } from "@phosphor-icons/react/Sailboat";
export { Smiley, SmileyIcon } from "@phosphor-icons/react/Smiley";
export { SoundcloudLogo, SoundcloudLogoIcon } from "@phosphor-icons/react/SoundcloudLogo";
export { SpeakerSimpleLow, SpeakerSimpleLowIcon } from "@phosphor-icons/react/SpeakerSimpleLow";
export { SpeakerSimpleNone, SpeakerSimpleNoneIcon } from "@phosphor-icons/react/SpeakerSimpleNone";
export { StarOfDavid, StarOfDavidIcon } from "@phosphor-icons/react/StarOfDavid";
export { TextH, TextHIcon } from "@phosphor-icons/react/TextH";
export { TextHFour, TextHFourIcon } from "@phosphor-icons/react/TextHFour";
export { TextHThree, TextHThreeIcon } from "@phosphor-icons/react/TextHThree";
export { Timer, TimerIcon } from "@phosphor-icons/react/Timer";
export { Van, VanIcon } from "@phosphor-icons/react/Van";
export { VectorThree, VectorThreeIcon } from "@phosphor-icons/react/VectorThree";
Loading
Loading