Initial commit: bface library, build fixes, and refreshed docs

- Externalize all @tamagui/* and tamagui subpaths so dist no longer vendors Tamagui.
- Emit TypeScript declarations with vite-plugin-dts; fix package exports types for ui/*.
- Align initEnv with profiles: displayName, brandLogo, api.baseURL, themeColor, uiShell.
- Stabilize tests with Node localStorage file; env tests pass.
- Update README and component docs for services, menus, API client, and development.
This commit is contained in:
Amer Agovic
2026-04-18 10:43:52 -05:00
commit 94a9f32969
87 changed files with 19750 additions and 0 deletions
+487
View File
@@ -0,0 +1,487 @@
/**
* Environment Configuration & Utilities
* Central config dictionary and environment discovery
*/
import { getProvider } from './storage.js';
// Private storage instance for config
const storage = getProvider('kv', 'config');
// Config dictionary - populated by loader
let config = {};
let consoleLoggerInstalled = false;
const lockedKeys = new Set();
const LOGGER_ICONS = {
debug: '.',
info: 'i',
log: '>',
warn: '!',
error: 'x'
};
const NATIVE_CONSOLE = {
log: console.log.bind(console),
info: (console.info || console.log).bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
debug: (console.debug || console.log).bind(console)
};
const perfNow = () => {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now();
}
return Date.now();
};
function normalizeLogLevel(level = 'log') {
if (level === 'info') return 'info';
if (level === 'warn') return 'warn';
if (level === 'error') return 'error';
if (level === 'debug') return 'debug';
return 'log';
}
function splitSourceAndMessage(args = []) {
if (args.length === 0) {
return {
source: 'App',
messageParts: []
};
}
const [firstArg, ...restArgs] = args;
if (typeof firstArg !== 'string') {
return {
source: 'App',
messageParts: args
};
}
const match = firstArg.match(/^\[([^\]]+)\]\s*(.*)$/s);
if (!match) {
return {
source: 'App',
messageParts: args
};
}
const [, source, firstMessage] = match;
const messageParts = firstMessage ? [firstMessage, ...restArgs] : restArgs;
return {
source,
messageParts
};
}
// Special config keys
export const CONFIG_KEYS = {
APP_NAME: 'APP_NAME',
APP_DISPLAY_NAME: 'APP_DISPLAY_NAME',
APP_DESCRIPTION: 'APP_DESCRIPTION',
BRAND_LOGO: 'BRAND_LOGO',
THEME_COLOR: 'THEME_COLOR',
UI_SHELL: 'UI_SHELL',
STORAGE_BACKEND: 'STORAGE_BACKEND',
API_BASE_URL: 'API_BASE_URL',
MODULES: 'MODULES',
SECURITY_CONFIG: 'SECURITY_CONFIG',
LOCALE: 'LOCALE',
/** Development host: extra dev UI, SW dev behavior, etc. Layered via getConfig (storage → profile → bundler). */
DEV_HOST: 'DEV_HOST'
};
// do not allow edits on these keys via setConfig - they are meant to be set from profile or env vars and not overridden at runtime
[
CONFIG_KEYS.API_BASE_URL,
CONFIG_KEYS.MODULES,
CONFIG_KEYS.SECURITY_CONFIG
].forEach((key) => lockedKeys.add(key));
/**
* Resolve dev-host flag from profile (explicit) or bundler hints.
* Profile wins: `dev_host: true|false` or `runtime.dev: true|false`.
* Otherwise Vite `import.meta.env.DEV`, else false.
* @param {Object|null|undefined} appConfig
* @returns {boolean}
*/
function resolveDevHostFlag(appConfig) {
if (appConfig && typeof appConfig.dev_host === 'boolean') {
return appConfig.dev_host;
}
if (appConfig?.runtime && typeof appConfig.runtime.dev === 'boolean') {
return appConfig.runtime.dev;
}
if (typeof import.meta !== 'undefined' && import.meta.env) {
return Boolean(import.meta.env.DEV);
}
return false;
}
/**
* Initialize environment config
* @param {Object} appConfig - Configuration from profile
*/
export function initEnv(appConfig) {
const api = appConfig.api || {};
config = {
APP_NAME: appConfig.id || appConfig.name,
APP_DISPLAY_NAME: appConfig.displayName || appConfig.short_name || appConfig.name,
APP_DESCRIPTION: appConfig.description || '',
BRAND_LOGO: appConfig.brand_logo || appConfig.brandLogo || appConfig.icons?.[0]?.src || '/favicon.svg',
THEME_COLOR: appConfig.theme_color || appConfig.themeColor || '#000000',
UI_SHELL: appConfig.ui_shell || appConfig.uiShell || 'EmptyShell',
STORAGE_BACKEND: appConfig.storage?.backend || 'localStorage',
API_BASE_URL: api.base_url || api.baseURL || '/api',
MODULES: appConfig.modules || [],
SECURITY_CONFIG: appConfig.security || {},
LOCALE: appConfig.locale || null,
[CONFIG_KEYS.DEV_HOST]: resolveDevHostFlag(appConfig),
// Store full profile for advanced access
_profile: appConfig
};
}
function resolveSystemLocale() {
if (typeof navigator !== 'undefined' && typeof navigator.language === 'string' && navigator.language.trim()) {
return navigator.language.trim();
}
return 'en-US';
}
/**
* Bootstrap environment as early as possible from the selected profile.
* This gives the app a single source of truth before full boot runs.
* @param {Object} appConfig
* @param {Object} options
* @param {boolean} [options.installLoggerFirst=true]
*/
export function bootstrapEnv(appConfig, options = {}) {
const { installLoggerFirst = true } = options;
if (installLoggerFirst) {
installConsoleLogger();
}
if (appConfig && typeof appConfig === 'object') {
initEnv(appConfig);
}
return getConfigDict();
}
export function log(source = 'App', level = 'log', ...messageParts) {
const normalizedLevel = normalizeLogLevel(level);
const sink = NATIVE_CONSOLE[normalizedLevel] || NATIVE_CONSOLE.log;
const timestamp = new Date().toISOString();
const icon = LOGGER_ICONS[normalizedLevel] || LOGGER_ICONS.log;
sink(`${timestamp} [${source}] ${icon}`, ...messageParts);
}
export function createLogger(source = 'App') {
return {
debug: (...messageParts) => log(source, 'debug', ...messageParts),
info: (...messageParts) => log(source, 'info', ...messageParts),
log: (...messageParts) => log(source, 'log', ...messageParts),
warn: (...messageParts) => log(source, 'warn', ...messageParts),
error: (...messageParts) => log(source, 'error', ...messageParts)
};
}
export function startTrace(source = 'App', label = 'operation', metadata = null) {
const startedAt = perfNow();
if (metadata !== null && metadata !== undefined) {
log(source, 'debug', `${label} started`, metadata);
} else {
log(source, 'debug', `${label} started`);
}
return {
end(extra = null, level = 'log') {
const durationMs = Math.round((perfNow() - startedAt) * 100) / 100;
if (extra !== null && extra !== undefined) {
log(source, level, `${label} finished in ${durationMs}ms`, extra);
} else {
log(source, level, `${label} finished in ${durationMs}ms`);
}
return durationMs;
},
fail(error = null) {
const durationMs = Math.round((perfNow() - startedAt) * 100) / 100;
log(source, 'error', `${label} failed in ${durationMs}ms`, error);
return durationMs;
}
};
}
export async function traceAsync(source, label, fn, metadata = null) {
const trace = startTrace(source, label, metadata);
try {
const result = await fn();
trace.end();
return result;
} catch (error) {
trace.fail(error);
throw error;
}
}
export function installConsoleLogger() {
if (consoleLoggerInstalled || typeof console === 'undefined') {
return;
}
consoleLoggerInstalled = true;
console.log = (...args) => {
const { source, messageParts } = splitSourceAndMessage(args);
log(source, 'log', ...messageParts);
};
console.info = (...args) => {
const { source, messageParts } = splitSourceAndMessage(args);
log(source, 'info', ...messageParts);
};
console.warn = (...args) => {
const { source, messageParts } = splitSourceAndMessage(args);
log(source, 'warn', ...messageParts);
};
console.error = (...args) => {
const { source, messageParts } = splitSourceAndMessage(args);
log(source, 'error', ...messageParts);
};
console.debug = (...args) => {
const { source, messageParts } = splitSourceAndMessage(args);
log(source, 'debug', ...messageParts);
};
}
export function lockConfigKey(key) {
if (!key) {
return false;
}
lockedKeys.add(key);
return true;
}
export function unlockConfigKey(key) {
if (!key) {
return false;
}
return lockedKeys.delete(key);
}
export function isConfigKeyLocked(key) {
return lockedKeys.has(key);
}
export function getLockedConfigKeys() {
return Array.from(lockedKeys);
}
function setMetaTag(name, content) {
if (typeof document === 'undefined') {
return;
}
let element = document.querySelector(`meta[name="${name}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute('name', name);
document.head.appendChild(element);
}
element.setAttribute('content', content);
}
function setFavicon(href) {
if (typeof document === 'undefined' || !href) {
return;
}
let element = document.querySelector('link[rel="icon"]');
if (!element) {
element = document.createElement('link');
element.setAttribute('rel', 'icon');
document.head.appendChild(element);
}
element.setAttribute('href', href);
if (!element.getAttribute('type')) {
element.setAttribute('type', 'image/svg+xml');
}
}
export async function syncDocumentHeadFromConfig(options = {}) {
if (typeof document === 'undefined') {
return null;
}
const {
titleFallback = 'PWA Template',
descriptionFallback = '',
themeColorFallback = '#000000'
} = options;
const [title, description, themeColor, brandLogo] = await Promise.all([
getConfig(CONFIG_KEYS.APP_DISPLAY_NAME, titleFallback),
getConfig(CONFIG_KEYS.APP_DESCRIPTION, descriptionFallback),
getConfig(CONFIG_KEYS.THEME_COLOR, themeColorFallback),
getConfig(CONFIG_KEYS.BRAND_LOGO, '/favicon.svg')
]);
document.title = title || titleFallback;
setMetaTag('description', description || descriptionFallback);
setMetaTag('theme-color', themeColor || themeColorFallback);
setFavicon(brandLogo || '/favicon.svg');
return {
title: title || titleFallback,
description: description || descriptionFallback,
themeColor: themeColor || themeColorFallback,
brandLogo: brandLogo || '/favicon.svg'
};
}
/**
* Get config value by key
* Checks in order: storage (if key exists) → config dictionary → environment variables → altValue
* Uses storage.hasKey() as a guard to skip unnecessary storage.get() calls for profile config keys
* @param {string} key - Config key
* @param {any} altValue - Alternative value if not found
* @returns {Promise<any>} Config value or altValue
*/
export async function getConfig(key, altValue = null) {
// 1. Check if key exists in storage first (quick check to avoid unnecessary get)
try {
const exists = await storage.hasKey(key);
if (exists) {
// Only do full storage.get() if key exists
const storedValue = await storage.get(key, null);
if (storedValue !== null && storedValue !== undefined) {
return storedValue;
}
}
// If key doesn't exist in storage, skip to config dictionary (faster path)
} catch (error) {
// Storage might not be available, continue to next check
console.warn(`[Env] Failed to check storage for "${key}":`, error);
}
// 2. Check config dictionary (in-memory config from profile)
if (config.hasOwnProperty(key)) {
return config[key];
}
// 3. Fall back to environment variables (Vite env vars)
// Check if import.meta.env exists (available in Vite, not in Node)
if (typeof import.meta !== 'undefined' && import.meta.env) {
const envKey = `VITE_${key}`;
const envValue = import.meta.env[envKey];
if (envValue !== undefined) {
return envValue;
}
}
// 4. Return alternative value
return altValue;
}
/**
* Set config value
* If key exists in config dictionary, update it there (in-memory).
* Otherwise, save to storage (for persistence).
* @param {string} key - Config key
* @param {any} value - Value to set
* @returns {Promise<void>}
*/
export async function setConfig(key, value) {
if (isConfigKeyLocked(key)) {
console.warn(`[Env] Refusing to set locked config key "${key}". Unlock it explicitly first if this override is intentional.`);
return false;
}
// If key exists in config dictionary, update it there (in-memory config)
if (config.hasOwnProperty(key)) {
config[key] = value;
return true;
} else {
// Otherwise, save to storage (for state/settings persistence)
try {
await storage.set(key, value);
return true;
} catch (error) {
console.warn(`[Env] Failed to set "${key}" in storage:`, error);
// Fallback: store in config dictionary if storage fails
config[key] = value;
return true;
}
}
}
/**
* Get full config object (read-only copy)
* @returns {Object} Config dictionary
*/
export function getConfigDict() {
return { ...config };
}
export async function getLocale(altValue = null) {
const savedLocale = await getConfig(CONFIG_KEYS.LOCALE, null);
if (savedLocale && typeof savedLocale === 'string') {
config[CONFIG_KEYS.LOCALE] = savedLocale;
return savedLocale;
}
return altValue || resolveSystemLocale();
}
export function getLocaleSync(altValue = null) {
return config[CONFIG_KEYS.LOCALE] || altValue || resolveSystemLocale();
}
export async function setLocale(locale) {
const normalizedLocale = typeof locale === 'string' && locale.trim() ? locale.trim() : null;
config[CONFIG_KEYS.LOCALE] = normalizedLocale;
try {
if (normalizedLocale) {
await storage.set(CONFIG_KEYS.LOCALE, normalizedLocale);
} else {
await storage.remove(CONFIG_KEYS.LOCALE);
}
} catch (error) {
console.warn('[Env] Failed to persist locale:', error);
}
return normalizedLocale || resolveSystemLocale();
}
/**
* Whether the app is treated as a development host (dev tooling, SW policy, etc.).
* Synchronous: uses in-memory config after {@link initEnv} / {@link bootstrapEnv}.
* For persisted overrides, use {@link getConfig} with {@link CONFIG_KEYS.DEV_HOST} in async code.
*/
export function isDevelopment() {
if (config && Object.prototype.hasOwnProperty.call(config, CONFIG_KEYS.DEV_HOST)) {
return Boolean(config[CONFIG_KEYS.DEV_HOST]);
}
if (typeof import.meta !== 'undefined' && import.meta.env) {
return Boolean(import.meta.env.DEV);
}
return false;
}
/**
* Inverse of {@link isDevelopment} when no explicit production flag exists.
*/
export function isProduction() {
if (typeof import.meta !== 'undefined' && import.meta.env && typeof import.meta.env.PROD === 'boolean') {
return import.meta.env.PROD;
}
return !isDevelopment();
}