From 7f0f5482e549a8a6d462066928ec0b1415ede721 Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Tue, 27 Oct 2026 11:09:42 +0100 Subject: [PATCH 1/2] fix(wp): upload custom fonts without Google Fonts lookup The shared import handler replaced local file data with a Google Fonts request before the custom-aware uploader could run. Preserve local data and report local upload failures with the right error. --- CHANGELOG.md | 1 + .../src/components/modules/fonts/FontsTab.tsx | 13 ++- .../src/__tests__/wpCustomFontImport.test.ts | 98 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 packages/www/src/__tests__/wpCustomFontImport.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e06dff9..f3bb84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed +- Fixed importing a local font into WordPress failing with a Google Font download error. Custom fonts now upload the selected file directly instead of trying to find it on Google Fonts. - Fixed Bricks sites hanging when removed Core Framework classes were cleaned up. The initial lookup joined post metadata five times, multiplying rows on sites with many metadata entries. It now checks all five Bricks metadata keys through one join while retaining the existing batched cleanup. - Fixed transparent variants and shade/tint counts for RAW colors that reference another variable, such as `var(--bg-body)`. Generated variants now follow the current source color, while manually edited swatches are preserved. Older saved swatches without edit history are left unchanged on load; use Reset on a stale swatch to return it to automatic generation, then save to update the site's CSS. - Fixed the block editor going blank with the Core Framework Gutenberg integration enabled. The editor script was loaded with an outdated, hard-coded dependency list that left out WordPress's `react-jsx-runtime`, so rendering the Core Framework class panel threw on an undefined `ReactJSXRuntime` and took the whole editor down. The script now loads the dependencies its build declares. diff --git a/packages/wp/src/components/modules/fonts/FontsTab.tsx b/packages/wp/src/components/modules/fonts/FontsTab.tsx index 2069896..fd33d05 100644 --- a/packages/wp/src/components/modules/fonts/FontsTab.tsx +++ b/packages/wp/src/components/modules/fonts/FontsTab.tsx @@ -88,7 +88,10 @@ export function FontsTab() { if (!font.family) return; try { - const files = await fetchGoogleFontFiles(font.family, selectedVariants); + const files = + font.category === "custom-font" + ? font.files + : await fetchGoogleFontFiles(font.family, selectedVariants); const resolvedFont = { ...font, files }; const id = `${font.title}-${font.family}-${selectedVariants.join(".")}`; const variants = font.variants.reduce((acc: FontVariantData[], v: FontVariantData) => { @@ -107,8 +110,12 @@ export function FontsTab() { setActiveTab(USER_FONTS); toast.success("Font added successfully"); } catch (error) { - console.error("Failed to download Google Font:", error); - toast.error("Could not download the selected Google Font"); + console.error("Failed to import font:", error); + toast.error( + font.category === "custom-font" + ? "Could not upload the selected custom font" + : "Could not download the selected Google Font", + ); } }; diff --git a/packages/www/src/__tests__/wpCustomFontImport.test.ts b/packages/www/src/__tests__/wpCustomFontImport.test.ts new file mode 100644 index 0000000..e8e0363 --- /dev/null +++ b/packages/www/src/__tests__/wpCustomFontImport.test.ts @@ -0,0 +1,98 @@ +import { act, createElement, type ComponentType } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { Provider, createStore, type PrimitiveAtom } from "jotai"; +import { toast } from "sonner"; + +// The WP shell has its own globals and aliases, checked by build:wp. +// Runtime imports keep that separate type environment out of the web build. +const { FontsTab } = require("../../../wp/src/components/modules/fonts/FontsTab") as { FontsTab: ComponentType }; +const { fontsDataAtom } = require("../../../wp/src/state") as { fontsDataAtom: PrimitiveAtom<{ fonts: Record[] }> }; + +jest.mock("../../../wp/src/state", () => { + const { atom } = jest.requireActual("jotai"); + return { fontsDataAtom: atom({ fonts: [] }) }; +}); +jest.mock("../../../wp/src/components/ClassSectionHeading", () => ({ ClassSectionHeading: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/components/FontsList", () => ({ FontsList: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/ImportGoogleFonts", () => ({ ImportGoogleFonts: () => null })); +jest.mock("../../../wp/src/components/modules/fonts/ImportCustomFonts", () => ({ + ImportCustomFonts: ({ setSelectedVariants, downloadFontsLocally }: { setSelectedVariants: (variants: string[]) => void; downloadFontsLocally: (font: unknown) => Promise }) => + createElement("div", null, createElement("button", { + onClick: () => { + setSelectedVariants(["400normal"]); + }, + }, "Select file"), + createElement("button", { onClick: (event: Event) => { + event.stopPropagation(); + return downloadFontsLocally({ family: "Local Test Font", title: "Local Test Font", category: "custom-font", + files: { "400normal": "data:font/woff2;base64,d09GMg==" }, + variants: [{ id: "400normal", cssSelector: "", comment: "", fontDisplay: "auto" }], + customSelectors: "", customVariable: "" }); + } }, "Add Font")), +})); +jest.mock("../../../wp/src/components/modules/fonts/utils/utils", () => { + Object.assign(globalThis, { TextEncoder: require("node:util").TextEncoder, TextDecoder: require("node:util").TextDecoder }); + return jest.requireActual("../../../wp/src/components/modules/fonts/utils/utils"); +}); +jest.mock("sonner", () => ({ toast: { error: jest.fn(), success: jest.fn() } })); + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const originalFetch = globalThis.fetch; +let root: Root; +let container: HTMLDivElement; +let store: ReturnType; +const fetchMock = jest.fn(); + +beforeEach(async () => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => {}); + globalThis.fetch = fetchMock; + Object.assign(window, { + coreframework: { core_api_url: "https://example.test/wp-json/core-framework/v2/" }, + wpApiSettings: { nonce: "test-nonce" }, + }); + fetchMock.mockImplementation(async (url: string) => { + if (url.includes("googleapis.com")) throw new Error("This local font is not on Google Fonts"); + return { ok: true, json: async () => ({ success: true }) }; + }); + store = createStore(); + container = document.createElement("div"); + root = createRoot(container); + await act(async () => root.render(createElement(Provider, { store }, createElement(FontsTab)))); +}); + +afterEach(() => { + act(() => root.unmount()); + globalThis.fetch = originalFetch; + jest.restoreAllMocks(); +}); + +async function click(text: string) { + const button = [...container.querySelectorAll("button")].find((button) => button.textContent === text || button.firstChild?.textContent === text); + if (!button) throw new Error(`Missing button: ${text}`); + await act(async () => button.click()); +} + +test("uploads the selected local file without replacing it with a Google Fonts lookup", async () => { + await click("Import Local Font"); + await click("Select file"); + await click("Add Font"); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith("https://example.test/wp-json/core-framework/v2/upload-fonts", expect.objectContaining({ + method: "POST", + body: JSON.stringify({ fonts: [{ font_base64: "d09GMg==", filename: "Local-Test-Font-400normal.woff2" }] }), + })); + expect(store.get(fontsDataAtom).fonts).toEqual([expect.objectContaining({ family: "Local Test Font", category: "custom-font" })]); + expect(toast.success).toHaveBeenCalledWith("Font added successfully"); + expect(toast.error).not.toHaveBeenCalled(); +}); + +test("keeps a failed local upload out of the project and reports a custom-font error", async () => { + fetchMock.mockResolvedValue({ ok: false, status: 500 }); + await click("Import Local Font"); + await click("Select file"); + await click("Add Font"); + expect(store.get(fontsDataAtom).fonts).toEqual([]); + expect(toast.success).not.toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith("Could not upload the selected custom font"); +}); From 62006e8def40c326c6c72f2f71731157db3eeda7 Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Tue, 27 Oct 2026 11:10:19 +0100 Subject: [PATCH 2/2] fix(release): require WordPress changelog entries The 2.0.2 readme omitted its release notes, leaving plugin details on 2.0.1. Restore those notes and reject future packages whose readme lacks a non-empty entry for the version being built. --- .claude/skills/release/SKILL.md | 5 +++-- CHANGELOG.md | 1 + RELEASING.md | 4 +++- packages/wp/readme.txt | 8 ++++++++ scripts/build-wp-release.ts | 8 ++++++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 5a50278..f6156b1 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -35,7 +35,8 @@ On 2026-09-26 the project was found still connected to the old private repo `Oxy 1. **On `main`, clean tree, CI green.** `scripts/release.ts` enforces branch and cleanliness and stops otherwise. 2. **Version bumped via `bun run bump`.** One constant, `APP_VERSION` in `packages/core/src/constants/version.ts`, drives ten pinned files — see `maintain` → *Registries*. The release builders reject a tag that does not match all of them, including `packages/wp/readme.txt`'s WordPress.org stable tag. 3. **Changelog closed.** Per `maintain`, entries are written per change, so release only *verifies*. Confirm every PR merged since the last tag is either present or justifiably absent, then close the open version by **adding its date** to the existing dateless heading. Nothing else moves. If you are authoring entries from `git log` at this point, the discipline failed upstream — say so. -4. **The gate passes.** See `run-and-test`. Docker must be running for `e2e:wp`. +4. **WordPress release notes included.** Copy the release's user-facing notes, including upgrade instructions and breaking changes, into a matching `= X.Y.Z =` entry under `== Changelog ==` in `packages/wp/readme.txt`. The WordPress builder rejects a missing or empty entry for the version being packaged; a link to GitHub does not replace these notes. +5. **The gate passes.** See `run-and-test`. Docker must be running for `e2e:wp`. ## Cutting it @@ -79,7 +80,7 @@ Reverting means shipping a *new, higher* version. Plan accordingly. ## Verifying a release actually landed -- WordPress.org shows the new stable tag, matching `packages/wp/readme.txt`. +- WordPress.org shows the new stable tag and its release notes, matching `packages/wp/readme.txt`. - The GitHub Release carries **both** `core-framework-X.Y.Z.zip` and `core-framework-figma-X.Y.Z.zip`. - The `publish` job succeeded — a green `verify` alone means nothing shipped. - Figma Community still shows the old version until David publishes by hand. Report that as outstanding, not done. diff --git a/CHANGELOG.md b/CHANGELOG.md index e06dff9..d17ce97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed +- Restored the missing 2.0.2 release notes in the WordPress plugin readme. Future WordPress packages now require a changelog entry for their version so update notes remain available in WordPress's plugin details. - Fixed Bricks sites hanging when removed Core Framework classes were cleaned up. The initial lookup joined post metadata five times, multiplying rows on sites with many metadata entries. It now checks all five Bricks metadata keys through one join while retaining the existing batched cleanup. - Fixed transparent variants and shade/tint counts for RAW colors that reference another variable, such as `var(--bg-body)`. Generated variants now follow the current source color, while manually edited swatches are preserved. Older saved swatches without edit history are left unchanged on load; use Reset on a stale swatch to return it to automatic generation, then save to update the site's CSS. - Fixed the block editor going blank with the Core Framework Gutenberg integration enabled. The editor script was loaded with an outdated, hard-coded dependency list that left out WordPress's `react-jsx-runtime`, so rendering the Core Framework class panel threw on an undefined `ReactJSXRuntime` and took the whole editor down. The script now loads the dependencies its build declares. diff --git a/RELEASING.md b/RELEASING.md index e4f929e..7899b2a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,7 +5,7 @@ Core Framework uses a tag-driven release process for the WordPress and Figma plu ## Release flow 1. Keep `main` releasable and make sure CI is green. -2. Run `bun run bump`, then update the changelog. +2. Run `bun run bump`, date the release's section in `CHANGELOG.md`, and copy its user-facing notes into a matching `= X.Y.Z =` entry under `== Changelog ==` in `packages/wp/readme.txt`. Include upgrade instructions and breaking changes so users can read them in WordPress's plugin details without visiting GitHub. 3. Commit the release changes to `main`. 4. Create and push a semantic version tag such as `v2.0.0`. 5. GitHub Actions verifies the repository and builds versioned WordPress and Figma release bundles. @@ -37,6 +37,8 @@ git push origin v2.0.0 The tag version must exactly match the WordPress plugin header, package metadata (including `packages/figma/package.json`), Core Framework version constant, and WordPress.org stable tag. The release builders reject mismatches. +The WordPress builder also rejects a missing or empty changelog entry for that version in `packages/wp/readme.txt`. It copies that readme into the release ZIP and WordPress.org deployment. Verify the notes alongside the new stable tag after publishing. + The release builders also reject retired hosted-service URLs, remote placeholder images, the removed remote interface font, and stale Figma version copy. They generate a production dependency license inventory for each artifact. The GitHub Release contains: diff --git a/packages/wp/readme.txt b/packages/wp/readme.txt index 0c4def3..60061ee 100644 --- a/packages/wp/readme.txt +++ b/packages/wp/readme.txt @@ -75,6 +75,14 @@ No. The font browser uses Google's public, keyless font endpoints. == Changelog == += 2.0.2 = + +* Fixed saving from Figma Desktop when its sandbox blocks local storage. +* Fixed Figma connections to WordPress sites using Plain permalinks, and added clearer connection errors. +* Fixed Oxygen Classic hanging on load when a project contains custom fonts. +* Stopped disabled fonts from appearing in the Oxygen and Bricks builders. +* Fixed local fonts with spaces in their family names failing to load. Re-save an affected font after updating to regenerate its CSS. + = 2.0.1 = * Fixed the Auto BEM class generator not appearing in the Bricks structure panel after updating to 2.0.0. diff --git a/scripts/build-wp-release.ts b/scripts/build-wp-release.ts index 0465277..3af98a7 100644 --- a/scripts/build-wp-release.ts +++ b/scripts/build-wp-release.ts @@ -57,6 +57,14 @@ async function getReleaseVersion(): Promise { } } + const readme = await readFile(join(WP_DIR, "readme.txt"), "utf8"); + const changelog = readme.split(/^== Changelog ==\s*$/m)[1]?.split(/^== /m)[0]; + const releaseHeading = new RegExp(`^= ${version.replaceAll(".", "\\.")} =\\s*$`, "m"); + const releaseNotes = changelog?.split(releaseHeading)[1]?.split(/^= /m)[0]; + if (!releaseNotes?.trim()) { + fail(`packages/wp/readme.txt needs a non-empty Changelog entry for ${version} before packaging`); + } + return version; }