mirror of
https://github.com/luxwallet/create.git
synced 2026-10-30 19:49:39 +00:00
Generator that scaffolds a ready-to-run branded Lux Wallet web app for a new
chain/org from a single BrandConfig. One command per new chain.
- CLI `create-lux-wallet <brand-ref> [outDir] [--force]` + programmatic
generate(brand, outDir): validates the brand via @luxwallet/brand's
defineBrand, then stamps apps/web/ (Vite + React) that mounts @luxwallet/ui
via WalletUIProvider, authenticates with @hanzo/iam (HIP-0111), reaches
chains through @luxwallet/sdk, and ships the brand's /brand.json.
- DRY runtime-brand.json strategy: ONE brand-neutral app shell, byte-identical
across brands (enforced by test). Only public/brand.json + public/logo.svg
differ. No per-brand forks. *.tmpl files get {{brand.*}} interpolation.
- Example brands lux/hanzo/zoo/pars with canonical @luxwallet/chains ids
(lux-c 96369, zoo 200200, hanzo 36963, pars 494949) + _template.json.
- New chains: add to @luxwallet/chains registry, reference id in brand.chains[].
- MIT, zero GPL — never imports @luxfi/wallet. @luxwallet/* + @hanzo/* are
workspace deps with a documented switch to npm versions once published.
Verified headlessly: CLI tsc --noEmit clean; 7/7 node:test pass (tree,
brand.json validity via defineBrand, no token leak, DRY shell identity, force,
resolution); sample generation of all four brands validates.
136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
/**
|
|
* Tests for @luxwallet/create. Node's built-in test runner (stdlib, zero deps).
|
|
*
|
|
* Run the BUILT output (dist/) the way consumers do:
|
|
* pnpm build && node --test
|
|
*
|
|
* (We import from ../dist so the test exercises the shipped artifact, and so it
|
|
* runs as plain JS under `node --test` without a TS loader.)
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { generate, loadBrand, listBundledBrands, parseBrand } from "../dist/index.js";
|
|
import { defineBrand } from "@luxwallet/brand";
|
|
|
|
function freshDir(): string {
|
|
return mkdtempSync(join(tmpdir(), "lw-create-"));
|
|
}
|
|
|
|
test("listBundledBrands returns the four example brands", () => {
|
|
const brands = listBundledBrands();
|
|
for (const id of ["lux", "hanzo", "zoo", "pars"]) {
|
|
assert.ok(brands.includes(id), `expected bundled brand ${id}`);
|
|
}
|
|
assert.ok(!brands.includes("_template"), "_template must be excluded");
|
|
});
|
|
|
|
test("every bundled brand validates via defineBrand", () => {
|
|
for (const id of listBundledBrands()) {
|
|
const brand = loadBrand(id);
|
|
assert.equal(brand.id, id);
|
|
assert.doesNotThrow(() => defineBrand(brand));
|
|
}
|
|
});
|
|
|
|
test("generate stamps a complete tree and a VALID brand.json", () => {
|
|
const out = freshDir();
|
|
try {
|
|
const brand = loadBrand("lux");
|
|
const result = generate(brand, out, { force: true });
|
|
|
|
// Expected files exist.
|
|
const web = join(out, "apps", "web");
|
|
for (const f of [
|
|
"package.json",
|
|
"index.html",
|
|
"tsconfig.json",
|
|
"vite.config.ts",
|
|
"src/main.tsx",
|
|
"src/App.tsx",
|
|
"src/Login.tsx",
|
|
"public/brand.json",
|
|
"public/logo.svg",
|
|
"README.md",
|
|
]) {
|
|
assert.ok(existsSync(join(web, f)), `missing ${f}`);
|
|
}
|
|
assert.ok(result.files.length >= 10);
|
|
|
|
// Emitted brand.json round-trips through defineBrand.
|
|
const emitted = parseBrand(readFileSync(join(web, "public", "brand.json"), "utf8"));
|
|
assert.equal(emitted.id, "lux");
|
|
assert.equal(emitted.iam.clientId, "lux-wallet");
|
|
assert.deepEqual(emitted.chains, ["lux-c-mainnet", "lux-x-mainnet", "lux-p-mainnet"]);
|
|
} finally {
|
|
rmSync(out, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("no template tokens leak and no .tmpl files are emitted", () => {
|
|
const out = freshDir();
|
|
try {
|
|
generate(loadBrand("hanzo"), out, { force: true });
|
|
const web = join(out, "apps", "web");
|
|
const pkg = readFileSync(join(web, "package.json"), "utf8");
|
|
assert.match(pkg, /"name":\s*"hanzo-wallet-web"/);
|
|
assert.ok(!pkg.includes("{{brand"), "package.json still has tokens");
|
|
assert.ok(!existsSync(join(web, "package.json.tmpl")), ".tmpl leaked");
|
|
const readme = readFileSync(join(web, "README.md"), "utf8");
|
|
assert.ok(!readme.includes("{{brand"), "README still has tokens");
|
|
} finally {
|
|
rmSync(out, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("app shell is byte-identical across brands (DRY)", () => {
|
|
const a = freshDir();
|
|
const b = freshDir();
|
|
try {
|
|
generate(loadBrand("lux"), a, { force: true });
|
|
generate(loadBrand("zoo"), b, { force: true });
|
|
const shell = [
|
|
"src/main.tsx",
|
|
"src/App.tsx",
|
|
"src/Login.tsx",
|
|
"src/vite-env.d.ts",
|
|
"index.html",
|
|
"tsconfig.json",
|
|
"vite.config.ts",
|
|
"public/logo.svg",
|
|
];
|
|
for (const f of shell) {
|
|
const fa = readFileSync(join(a, "apps", "web", f), "utf8");
|
|
const fb = readFileSync(join(b, "apps", "web", f), "utf8");
|
|
assert.equal(fa, fb, `${f} differs between brands — shell must be brand-neutral`);
|
|
}
|
|
// brand.json MUST differ (that's the whole point).
|
|
const ba = readFileSync(join(a, "apps", "web", "public", "brand.json"), "utf8");
|
|
const bb = readFileSync(join(b, "apps", "web", "public", "brand.json"), "utf8");
|
|
assert.notEqual(ba, bb, "brand.json must differ per brand");
|
|
} finally {
|
|
rmSync(a, { recursive: true, force: true });
|
|
rmSync(b, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("generate refuses a non-empty dir without force", () => {
|
|
const out = freshDir();
|
|
try {
|
|
generate(loadBrand("lux"), out, { force: true });
|
|
assert.throws(() => generate(loadBrand("lux"), out), /not empty/);
|
|
// force overwrites cleanly.
|
|
assert.doesNotThrow(() => generate(loadBrand("pars"), out, { force: true }));
|
|
const emitted = parseBrand(readFileSync(join(out, "apps", "web", "public", "brand.json"), "utf8"));
|
|
assert.equal(emitted.id, "pars");
|
|
} finally {
|
|
rmSync(out, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("loadBrand rejects an unknown ref", () => {
|
|
assert.throws(() => loadBrand("definitely-not-a-brand"), /not found/);
|
|
});
|