mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
The SDK now ships with @hanzo/gui + @hanzogui/{core,helpers,constants,
themes,animate,animate-presence,button,input} as direct dependencies
(moved out of the optional peerDependencies block). Phase 3 R2 starts
swapping native primitives onto Tamagui-based @hanzo/gui components,
so consumers no longer have to install those packages themselves.
Three plumbing changes make this actually bundle in a consumer:
1. scripts/patch-hanzogui-exports.cjs (vendored from lux/exchange):
the published @hanzo/[email protected] + @hanzogui/*@7.0.0 packages ship
without a populated dist/, so their package.json main/module/exports
point at folders that do not exist. The patch rewrites those entries
to point at the included src/ at install time. Root postinstall hook
runs it deterministically on every pnpm install.
2. app/bridge/vite.config.ts: alias react-native -> react-native-web,
dedupe react/react-dom/react-native-web, set TAMAGUI_TARGET=web and
__DEV__=false. Minimum viable Tamagui wiring (mirrors the wiring
lux/exchange ships in a much larger config).
3. app/bridge/package.json: add react-native-web@^0.19.0 dependency
so the alias above resolves.
Result: pnpm install in a fresh checkout produces a working build with
zero manual setup. `pnpm build` in app/bridge succeeds.
270 lines
9.6 KiB
JavaScript
270 lines
9.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Canonical local workaround for the broken @hanzo/[email protected] + @hanzogui/*@7.0.0
|
|
// publish: every package in those scopes shipped without a populated dist/ and
|
|
// their src/ still references the pre-rename Hanzogui* symbols.
|
|
//
|
|
// What this script does, deterministically, on every install:
|
|
// 1. For each @hanzo/{gui,theming} and @hanzogui/* package whose dist/ is
|
|
// empty, rewrites its package.json main/module/types/exports to point at
|
|
// the existing src/index.{ts,tsx,native.ts,native.tsx} and adds explicit
|
|
// subpath exports for every src/ file. This lets Vite/Rolldown compile
|
|
// the TypeScript sources on the fly.
|
|
// 2. Renames the lingering Hanzogui* symbols (createHanzogui, HanzoguiRoot,
|
|
// HanzoguiProvider, HanzoguiInternalConfig, etc.) in vendored sources to
|
|
// their canonical Gui* equivalents. Also renames the matching .ts/.tsx
|
|
// files (createHanzogui.ts → createGui.ts) so cross-file imports still
|
|
// resolve.
|
|
//
|
|
// REMOVE THIS FILE the moment the @hanzo/gui SDK republishes 7.x with a real
|
|
// dist/ (or with exports that point at src/) — there is exactly one way to
|
|
// build this app and it MUST be that path. Until then, this is the only
|
|
// supported way to make `pnpm install && pnpm exec vite build` succeed.
|
|
//
|
|
// Verified 2026-04-27: dropping this hook + running `pnpm install
|
|
// --ignore-scripts` from a clean checkout produces
|
|
// `Rolldown failed to resolve import "@hanzo/gui"` because the published
|
|
// package's exports map points at a dist/ that does not exist.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const SCOPES = ['@hanzogui', '@hanzo'];
|
|
const PATCH_NAMES = new Set(['gui', 'theming']);
|
|
const ROOT_NM = path.resolve(__dirname, '..', 'node_modules');
|
|
const PNPM_STORE = path.join(ROOT_NM, '.pnpm');
|
|
const ROOTS = [ROOT_NM];
|
|
if (fs.existsSync(PNPM_STORE)) {
|
|
for (const e of fs.readdirSync(PNPM_STORE, { withFileTypes: true })) {
|
|
if (!e.isDirectory()) continue;
|
|
const inner = path.join(PNPM_STORE, e.name, 'node_modules');
|
|
if (fs.existsSync(inner)) ROOTS.push(inner);
|
|
}
|
|
}
|
|
|
|
let patched = 0;
|
|
|
|
function findScopeDirs(start) {
|
|
const found = [];
|
|
if (!fs.existsSync(start)) return found;
|
|
const queue = [start];
|
|
while (queue.length) {
|
|
const dir = queue.shift();
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const e of entries) {
|
|
if (!e.isDirectory()) continue;
|
|
const full = path.join(dir, e.name);
|
|
if (SCOPES.includes(e.name)) {
|
|
found.push(full);
|
|
} else if (e.name === 'node_modules') {
|
|
queue.push(full);
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
function distEmpty(pkgDir) {
|
|
const dist = path.join(pkgDir, 'dist');
|
|
if (!fs.existsSync(dist)) return true;
|
|
let hasFiles = false;
|
|
function walk(d) {
|
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
if (hasFiles) return;
|
|
if (e.isDirectory()) walk(path.join(d, e.name));
|
|
else if (e.isFile()) hasFiles = true;
|
|
}
|
|
}
|
|
try { walk(dist); } catch {}
|
|
return !hasFiles;
|
|
}
|
|
|
|
function pickEntry(pkgDir) {
|
|
const candidates = [
|
|
'src/index.ts',
|
|
'src/index.tsx',
|
|
'src/index.web.ts',
|
|
'src/index.web.tsx',
|
|
];
|
|
for (const c of candidates) {
|
|
if (fs.existsSync(path.join(pkgDir, c))) return './' + c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function pickNativeEntry(pkgDir) {
|
|
const candidates = [
|
|
'src/index.native.ts',
|
|
'src/index.native.tsx',
|
|
];
|
|
for (const c of candidates) {
|
|
if (fs.existsSync(path.join(pkgDir, c))) return './' + c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function patchPackage(pkgDir) {
|
|
const pjPath = path.join(pkgDir, 'package.json');
|
|
if (!fs.existsSync(pjPath)) return false;
|
|
if (!distEmpty(pkgDir)) return false;
|
|
const entry = pickEntry(pkgDir);
|
|
if (!entry) return false;
|
|
const native = pickNativeEntry(pkgDir);
|
|
const types = fs.existsSync(path.join(pkgDir, 'types', 'index.d.ts'))
|
|
? './types/index.d.ts'
|
|
: entry;
|
|
const pj = JSON.parse(fs.readFileSync(pjPath, 'utf8'));
|
|
pj.main = entry;
|
|
pj.module = entry;
|
|
pj.types = types;
|
|
pj.source = entry;
|
|
const exportsMap = {
|
|
'./package.json': './package.json',
|
|
'.': {
|
|
types,
|
|
...(native ? { 'react-native': native } : {}),
|
|
browser: entry,
|
|
module: entry,
|
|
import: entry,
|
|
require: entry,
|
|
default: entry,
|
|
},
|
|
};
|
|
// Generate explicit subpath exports for every file in src/ so consumers
|
|
// that import `@scope/pkg/foo` get the right file. We emit each base name
|
|
// (without extension) plus the extension-suffixed variants.
|
|
const srcDir = path.join(pkgDir, 'src');
|
|
if (fs.existsSync(srcDir)) {
|
|
const seen = new Set();
|
|
function walk(dir, rel) {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, e.name);
|
|
const childRel = rel ? path.join(rel, e.name) : e.name;
|
|
if (e.isDirectory()) { walk(full, childRel); continue; }
|
|
if (!/\.(ts|tsx)$/.test(e.name)) continue;
|
|
if (e.name.endsWith('.d.ts')) continue;
|
|
if (e.name.endsWith('.test.ts') || e.name.endsWith('.test.tsx')) continue;
|
|
const noExt = childRel.replace(/\.(ts|tsx)$/, '');
|
|
if (noExt === 'index' || /(^|\/)index$/.test(noExt)) continue;
|
|
if (/\.native$/.test(noExt)) continue;
|
|
if (/\.web$/.test(noExt)) continue;
|
|
const baseKey = './' + noExt.replace(/\\/g, '/');
|
|
if (seen.has(baseKey)) continue;
|
|
seen.add(baseKey);
|
|
const target = './src/' + childRel.replace(/\\/g, '/');
|
|
const nativeFull = path.join(srcDir, noExt + '.native.tsx');
|
|
const nativeFull2 = path.join(srcDir, noExt + '.native.ts');
|
|
const nativeRel = fs.existsSync(nativeFull)
|
|
? './src/' + noExt + '.native.tsx'
|
|
: fs.existsSync(nativeFull2) ? './src/' + noExt + '.native.ts' : null;
|
|
exportsMap[baseKey] = {
|
|
...(nativeRel ? { 'react-native': nativeRel } : {}),
|
|
browser: target,
|
|
module: target,
|
|
import: target,
|
|
require: target,
|
|
default: target,
|
|
};
|
|
}
|
|
}
|
|
walk(srcDir, '');
|
|
}
|
|
pj.exports = exportsMap;
|
|
fs.writeFileSync(pjPath, JSON.stringify(pj, null, 2) + '\n');
|
|
return true;
|
|
}
|
|
|
|
for (const root of ROOTS) {
|
|
for (const scopeDir of findScopeDirs(root)) {
|
|
const scope = path.basename(scopeDir);
|
|
for (const name of fs.readdirSync(scopeDir)) {
|
|
// For @hanzo scope, only touch known broken packages so we don't
|
|
// clobber unrelated published packages like @hanzo/branding.
|
|
if (scope === '@hanzo' && !PATCH_NAMES.has(name)) continue;
|
|
const pkgDir = path.join(scopeDir, name);
|
|
if (patchPackage(pkgDir)) patched++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Source-level fixups for known stale references in published 7.0.0 sources.
|
|
// `@hanzogui/web` exports `GuiRoot`, but `@hanzogui/portal`'s source still
|
|
// imports the old `HanzoguiRoot` symbol. Rewrite locally so vite can compile.
|
|
let srcFixups = 0;
|
|
// Generic rename map. Old names in published 7.0.0 sources → canonical Gui*.
|
|
// Order matters: longer prefixes first so `Hanzogui*` doesn't shadow rename of
|
|
// inner `Hanzogui*` token.
|
|
const RENAMES = [
|
|
[/\.\/createHanzogui\b/g, './createGui'],
|
|
[/\.\/views\/HanzoguiProvider\b/g, './views/GuiProvider'],
|
|
[/\bcreateHanzogui\b/g, 'createGui'],
|
|
[/\bCreateHanzoguiConfig\b/g, 'CreateGuiConfig'],
|
|
[/\bCreateHanzoguiProps\b/g, 'CreateGuiProps'],
|
|
[/\bHanzoguiProviderProps\b/g, 'GuiProviderProps'],
|
|
[/\bHanzoguiInternalConfig\b/g, 'GuiInternalConfig'],
|
|
[/\bHanzoguiProvider\b/g, 'GuiProvider'],
|
|
[/\bHanzoguiRoot\b/g, 'GuiRoot'],
|
|
[/\bisHanzoguiComponent\b/g, 'isGuiComponent'],
|
|
[/\bisHanzoguiElement\b/g, 'isGuiElement'],
|
|
[/\bHanzoguiComponent\b/g, 'GuiComponent'],
|
|
[/\bHanzoguiElement\b/g, 'GuiElement'],
|
|
[/\bHanzoguiInstance\b/g, 'GuiInstance'],
|
|
[/\bHanzoguiInternalConfig\b/g, 'GuiInternalConfig'],
|
|
];
|
|
|
|
function fixupSource(file) {
|
|
if (!fs.existsSync(file)) return;
|
|
const before = fs.readFileSync(file, 'utf8');
|
|
let after = before;
|
|
for (const [re, repl] of RENAMES) after = after.replace(re, repl);
|
|
if (after !== before) {
|
|
fs.writeFileSync(file, after);
|
|
srcFixups++;
|
|
}
|
|
}
|
|
|
|
function renameFile(from, to) {
|
|
if (fs.existsSync(from) && !fs.existsSync(to)) {
|
|
fs.renameSync(from, to);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function fixupTree(pkgDir) {
|
|
const srcDir = path.join(pkgDir, 'src');
|
|
if (!fs.existsSync(srcDir)) return;
|
|
// Rename files using old names so import paths still resolve.
|
|
renameFile(path.join(srcDir, 'createHanzogui.ts'), path.join(srcDir, 'createGui.ts'));
|
|
renameFile(path.join(srcDir, 'createHanzogui.tsx'), path.join(srcDir, 'createGui.tsx'));
|
|
const viewsDir = path.join(srcDir, 'views');
|
|
if (fs.existsSync(viewsDir)) {
|
|
renameFile(path.join(viewsDir, 'HanzoguiProvider.tsx'), path.join(viewsDir, 'GuiProvider.tsx'));
|
|
renameFile(path.join(viewsDir, 'HanzoguiProvider.native.tsx'), path.join(viewsDir, 'GuiProvider.native.tsx'));
|
|
}
|
|
function walk(d) {
|
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
const full = path.join(d, e.name);
|
|
if (e.isDirectory()) walk(full);
|
|
else if (/\.(ts|tsx|mjs|cjs|js)$/.test(e.name)) fixupSource(full);
|
|
}
|
|
}
|
|
walk(srcDir);
|
|
}
|
|
|
|
for (const root of ROOTS) {
|
|
for (const scopeDir of findScopeDirs(root)) {
|
|
for (const name of fs.readdirSync(scopeDir)) {
|
|
const pkgDir = path.join(scopeDir, name);
|
|
const stat = (() => { try { return fs.statSync(pkgDir); } catch { return null; } })();
|
|
if (!stat || !stat.isDirectory()) continue;
|
|
fixupTree(pkgDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`patch-hanzogui-exports: patched ${patched} package(s); fixed ${srcFixups} source file(s)`);
|