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 .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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 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 Figma synchronization stopping when a RAW color referenced another variable without a dark-mode value. Light and dark colors now resolve independently, so mixed literal and referenced colors keep their correct values.
- 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.
Expand Down
4 changes: 3 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions packages/wp/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 10 additions & 3 deletions packages/wp/src/components/modules/fonts/FontsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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",
);
}
};

Expand Down
98 changes: 98 additions & 0 deletions packages/www/src/__tests__/wpCustomFontImport.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>[] }> };

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<void> }) =>
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<typeof createStore>;
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");
});
8 changes: 8 additions & 0 deletions scripts/build-wp-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ async function getReleaseVersion(): Promise<string> {
}
}

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;
}

Expand Down
Loading