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
7 changes: 7 additions & 0 deletions .changeset/flags-subpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@devslab/site-kit": patch
---

Move flag locale menu data (`FLAG_COUNTRY`, `LOCALE_FLAGS`, `flagFor`) off the runtime-neutral `.` entry and onto a dedicated `@devslab/site-kit/flags` subpath, so consumers that never render a flag menu no longer bundle ~110 KB of vendored SVG. `src/solid/locale-menu.tsx` already imported `../core/flags.mjs` directly, so this only changes the public barrel.

Since `0.4.0` has not been published yet, this patch bump lands as `0.4.1` — the fixed release group means the flag locale menu (0.4.0) and this follow-up ship together as `0.4.1`.
4 changes: 3 additions & 1 deletion packages/site-kit/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ claim leaf가 검증된 사실 레지스트리를 참조하도록 강제한다.
디스클로저를 렌더링한다 — JavaScript 없이도 동작하며, Solid는 Escape로 닫기와
`onLocaleChange(locale, href)` 콜백을 더한다. `SiteHeader`는 `localeVariant`를
그대로 전달한다. 국기 데이터(`FLAG_COUNTRY`, `LOCALE_FLAGS`, `flagFor`)는
런타임 중립 entry에서 export되며, 아트워크는 flag-icons에서 벤더링했다(MIT,
런타임 중립 `.` entry가 아니라 전용 서브패스 `@devslab/site-kit/flags`에서
export된다 — 벤더링한 아트워크가 SVG ~110 KB라 대부분의 소비자는 국기 메뉴를
렌더링하지 않기 때문이다. 아트워크는 flag-icons에서 벤더링했다(MIT,
`flags/LICENSE-flag-icons.txt`). 국기는 `dds-icons` 항목이 아니라 site-kit
데이터다 — 아이콘 세트의 계약이 단색 `currentColor` 스트로크를 요구하기
때문이다.
2 changes: 1 addition & 1 deletion packages/site-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ control search indexing, citation crawlers, and model-training crawlers.

## Locale menu variants

`LocaleMenu` renders a native `<select>` by default. `variant="flag"` renders a `<details>` disclosure whose trigger is the current locale's flag and whose rows are flag + native-name links — it works without JavaScript; Solid adds Escape-to-close and the `onLocaleChange(locale, href)` callback. `SiteHeader` forwards `localeVariant`. Flag data (`FLAG_COUNTRY`, `LOCALE_FLAGS`, `flagFor`) is exported from the runtime-neutral entry; the artwork is vendored from flag-icons (MIT, `flags/LICENSE-flag-icons.txt`). Flags are site-kit data, not `dds-icons` entries, because the icon set's contract requires single-colour `currentColor` strokes.
`LocaleMenu` renders a native `<select>` by default. `variant="flag"` renders a `<details>` disclosure whose trigger is the current locale's flag and whose rows are flag + native-name links — it works without JavaScript; Solid adds Escape-to-close and the `onLocaleChange(locale, href)` callback. `SiteHeader` forwards `localeVariant`. Flag data (`FLAG_COUNTRY`, `LOCALE_FLAGS`, `flagFor`) is exported from `@devslab/site-kit/flags`, a dedicated subpath — not the runtime-neutral `.` entry — because the vendored artwork is ~110 KB of SVG and most consumers never render a flag menu. The artwork is vendored from flag-icons (MIT, `flags/LICENSE-flag-icons.txt`). Flags are site-kit data, not `dds-icons` entries, because the icon set's contract requires single-colour `currentColor` strokes.
4 changes: 4 additions & 0 deletions packages/site-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"types": "./src/core/index.d.mts",
"import": "./src/core/index.mjs"
},
"./flags": {
"types": "./src/core/flags.d.mts",
"import": "./src/core/flags.mjs"
},
"./solid": {
"types": "./dist/index.d.ts",
"worker": "./dist/solid.server.js",
Expand Down
7 changes: 6 additions & 1 deletion packages/site-kit/scripts/build-flags.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const FLAG_COUNTRY = {
ko: "kr", en: "us", ja: "jp", "zh-HK": "hk", "zh-TW": "tw", hi: "in", vi: "vn",
id: "id", th: "th", "pt-BR": "br", fr: "fr", de: "de", es: "es", ar: "sa",
};
const HEADER = "/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT) */";
const HEADER =
"/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT). Copyright (c) 2013 Panayiotis Lipiridis */";

const entries = Object.entries(FLAG_COUNTRY).map(([locale, country]) => {
const text = readFileSync(join(pkg, "flags", `${country}.svg`), "utf8").trim();
Expand All @@ -27,6 +28,10 @@ const entries = Object.entries(FLAG_COUNTRY).map(([locale, country]) => {
if (!viewBox) throw new Error(`flags/${country}.svg: no viewBox`);
const body = text.replace(/^<svg\b[^>]*>/, "").replace(/<\/svg>\s*$/, "").trim();
if (/<script|on[a-z]+=/i.test(body)) throw new Error(`flags/${country}.svg: active content`);
if (/\bid='/.test(body)) throw new Error(`flags/${country}.svg: id attribute must be double-quoted`);
for (const match of body.matchAll(/\bid="([^"]*)"/g)) {
if (!/^[\w-]+$/.test(match[1])) throw new Error(`flags/${country}.svg: unsafe id attribute value: ${match[1]}`);
}
return { locale, country, viewBox, body };
});

Expand Down
2 changes: 1 addition & 1 deletion packages/site-kit/src/core/flags.d.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT) */
/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT). Copyright (c) 2013 Panayiotis Lipiridis */
import type { SiteLocale } from "./locales.mjs";
export interface LocaleFlag { readonly country: string; readonly viewBox: string; readonly body: string }
export declare const FLAG_COUNTRY: Readonly<Record<SiteLocale, string>>;
Expand Down
2 changes: 1 addition & 1 deletion packages/site-kit/src/core/flags.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT) */
/* Generated by scripts/build-flags.mjs — do not edit. Source: flags/*.svg (flag-icons, MIT). Copyright (c) 2013 Panayiotis Lipiridis */
export const FLAG_COUNTRY = Object.freeze({
"ko": "kr",
"en": "us",
Expand Down
1 change: 0 additions & 1 deletion packages/site-kit/src/core/index.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export * from "./locales.mjs";
export * from "./catalog.mjs";
export * from "./seo.mjs";
export * from "./geo.mjs";
export * from "./flags.mjs";
1 change: 0 additions & 1 deletion packages/site-kit/src/core/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export { LOCALES, canonicalLocale, localeAttributes, resolveLocale } from "./loc
export { CatalogValidationError, createTranslator, validateCatalogs } from "./catalog.mjs";
export { ROBOTS_USER_AGENTS, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
export { VerifiedFactRegistry, buildVerifiedJsonLd, renderLlmsTxt } from "./geo.mjs";
export { FLAG_COUNTRY, LOCALE_FLAGS, flagFor } from "./flags.mjs";
2 changes: 1 addition & 1 deletion packages/site-kit/src/solid/__tests__/locale-menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ it("flag variant renders a flag-only trigger named after the current language",
const trigger = host.querySelector<HTMLElement>(".site-locale-flag__trigger")!;
expect(trigger.tagName).toBe("SUMMARY");
expect(trigger.textContent?.trim()).toBe("");
expect(trigger.getAttribute("aria-label")).toBe("English");
expect(trigger.getAttribute("aria-label")).toBe("Language: English");
expect(trigger.querySelector("svg")?.getAttribute("aria-hidden")).toBe("true");
});

Expand Down
6 changes: 5 additions & 1 deletion packages/site-kit/src/solid/__tests__/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { renderToString } from "solid-js/web";
import { expect, it } from "vitest";

import { LocaleMenu, MarketingShell } from "../index";
import { LOCALE_FLAGS } from "../../core/flags.mjs";
import { locale, messages } from "./fixtures";

const Fixture = () => <MarketingShell
Expand All @@ -23,5 +24,8 @@ it("server-renders the flag variant as a working disclosure without JavaScript",
expect(html).toContain('class="site-locale-flag__trigger"');
expect((html.match(/class="site-locale-flag__option"/g) ?? []).length).toBe(14);
expect(html).toContain('href="/ar"');
expect(html).not.toContain("<script");
expect(html).toContain('viewBox="0 0 640 480"');
const koPath = LOCALE_FLAGS.ko.body.match(/d="[^"]{20,}"/)?.[0];
if (!koPath) throw new Error("expected a d=\"…\" path fragment in LOCALE_FLAGS.ko.body");
expect(html).toContain(koPath);
});
2 changes: 1 addition & 1 deletion packages/site-kit/src/solid/locale-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function FlagLocaleMenu(props: LocaleMenuProps) {
};
return (
<details ref={details} class="site-locale-flag" onKeyDown={onKeyDown}>
<summary ref={trigger} class="site-locale-flag__trigger" aria-label={current().nativeName} title={props.messages.localeLabel}>
<summary ref={trigger} class="site-locale-flag__trigger" aria-label={`${props.messages.localeLabel}: ${current().nativeName}`} title={props.messages.localeLabel}>
<Flag locale={props.state.locale} class="site-locale-flag__svg" />
</summary>
<ul class="site-locale-flag__list" role="list">
Expand Down
22 changes: 21 additions & 1 deletion scripts/verify-site-kit-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,34 @@ try {
const installedRoot = join(temp, "node_modules", "@devslab", "site-kit");
const manifest = JSON.parse(await readFile(join(installedRoot, "package.json"), "utf8"));
assert.equal(manifest.name, "@devslab/site-kit");
for (const path of ["dist/solid.js", "dist/index.d.ts", "src/core/index.mjs", "src/core/index.d.mts", "src/tanstack-start.mjs", "src/tanstack-start.d.mts", "styles.css"]) {
for (const path of [
"dist/solid.js", "dist/index.d.ts",
"src/core/index.mjs", "src/core/index.d.mts",
"src/core/flags.mjs", "src/core/flags.d.mts",
"src/tanstack-start.mjs", "src/tanstack-start.d.mts",
"styles.css", "flags/LICENSE-flag-icons.txt",
]) {
await access(join(installedRoot, path));
}
assert.equal(manifest.publishConfig.access, "public");
assert.equal(manifest.license, "SEE LICENSE IN LICENSE");
assert.equal(manifest.peerDependencies["solid-js"], "1.9.15");
const core = await import(pathToFileURL(join(installedRoot, "src", "core", "index.mjs")));
assert.equal(core.LOCALES.length, 14);
assert.equal(
typeof (await import(pathToFileURL(join(installedRoot, "src", "core", "flags.mjs")))).flagFor,
"function",
"src/core/flags.mjs must export flagFor",
);
assert.ok(manifest.exports["./flags"], "package.json exports must declare a ./flags subpath");
const subpathProbe = join(temp, "resolve-flags-subpath.mjs");
await writeFile(
subpathProbe,
'import { flagFor } from "@devslab/site-kit/flags";\nif (typeof flagFor !== "function") throw new Error("@devslab/site-kit/flags did not resolve to flagFor");\n',
"utf8",
);
const subpathResult = spawnSync(process.execPath, [subpathProbe], { cwd: temp, encoding: "utf8" });
if (subpathResult.status !== 0) throw new Error(`@devslab/site-kit/flags subpath resolution failed\n${subpathResult.stdout}\n${subpathResult.stderr}`);
console.log("site-kit pack, public publish dry-run, and fresh consumer import passed");
} finally {
await rm(temp, { recursive: true, force: true });
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/site-kit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const flagFixture = `<!doctype html><html lang="ar" dir="rtl"><head><meta charse
<a class="site-brand" href="/ar">لينك</a>
<button type="button" class="dds-btn dds-btn--ghost site-menu-button" aria-expanded="false" aria-controls="site-navigation">فتح القائمة</button>
<nav id="site-navigation" class="site-nav" data-open="false" aria-label="التنقل الرئيسي"><ul class="site-nav__list"><li><a href="/docs">الوثائق</a></li></ul></nav>
<div class="site-header__controls" data-open="false"><details class="site-locale-flag"><summary class="site-locale-flag__trigger" aria-label="العربية"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="green"/></svg></summary><ul class="site-locale-flag__list" role="list"><li><a class="site-locale-flag__option" href="/ko" lang="ko" hreflang="ko" dir="ltr"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="white"/></svg><span>한국어</span></a></li><li><a class="site-locale-flag__option" href="/ar" lang="ar" hreflang="ar" dir="rtl" aria-current="true"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="green"/></svg><span>العربية</span></a></li></ul></details><a class="dds-btn dds-btn--primary" href="/access">طلب الوصول</a></div>
<div class="site-header__controls" data-open="false"><details class="site-locale-flag"><summary class="site-locale-flag__trigger" aria-label="اللغة: العربية"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="green"/></svg></summary><ul class="site-locale-flag__list" role="list"><li><a class="site-locale-flag__option" href="/ko" lang="ko" hreflang="ko" dir="ltr"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="white"/></svg><span>한국어</span></a></li><li><a class="site-locale-flag__option" href="/ar" lang="ar" hreflang="ar" dir="rtl" aria-current="true"><svg class="site-locale-flag__svg" viewBox="0 0 640 480" aria-hidden="true"><rect width="640" height="480" fill="green"/></svg><span>العربية</span></a></li></ul></details><a class="dds-btn dds-btn--primary" href="/access">طلب الوصول</a></div>
</div></header><main id="main-content" class="site-main"><h1>واجهة عربية طويلة لا ينبغي أن تتجاوز عرض الشاشة</h1></main></div>
<script>document.querySelector('.site-menu-button').addEventListener('click', (event) => { const button=event.currentTarget; const open=button.getAttribute('aria-expanded')!=='true'; button.setAttribute('aria-expanded', String(open)); document.querySelector('#site-navigation').dataset.open=String(open); document.querySelector('.site-header__controls').dataset.open=String(open); });</script>
</body></html>`;
Expand Down Expand Up @@ -110,7 +110,7 @@ for (const width of [1280, 375]) {
await page.getByRole("button", { name: "فتح القائمة" }).click();
}
const trigger = page.locator(".site-locale-flag__trigger");
await expect(trigger).toHaveAttribute("aria-label", "العربية");
await expect(trigger).toHaveAttribute("aria-label", "اللغة: العربية");
const box = (await trigger.boundingBox())!;
expect(box.width).toBeGreaterThanOrEqual(44);
expect(box.height).toBeGreaterThanOrEqual(44);
Expand Down
6 changes: 5 additions & 1 deletion tests/site-kit-contracts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ test("flag locale menu is a native disclosure with tokenised, logical styles", a
assert.match(styles, /min-block-size:\s*44px/);
assert.doesNotMatch(styles, /#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(/, "site-kit styles must stay on tokens");
const core = await read("packages/site-kit/src/core/index.mjs");
assert.match(core, /flagFor/);
assert.doesNotMatch(core, /flagFor/);
const manifest = await json("packages/site-kit/package.json");
assert.ok(manifest.exports["./flags"], "flag data must ship on its own subpath, not the root barrel");
assert.equal(manifest.exports["./flags"].types, "./src/core/flags.d.mts");
assert.equal(manifest.exports["./flags"].import, "./src/core/flags.mjs");
assert.match(await read("packages/site-kit/package.json"), /build-flags\.mjs --check/);
});

Expand Down