chore: migrate package sources from Aphrody

This commit is contained in:
aphrody-dev committed 2026-10-12 08:23:36 +02:00
1 parent a97691eec2
commit 8a60e6518c
11 files changed
+754 -2

No files matched your search

+17 -2
View File
@@ -1,2 +1,17 @@
# aphrody-m3-config # @aphrody/m3-config
Aphrody package @aphrody/m3-config
Shared presets for the first-party Material Design 3 packages (`packages/m3-*`, `packages/m3-react`,
`packages/material-web`). Private; consumed as a `workspace:*` devDependency. Contract and status:
[`docs/design/M3-PLAN.md`](../../docs/plans/design/M3-PLAN.md), enforced by `bun run bun:check` (rule `m3`).
| Export | Use |
| --- | --- |
| `@aphrody/m3-config/tsconfig.json` | `"extends"` for `tsconfig.json`; the package keeps only `include` and its own overrides. |
| `@aphrody/m3-config/tsconfig.build.json` | Second entry of `"extends": ["./tsconfig.json", "@aphrody/m3-config/tsconfig.build.json"]` (declaration-only emit); the package sets `outDir`, `rootDir`, `include`. |
| `@aphrody/m3-config/bun-build` | The one build preset, on `Bun.build`: `m3Package({ entry, target, splitting, external, useClient })` types a package's `build.config.ts`; `scripts/build.ts` at the repository root runs every config (dependency order, content-hash cache, declarations through `tsc`). `m3BunBuild(...)` is the imperative form; `M3_EXTERNAL` and `prependUseClient` (`'use client';` through `Bun.file` / `Bun.write`) are defined once here. |
| `@aphrody/m3-config/cache-db` | `M3UiCache` / `getSharedUiCache()`: embedded SQLite cache on `bun:sqlite` (WAL mode) for themes, icons, tokens, sessions. |
| `@aphrody/m3-config/test/dom` | `bun test` preload: happy-dom + ElementInternals polyfill. |
Workspace packages resolve to their sources under Bun (`bun` / `import` conditions); `dist/` is built by
`bun scripts/build.ts` for consumers that need the `default` condition and the declarations. Bun resolves bare
package specifiers natively before any runtime plugin sees them, so a preload cannot redirect them to sources.
+221
View File
@@ -0,0 +1,221 @@
// SPDX-License-Identifier: Apache-2.0
/**
* The one build preset of the first-party Material Design 3 packages, on `Bun.build`.
*
* Declarative, per package (`build.config.ts`):
*
* import { m3Package } from "@aphrody/m3-config/bun-build";
* export default m3Package({ entry: ["src/index.ts", "src/react.tsx"], useClient: ["react.js"] });
*
* `scripts/build.ts` at the repository root reads every `build.config.ts`, builds the packages in dependency
* order with a content-hash cache and emits the declarations. Imperative, for one-off use:
*
* import { m3BunBuild } from "@aphrody/m3-config/bun-build";
* await m3BunBuild({ entry: ["src/index.ts"] }); // or: bun packages/m3-config/bun-build.ts --entry src/index.ts
*
* Provides native ESM bundling with code splitting, the React client directive (`'use client';`)
* through `Bun.file` / `Bun.write`, external sourcemaps and the shared external list.
*/
import { Glob, type BuildArtifact, type BunPlugin } from "bun";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
export const USE_CLIENT = "'use client';\n";
/** Everything the M3 packages never bundle: React, Lit, motion, Tailwind and the workspace scope. */
export const M3_EXTERNAL: string[] = [
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"lit",
"lit/*",
"@lit/*",
"motion",
"motion/*",
"tailwindcss",
"@aphrody/*",
];
export type M3Target = "browser" | "bun" | "node";
/** What a package declares in `build.config.ts`. Entry paths may be globs, relative to the package. */
export interface M3PackageConfig {
/** Entrypoint(s). Default `["src/index.ts"]`. */
entry?: string | readonly string[];
/** Runtime target (default `browser`). */
target?: M3Target;
/** Code splitting across entries (default true). */
splitting?: boolean;
/** Packages left unbundled in addition to {@link M3_EXTERNAL} and the package's own dependencies and peers; wildcards allowed. */
external?: readonly string[];
/**
* `true` (default) marks every emitted JS file with `'use client';`, a list limits that to the
* named files (`react.js`, `shims/link.js`), `false` skips it (packages without React code).
*/
useClient?: boolean | readonly string[];
}
/** Types a `build.config.ts`; the identity otherwise. */
export function m3Package(config: M3PackageConfig = {}): M3PackageConfig {
return config;
}
export interface M3BunBuildOptions extends M3PackageConfig {
/** Output directory. Defaults to `"dist"`. */
outDir?: string;
/** Module format: `"esm"` (default) or `"cjs"`. */
format?: "esm" | "cjs";
/** Sourcemap mode. Defaults to `"external"`. */
sourcemap?: "external" | "inline" | "none";
/** Minification. Defaults to false. */
minify?: boolean | { whitespace?: boolean; syntax?: boolean; identifiers?: boolean };
/** Additional Bun plugins. */
plugins?: BunPlugin[];
/** Compile-time constant defines. */
define?: Record<string, string>;
/** Working directory. Defaults to `process.cwd()`. */
cwd?: string;
}
export interface M3BunBuildResult {
success: boolean;
outDir: string;
outputs: BuildArtifact[];
logs: string[];
}
/**
* The packages a build leaves unbundled: {@link M3_EXTERNAL}, `node:*`, the `dependencies` and `peerDependencies` of the
* package at `cwd` (consumers install them) and `extra`. Anything else a package imports is bundled in.
*/
export function externals(cwd: string, extra: readonly string[] = []): string[] {
const manifest = join(cwd, "package.json");
const names = new Set<string>();
if (existsSync(manifest)) {
const { dependencies, peerDependencies } = JSON.parse(readFileSync(manifest, "utf8")) as {
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
};
for (const name of [
...Object.keys(dependencies ?? {}),
...Object.keys(peerDependencies ?? {}),
]) {
names.add(name);
names.add(`${name}/*`);
}
}
// `node:*` stays an import in every target: the browser target would otherwise inline Node polyfills.
return [...new Set([...M3_EXTERNAL, "node:*", ...names, ...extra])];
}
/** Expand glob entries (`wrappers/*.ts`) against `cwd`; plain paths pass through. */
export function expandEntries(entries: string | readonly string[], cwd: string): string[] {
const out: string[] = [];
for (const entry of typeof entries === "string" ? [entries] : entries) {
if (/[*?{]/.test(entry)) {
// eslint-disable-next-line unicorn/no-array-sort -- Sorting a fresh array keeps the shared ES2022 type contract.
const matches = [...new Glob(entry).scanSync({ cwd, onlyFiles: true })].sort();
if (matches.length === 0) throw new Error(`m3-config: entry glob matches nothing: ${entry}`);
out.push(...matches.map((m) => resolve(cwd, m)));
} else out.push(resolve(cwd, entry));
}
return out;
}
/** Deepest directory that contains every entry: the base of the output layout (`src/a.ts` -> `dist/a.js`). */
export function commonDir(files: readonly string[]): string {
const [first, ...rest] = files.map((f) => dirname(f).replaceAll("\\", "/").split("/"));
let common = first ?? [];
for (const parts of rest) {
let i = 0;
while (i < common.length && i < parts.length && common[i] === parts[i]) i++;
common = common.slice(0, i);
}
return common.join("/") || "/";
}
/**
* Prepend the React client directive to emitted `.js` files using Bun's native Glob and I/O.
* Returns the number of files changed; a second run changes none.
*/
export async function prependUseClient(dir: string, only?: readonly string[]): Promise<number> {
const files: string[] = [];
for await (const found of new Glob("**/*.js").scan({ cwd: dir, onlyFiles: true })) {
const path = found.replaceAll("\\", "/");
if (!only || only.includes(path.split("/").pop() ?? path) || only.includes(path))
files.push(path);
}
const changed = await Promise.all(
files.map(async (path) => {
const file = join(dir, path);
const content = await Bun.file(file).text();
if (content.startsWith(USE_CLIENT)) return 0;
await Bun.write(file, USE_CLIENT + content);
return 1;
}),
);
return changed.reduce<number>((sum, n) => sum + n, 0);
}
/** Executes a native Bun build for an M3 package. */
export async function m3BunBuild(options: M3BunBuildOptions = {}): Promise<M3BunBuildResult> {
const cwd = options.cwd ?? process.cwd();
const entrypoints = expandEntries(options.entry ?? ["src/index.ts"], cwd);
const outdir = resolve(cwd, options.outDir ?? "dist");
const useClient = options.useClient ?? true;
const result = await Bun.build({
entrypoints,
root: commonDir(entrypoints),
outdir,
target: options.target ?? "browser",
format: options.format ?? "esm",
splitting: options.splitting ?? true,
sourcemap: options.sourcemap ?? "external",
minify: options.minify ?? false,
external: externals(cwd, options.external),
plugins: options.plugins ?? [],
define: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"),
...options.define,
},
throw: false,
});
if (!result.success) {
return {
success: false,
outDir: outdir,
outputs: result.outputs,
logs: result.logs.map((log) => String(log)),
};
}
if (useClient !== false)
await prependUseClient(
outdir,
Array.isArray(useClient) ? (useClient as readonly string[]) : undefined,
);
return { success: true, outDir: outdir, outputs: result.outputs, logs: [] };
}
// Direct CLI invocation: `bun packages/m3-config/bun-build.ts --entry src/index.ts`
if (import.meta.main) {
const args = process.argv.slice(2);
const options: M3BunBuildOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--entry" || arg === "-e") options.entry = args[++i]!;
else if (arg === "--outdir" || arg === "-o") options.outDir = args[++i]!;
else if (arg === "--target" || arg === "-t") options.target = args[++i] as M3Target;
else if (arg === "--minify" || arg === "-m") options.minify = true;
}
const res = await m3BunBuild(options);
if (!res.success) {
console.error("[m3-bun-build] Failed to build:", res.logs.join("\n"));
process.exit(1);
}
console.log(
`[m3-bun-build] Successfully built ${res.outputs.length} artifact(s) to ${res.outDir}`,
);
}
+168
View File
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0
/**
* Native Bun SQLite cache for Aphrody UI runtime and tooling.
*
* Backed by Bun's built-in `bun:sqlite` with WAL mode:
* - Ultra-low latency (<100µs)
* - Zero external dependencies
* - High-concurrency safe via WAL journal mode
* - Used for caching theme palettes, icon SVGs, CSS tokens, and UI session state.
*/
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
export interface CacheEntry<T = unknown> {
key: string;
namespace: string;
value: T;
updatedAt: number;
}
export interface CacheStats {
totalEntries: number;
namespaces: Record<string, number>;
databasePath: string;
}
export class M3UiCache {
private db: Database;
private path: string;
constructor(dbPath: string = ":memory:") {
this.path = dbPath;
if (dbPath !== ":memory:") {
mkdirSync(dirname(dbPath), { recursive: true });
}
this.db = new Database(dbPath, { create: true });
this.init();
}
private init(): void {
this.db.run("PRAGMA journal_mode = WAL;");
this.db.run("PRAGMA synchronous = NORMAL;");
this.db.run("PRAGMA temp_store = MEMORY;");
this.db.run(`
CREATE TABLE IF NOT EXISTS ui_cache (
key TEXT NOT NULL,
namespace TEXT NOT NULL,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (namespace, key)
);
`);
this.db.run(`
CREATE INDEX IF NOT EXISTS idx_ui_cache_ns ON ui_cache (namespace);
`);
}
/**
* Retrieves a cached value, or returns null if not found.
*/
get<T = unknown>(namespace: string, key: string): T | null {
const row = this.db
.query<{ value: string }, [string, string]>(
"SELECT value FROM ui_cache WHERE namespace = ? AND key = ? LIMIT 1",
)
.get(namespace, key);
if (!row) return null;
try {
return JSON.parse(row.value) as T;
} catch {
return row.value as unknown as T;
}
}
/**
* Stores a value in the cache under namespace and key.
*/
set<T = unknown>(namespace: string, key: string, value: T): void {
const serialized = JSON.stringify(value);
if (serialized === undefined) throw new TypeError("M3UiCache: value must be JSON serializable");
const now = Date.now();
this.db
.query(
`INSERT INTO ui_cache (key, namespace, value, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT (namespace, key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at`,
)
.run(key, namespace, serialized, now);
}
/**
* Removes a specific key from the cache.
*/
delete(namespace: string, key: string): boolean {
const result = this.db
.query("DELETE FROM ui_cache WHERE namespace = ? AND key = ?")
.run(namespace, key);
return result.changes > 0;
}
/**
* Clears an entire namespace, or all entries if no namespace specified.
*/
clear(namespace?: string): number {
if (namespace !== undefined) {
const result = this.db.query("DELETE FROM ui_cache WHERE namespace = ?").run(namespace);
return result.changes;
}
const result = this.db.query("DELETE FROM ui_cache").run();
return result.changes;
}
/**
* Returns cache metrics and entry counts.
*/
stats(): CacheStats {
const totalRow = this.db
.query<{ total: number }, []>("SELECT COUNT(*) AS total FROM ui_cache")
.get();
const total = totalRow?.total ?? 0;
const nsRows = this.db
.query<{ namespace: string; count: number }, []>(
"SELECT namespace, COUNT(*) AS count FROM ui_cache GROUP BY namespace",
)
.all();
const namespaces: Record<string, number> = Object.create(null);
for (const r of nsRows) {
namespaces[r.namespace] = r.count;
}
return {
totalEntries: total,
namespaces,
databasePath: this.path,
};
}
/**
* Closes the underlying SQLite database connection.
*/
close(): void {
this.db.close();
}
}
/** Global shared instance for in-process UI caching */
let defaultInstance: M3UiCache | null = null;
export function getSharedUiCache(path?: string): M3UiCache {
if (!defaultInstance) {
const defaultPath =
path ??
process.env.APHRODY_UI_CACHE_PATH ??
(process.env.HOME ? `${process.env.HOME}/.aphrody/cache/ui.sqlite` : ":memory:");
defaultInstance = new M3UiCache(defaultPath);
}
return defaultInstance;
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@aphrody/m3-config",
"version": "1.4.4",
"description": "Shared build, typecheck and test presets for the @aphrody Material Design 3 packages: tsconfig, the Bun.build preset and the happy-dom test preload.",
"license": "Apache-2.0",
"author": "aphrody-code",
"files": [
"README.md",
"bun-build.ts",
"cache-db.ts",
"tsconfig.build.json",
"tsconfig.check.json",
"tsconfig.json",
"test/dom.ts"
],
"type": "module",
"exports": {
"./package.json": "./package.json",
"./tsconfig.json": "./tsconfig.json",
"./tsconfig.build.json": "./tsconfig.build.json",
"./bun-build": "./bun-build.ts",
"./cache-db": "./cache-db.ts",
"./test/dom": "./test/dom.ts"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "bun test",
"typecheck": "bun check -p tsconfig.check.json --noEmit"
},
"dependencies": {
"@happy-dom/global-registrator": "catalog:",
"element-internals-polyfill": "catalog:"
},
"engines": {
"bun": ">=1.4.4"
}
}
+151
View File
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: Apache-2.0
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { M3_EXTERNAL, commonDir, expandEntries, externals, m3BunBuild, m3Package, prependUseClient } from "../bun-build.ts";
const DIRECTIVE = "'use client';\n";
let dir = "";
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "m3-bun-build-"));
await mkdir(join(dir, "src"), { recursive: true });
});
afterEach(async () => {
if (dir) await rm(dir, { recursive: true, force: true });
});
describe("m3BunBuild native preset", () => {
test("prepends 'use client' using native Bun I/O", async () => {
const dist = join(dir, "dist");
await mkdir(join(dist, "sub"), { recursive: true });
await Bun.write(join(dist, "index.js"), "export const a = 1;\n");
await Bun.write(join(dist, "sub", "btn.js"), "export const b = 2;\n");
const modified = await prependUseClient(dist);
expect(modified).toBe(2);
const content1 = await Bun.file(join(dist, "index.js")).text();
const content2 = await Bun.file(join(dist, "sub", "btn.js")).text();
expect(content1).toBe(`${DIRECTIVE}export const a = 1;\n`);
expect(content2).toBe(`${DIRECTIVE}export const b = 2;\n`);
// Second run should be idempotent
const modifiedSecond = await prependUseClient(dist);
expect(modifiedSecond).toBe(0);
});
test("bundles TypeScript code into dist using Bun.build", async () => {
await Bun.write(
join(dir, "src", "index.ts"),
`export const greet = (name: string): string => 'Hello ' + name;`,
);
const result = await m3BunBuild({
entry: "src/index.ts",
cwd: dir,
target: "browser",
useClient: true,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBeGreaterThan(0);
const outFile = join(dir, "dist", "index.js");
expect(await Bun.file(outFile).exists()).toBe(true);
const text = await Bun.file(outFile).text();
expect(text).toContain(DIRECTIVE);
expect(text).toContain("greet");
});
test("preserves M3_EXTERNAL dependencies", () => {
expect(M3_EXTERNAL).toContain("react");
expect(M3_EXTERNAL).toContain("lit");
expect(M3_EXTERNAL).toContain("@aphrody/*");
});
test("an explicit list marks only those files", async () => {
const dist = join(dir, "dist");
await mkdir(join(dist, "sub"), { recursive: true });
await Bun.write(join(dist, "index.js"), "export {};\n");
await Bun.write(join(dist, "react.js"), "export {};\n");
await Bun.write(join(dist, "sub", "link.js"), "export {};\n");
await Bun.write(join(dist, "index.js.map"), "{}");
expect(await prependUseClient(dist, ["react.js", "sub/link.js"])).toBe(2);
expect(await Bun.file(join(dist, "index.js")).text()).toBe("export {};\n");
expect(await Bun.file(join(dist, "react.js")).text()).toStartWith(DIRECTIVE);
expect(await Bun.file(join(dist, "sub", "link.js")).text()).toStartWith(DIRECTIVE);
expect(await Bun.file(join(dist, "index.js.map")).text()).toBe("{}");
});
test("entry globs expand against the package and plain paths pass through", async () => {
await Bun.write(join(dir, "wrappers", "a.ts"), "export {};\n");
await Bun.write(join(dir, "wrappers", "b.ts"), "export {};\n");
expect(expandEntries(["index.ts", "wrappers/*.ts"], dir)).toEqual([
join(dir, "index.ts"),
join(dir, "wrappers", "a.ts"),
join(dir, "wrappers", "b.ts"),
]);
expect(() => expandEntries("nothing/*.ts", dir)).toThrow(/matches nothing/);
});
test("the output layout is based on the deepest common directory of the entries", () => {
expect(commonDir(["/p/src/index.ts", "/p/src/shims/link.ts"])).toBe("/p/src");
expect(commonDir(["/p/index.ts", "/p/wrappers/a.ts"])).toBe("/p");
expect(commonDir(["/p/a/x.ts", "/q/b/y.ts"])).toBe("/");
});
test("m3Package is a typed identity", () => {
const config = { entry: ["src/a.ts"], target: "node", useClient: false } as const;
expect(m3Package({ ...config })).toEqual(config);
expect(m3Package()).toEqual({});
});
test("a build of several entries keeps the src-relative layout, splits shared code and marks the listed entries", async () => {
await Bun.write(join(dir, "src", "shared.ts"), "export const shared = () => 'shared';\n");
await Bun.write(join(dir, "src", "a.ts"), "import { shared } from './shared'; export const a = shared() + 'a';\n");
await Bun.write(join(dir, "src", "react.tsx"), "import { shared } from './shared'; export const r = shared() + 'r';\n");
const result = await m3BunBuild({ entry: ["src/a.ts", "src/react.tsx"], cwd: dir, useClient: ["react.js"] });
expect(result.success).toBe(true);
expect(await Bun.file(join(dir, "dist", "a.js")).text()).not.toStartWith(DIRECTIVE);
expect(await Bun.file(join(dir, "dist", "react.js")).text()).toStartWith(DIRECTIVE);
});
test("a build error is returned, not thrown", async () => {
await Bun.write(join(dir, "src", "bad.ts"), "import './nope';\n");
const result = await m3BunBuild({ entry: "src/bad.ts", cwd: dir });
expect(result.success).toBe(false);
expect(result.logs.join("\n")).toContain("nope");
});
});
describe("externals", () => {
test("shared list, then the package's dependencies and peers with their subpaths, then extras", async () => {
await Bun.write(
join(dir, "package.json"),
JSON.stringify({ dependencies: { swiper: "1" }, peerDependencies: { "@scope/x": "1" }, devDependencies: { bundled: "1" } }),
);
const list = externals(dir, ["bun:*"]);
expect(list).toEqual(expect.arrayContaining([...M3_EXTERNAL, "node:*", "swiper", "swiper/*", "@scope/x", "@scope/x/*", "bun:*"]));
expect(list).not.toContain("bundled");
expect(new Set(list).size).toBe(list.length);
});
test("a dependency stays an import and a dev-only package is bundled in", async () => {
await Bun.write(join(dir, "package.json"), JSON.stringify({ dependencies: { kept: "1" } }));
await Bun.write(join(dir, "node_modules", "kept", "package.json"), '{"name":"kept","main":"index.js"}');
await Bun.write(join(dir, "node_modules", "kept", "index.js"), "export const kept = 'KEPT-BODY';\n");
await Bun.write(join(dir, "node_modules", "inlined", "package.json"), '{"name":"inlined","main":"index.js"}');
await Bun.write(join(dir, "node_modules", "inlined", "index.js"), "export const inlined = 'INLINED-BODY';\n");
await Bun.write(join(dir, "src", "index.ts"), "import { kept } from 'kept'; import { inlined } from 'inlined'; export const both = kept + inlined;\n");
const result = await m3BunBuild({ entry: "src/index.ts", cwd: dir, useClient: false });
expect(result.success).toBe(true);
const out = await Bun.file(join(dir, "dist", "index.js")).text();
expect(out).toContain('from "kept"');
expect(out).not.toContain("KEPT-BODY");
expect(out).toContain("INLINED-BODY");
});
});
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
import { describe, expect, test } from "bun:test";
import { M3UiCache } from "../cache-db.ts";
describe("M3UiCache (native bun:sqlite)", () => {
test("stores, retrieves and deletes cached entries with typed objects", () => {
const cache = new M3UiCache(":memory:");
// Theme palette cache test
const palette = { primary: "#6750a4", secondary: "#625b71" };
cache.set("theme", "seed:#6750a4", palette);
const retrieved = cache.get<typeof palette>("theme", "seed:#6750a4");
expect(retrieved).toEqual(palette);
// Non-existent key
expect(cache.get("theme", "missing")).toBeNull();
// Stats
const stats = cache.stats();
expect(stats.totalEntries).toBe(1);
expect(stats.namespaces["theme"]).toBe(1);
// Delete
expect(cache.delete("theme", "seed:#6750a4")).toBe(true);
expect(cache.get("theme", "seed:#6750a4")).toBeNull();
cache.close();
});
test("clears namespaces selectively or entirely", () => {
const cache = new M3UiCache(":memory:");
cache.set("icons", "home", "<svg>home</svg>");
cache.set("icons", "settings", "<svg>settings</svg>");
cache.set("tokens", "sys-color", ":root { --color: red; }");
expect(cache.stats().totalEntries).toBe(3);
// Clear icons only
const clearedIcons = cache.clear("icons");
expect(clearedIcons).toBe(2);
expect(cache.get("icons", "home")).toBeNull();
expect(cache.get("tokens", "sys-color")).not.toBeNull();
// Clear all
cache.clear();
expect(cache.stats().totalEntries).toBe(0);
cache.close();
});
});
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
import { expect, test } from "bun:test";
import { M3UiCache } from "../cache-db.ts";
test("cache preserves JSON-looking strings as strings", () => {
const cache = new M3UiCache();
try {
for (const value of ["null", "true", "false", "123", '"quoted"', '{"a":1}', "[1,2]", ""]) {
cache.set("values", value, value);
expect(cache.get<string>("values", value)).toBe(value);
}
cache.set("values", "object", { a: [1, null, true] });
expect(cache.get<{ a: (number | boolean | null)[] }>("values", "object")).toEqual({
a: [1, null, true],
});
expect(() => cache.set("values", "undefined", undefined)).toThrow("JSON serializable");
expect(cache.get("values", "undefined")).toBeNull();
} finally {
cache.close();
}
});
test("clearing the empty namespace preserves all other data", () => {
const cache = new M3UiCache();
try {
cache.set("", "key", "empty namespace");
cache.set("other", "key", "protected");
cache.set("__proto__", "key", "literal namespace");
expect(cache.stats().namespaces["__proto__"]).toBe(1);
expect(cache.clear("")).toBe(1);
expect(cache.get<string>("other", "key")).toBe("protected");
expect(cache.get<string>("__proto__", "key")).toBe("literal namespace");
} finally {
cache.close();
}
});
+25
View File
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Shared `bun test` preload: one happy-dom for the whole run (react-dom decides whether it can use
// the DOM when it is first loaded, so the DOM must exist before any test module imports it), plus
// ElementInternals, which happy-dom lacks and the form-associated md-* elements need.
//
// [test]
// preload = ["@aphrody/m3-config/test/dom"]
import { GlobalRegistrator } from "@happy-dom/global-registrator";
const nativeTS = globalThis.TransformStream;
const nativeRS = globalThis.ReadableStream;
const nativeWS = globalThis.WritableStream;
if (!GlobalRegistrator.isRegistered) {
GlobalRegistrator.register();
}
if (nativeTS) globalThis.TransformStream = nativeTS;
if (nativeRS) globalThis.ReadableStream = nativeRS;
if (nativeWS) globalThis.WritableStream = nativeWS;
try {
await import("element-internals-polyfill");
} catch {}
// Rendering is driven with `flushSync`, not React Testing Library: silence the act() warning.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = false;
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"bun-build.ts",
"cache-db.ts",
"test/**/*.ts"
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": false,
"noImplicitOverride": false,
"types": [
"bun",
"node"
],
"jsx": "react-jsx",
"declaration": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true
}
}