[AI] Fix 'process is not defined' by separating client and server code

Root cause: customFunctions.ts was imported by client-side code but
had server dependencies, causing db module (and transitive dependencies
like adm-zip) to be bundled into browser.

Solution: Split into two files to properly separate concerns:
1. customFunctions.ts - Plugin only (no server imports, safe for browser)
2. customFunctionsPreferences.ts - Server-only preferences loading (uses aqlQuery)

Changes:
- Created customFunctionsPreferences.ts with loadUserPreferencesForFormulas()
- Uses aqlQuery (Actual's custom SQL) instead of direct db imports
- Moved helper functions (getLocaleDefaults, getCurrencyFromLocale) to new file
- customFunctions.ts now only has setCachedUserPreferences() setter
- action.ts loads prefs and caches them at module init
- Client code can still import CustomFunctionsPlugin safely

Co-authored-by: lelemm <lelemm@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-11 02:32:20 +00:00
parent 579aba3a80
commit 05876367ba
3 changed files with 155 additions and 141 deletions

View File

@@ -13,8 +13,9 @@ import { amountToInteger } from '#shared/util';
import {
CustomFunctionsPlugin,
customFunctionsTranslations,
loadUserPreferencesForFormulas,
setCachedUserPreferences,
} from './customFunctions';
import { loadUserPreferencesForFormulas } from './customFunctionsPreferences';
import { assert } from './rule-utils';
HyperFormula.registerLanguage('enUS', enUS);
@@ -25,7 +26,9 @@ HyperFormula.registerFunctionPlugin(
// Load user preferences on module initialization for formatting functions
// This is async but happens once when the module loads
void loadUserPreferencesForFormulas();
void loadUserPreferencesForFormulas().then(prefs => {
setCachedUserPreferences(prefs);
});
const ACTION_OPS = [
'set',

View File

@@ -3,157 +3,27 @@ import type { InterpreterState } from 'hyperformula/typings/interpreter/Interpre
import type { ProcedureAst } from 'hyperformula/typings/parser';
import { getCurrency } from '#shared/currencies';
import type { Currency } from '#shared/currencies';
import { getNumberFormat, integerToAmount } from '#shared/util';
import type { NumberFormats } from '#shared/util';
import { integerToAmount } from '#shared/util';
import type { UserPreferences } from './customFunctionsPreferences';
// User feedback: Make formatting functions respect app settings with locale-based fallbacks
// Global state to store user preferences for formatting functions
// This is set before formula execution to avoid async issues in HyperFormula custom functions
let cachedUserPreferences: {
currency: Currency;
numberFormat: NumberFormats;
thousandsSeparator: string;
decimalSeparator: string;
locale: string;
} | null = null;
let cachedUserPreferences: UserPreferences | null = null;
// Helper to get locale-based number format defaults
function getLocaleDefaults(locale?: string): {
thousandsSeparator: string;
decimalSeparator: string;
} {
// Default to en-US if no locale
const actualLocale = locale || 'en-US';
// Map common locales to their number formats
if (
actualLocale.startsWith('de') ||
actualLocale.startsWith('es') ||
actualLocale.startsWith('it')
) {
// German, Spanish, Italian: 1.000,00
return { thousandsSeparator: '.', decimalSeparator: ',' };
} else if (
actualLocale.startsWith('fr') ||
actualLocale.startsWith('ru') ||
actualLocale.startsWith('cs')
) {
// French, Russian, Czech: 1 000,00
return { thousandsSeparator: '\u202F', decimalSeparator: ',' };
} else if (actualLocale.startsWith('de-CH')) {
// Swiss German: 1'000.00
return { thousandsSeparator: '\u2019', decimalSeparator: '.' };
} else if (
actualLocale.startsWith('en-IN') ||
actualLocale.startsWith('hi')
) {
// Indian: 1,00,000.00 (but we'll use standard comma for simplicity)
return { thousandsSeparator: ',', decimalSeparator: '.' };
} else {
// Default (en-US, en-GB, etc.): 1,000.00
return { thousandsSeparator: ',', decimalSeparator: '.' };
}
}
// Helper to determine currency from locale
function getCurrencyFromLocale(locale: string): Currency {
if (locale.startsWith('en-GB')) {
return getCurrency('GBP');
} else if (
locale.startsWith('de') ||
locale.startsWith('fr') ||
locale.startsWith('es') ||
locale.startsWith('it') ||
locale.startsWith('nl')
) {
return getCurrency('EUR');
} else if (locale.startsWith('ja')) {
return getCurrency('JPY');
} else if (locale.startsWith('en-IN') || locale.startsWith('hi')) {
return getCurrency('INR');
} else if (locale.startsWith('en-CA')) {
return getCurrency('CAD');
} else if (locale.startsWith('en-AU')) {
return getCurrency('AUD');
} else {
return getCurrency('USD');
}
}
// Function to load and cache user preferences
// This should be called before formula execution (can be async)
export async function loadUserPreferencesForFormulas(): Promise<void> {
try {
// Dynamically import db only when needed (server-side only)
// This prevents bundling server code into the browser
const db = await import('#server/db');
// Get currency code from preferences
const currencyCodePref = await db.first<Pick<db.DbPreference, 'value'>>(
'SELECT value FROM preferences WHERE id = ?',
['defaultCurrencyCode'],
);
const currencyCode = currencyCodePref?.value || null;
// Get number format from preferences
const numberFormatPref = await db.first<Pick<db.DbPreference, 'value'>>(
'SELECT value FROM preferences WHERE id = ?',
['numberFormat'],
);
const numberFormatValue =
(numberFormatPref?.value as NumberFormats) || null;
// Get locale from preferences
const localePref = await db.first<Pick<db.DbPreference, 'value'>>(
'SELECT value FROM preferences WHERE id = ?',
['locale'],
);
const locale = localePref?.value || 'en-US';
// Determine currency
const currency = currencyCode
? getCurrency(currencyCode)
: getCurrencyFromLocale(locale);
// Get number format settings
const numberFormatSettings = getNumberFormat({
format: numberFormatValue || undefined,
});
// Get locale-based defaults as fallback
const localeDefaults = getLocaleDefaults(locale);
cachedUserPreferences = {
currency,
numberFormat: numberFormatValue || 'comma-dot',
thousandsSeparator:
numberFormatSettings.thousandsSeparator ||
localeDefaults.thousandsSeparator,
decimalSeparator:
numberFormatSettings.decimalSeparator ||
localeDefaults.decimalSeparator,
locale,
};
} catch {
// Fallback to defaults if preferences can't be loaded
cachedUserPreferences = {
currency: getCurrency('USD'),
numberFormat: 'comma-dot',
thousandsSeparator: ',',
decimalSeparator: '.',
locale: 'en-US',
};
}
// Setter for cached preferences (called from server-side code only)
export function setCachedUserPreferences(prefs: UserPreferences): void {
cachedUserPreferences = prefs;
}
// Synchronous getter for cached preferences (used by custom functions)
function getUserPreferences() {
function getUserPreferences(): UserPreferences {
if (!cachedUserPreferences) {
// If not loaded, use defaults
return {
currency: getCurrency('USD'),
numberFormat: 'comma-dot' as NumberFormats,
numberFormat: 'comma-dot',
thousandsSeparator: ',',
decimalSeparator: '.',
locale: 'en-US',

View File

@@ -0,0 +1,141 @@
// Server-only file for loading user preferences for custom functions
// This file should NEVER be imported by client code
import { aqlQuery } from '#server/aql';
import { getCurrency } from '#shared/currencies';
import type { Currency } from '#shared/currencies';
import { q } from '#shared/query';
import { getNumberFormat } from '#shared/util';
import type { NumberFormats } from '#shared/util';
// Helper to get locale-based number format defaults
function getLocaleDefaults(locale?: string): {
thousandsSeparator: string;
decimalSeparator: string;
} {
// Default to en-US if no locale
const actualLocale = locale || 'en-US';
// Map common locales to their number formats
if (
actualLocale.startsWith('de') ||
actualLocale.startsWith('es') ||
actualLocale.startsWith('it')
) {
// German, Spanish, Italian: 1.000,00
return { thousandsSeparator: '.', decimalSeparator: ',' };
} else if (
actualLocale.startsWith('fr') ||
actualLocale.startsWith('ru') ||
actualLocale.startsWith('cs')
) {
// French, Russian, Czech: 1 000,00
return { thousandsSeparator: '\u202F', decimalSeparator: ',' };
} else if (actualLocale.startsWith('de-CH')) {
// Swiss German: 1'000.00
return { thousandsSeparator: '\u2019', decimalSeparator: '.' };
} else if (
actualLocale.startsWith('en-IN') ||
actualLocale.startsWith('hi')
) {
// Indian: 1,00,000.00 (but we'll use standard comma for simplicity)
return { thousandsSeparator: ',', decimalSeparator: '.' };
} else {
// Default (en-US, en-GB, etc.): 1,000.00
return { thousandsSeparator: ',', decimalSeparator: '.' };
}
}
// Helper to determine currency from locale
function getCurrencyFromLocale(locale: string): Currency {
if (locale.startsWith('en-GB')) {
return getCurrency('GBP');
} else if (
locale.startsWith('de') ||
locale.startsWith('fr') ||
locale.startsWith('es') ||
locale.startsWith('it') ||
locale.startsWith('nl')
) {
return getCurrency('EUR');
} else if (locale.startsWith('ja')) {
return getCurrency('JPY');
} else if (locale.startsWith('en-IN') || locale.startsWith('hi')) {
return getCurrency('INR');
} else if (locale.startsWith('en-CA')) {
return getCurrency('CAD');
} else if (locale.startsWith('en-AU')) {
return getCurrency('AUD');
} else {
return getCurrency('USD');
}
}
export type UserPreferences = {
currency: Currency;
numberFormat: NumberFormats;
thousandsSeparator: string;
decimalSeparator: string;
locale: string;
};
// Function to load user preferences from the database
// This should be called before formula execution (server-side only)
export async function loadUserPreferencesForFormulas(): Promise<UserPreferences> {
try {
// Use aqlQuery to fetch preferences (Actual's custom SQL language)
const currencyPref = await aqlQuery(
q('preferences').filter({ id: 'defaultCurrencyCode' }).select('*'),
);
const currencyCode =
currencyPref.data.length > 0 ? currencyPref.data[0].value : null;
const numberFormatPref = await aqlQuery(
q('preferences').filter({ id: 'numberFormat' }).select('*'),
);
const numberFormatValue =
numberFormatPref.data.length > 0
? (numberFormatPref.data[0].value as NumberFormats)
: null;
const localePref = await aqlQuery(
q('preferences').filter({ id: 'locale' }).select('*'),
);
const locale =
localePref.data.length > 0 ? localePref.data[0].value : 'en-US';
// Determine currency
const currency = currencyCode
? getCurrency(currencyCode)
: getCurrencyFromLocale(locale);
// Get number format settings
const numberFormatSettings = getNumberFormat({
format: numberFormatValue || undefined,
});
// Get locale-based defaults as fallback
const localeDefaults = getLocaleDefaults(locale);
return {
currency,
numberFormat: numberFormatValue || 'comma-dot',
thousandsSeparator:
numberFormatSettings.thousandsSeparator ||
localeDefaults.thousandsSeparator,
decimalSeparator:
numberFormatSettings.decimalSeparator ||
localeDefaults.decimalSeparator,
locale,
};
} catch {
// Fallback to defaults if preferences can't be loaded
return {
currency: getCurrency('USD'),
numberFormat: 'comma-dot',
thousandsSeparator: ',',
decimalSeparator: '.',
locale: 'en-US',
};
}
}