perf(bundle): drop the BAS chart and the Node crypto polyfill from the shared client baseline (#1942)
* perf(bundle): drop the BAS chart and the Node crypto polyfill from the shared client baseline
Two chunks rode along in the first-load JS of almost every dashboard route:
the full BAS 2026 chart (315 KB uncompressed, in 81 route manifests) and
the browser polyfill for Node's crypto/vm/Buffer (327 KB, in 26 routes
incl. login and register). Neither was needed on first paint; both got
there through static imports of helpers that happen to live next to code
that needs the data or the builtin.
Node polyfill (4 pure splits, behaviour unchanged, re-exported from the
original modules for server callers):
- lib/auth/bankid-flags.ts: isBankIdEnabled (login, register, security
settings imported it from bankid.ts, which imports crypto).
- lib/import/bank-file/formats.ts: the format registry + detection (the
import history imported getFormat from parser.ts, which hashes).
- lib/salary/personnummer-format.ts: parsing/validation/formatting (the
employee forms reached the encrypting personnummer.ts via tax-column).
- lib/auth/api-key-scopes.ts: scope catalogue, groups, tool map, helpers
(the API key panel imported STAGING_SCOPES from the key generator).
BAS chart:
- lib/bookkeeping/bas-lazy.ts + use-bas-reference.ts: the chart becomes a
dynamic import, fetched once per session after first paint; components
that show BAS names/descriptions call useBasReference() and re-render
when it lands. Until then (and on the server) only the hardcoded
account-descriptions answer, so SSR and hydration agree.
- lib/bookkeeping/bas-labels.ts: class/group labels out of bas-reference.ts
(account-descriptions needed a label and paid for the whole chart).
- lib/bookkeeping/bas-account-numbers.ts (generated, ~11 KB) +
scripts/generate-bas-account-numbers.ts (--check) + parity test:
isStandardBASAccountNumber for AddAccountDialog/ChartOfAccountsManager.
- lib/bookkeeping/account-classifier-{heuristic,client}.ts: the BAS-aligned
heuristic shared by the server classifier and a client variant that uses
the lazy chart.
- lib/bookkeeping/invoice-accounts.ts: INVOICE_FX_RATE_MISSING,
InvoiceFxRateMissingError, getRevenueAccount, getOutputVatAccount out of
invoice-entries.ts, whose engine import pulled account-backfill and the
chart into SendInvoiceDialog/PaymentBookingDialog.
- CorrectOpeningBalanceDialog re-seeds names when the chart lands;
OpeningBalanceRowEditor builds its Fuse indexes lazily; the
ChartOfAccountsManager BAS-katalog tab awaits the chunk.
Tooling:
- scripts/perf/client-import-closure.mjs: static import closure of every
'use client' module with the shortest chain to a target (file or bare
specifier); found every path above without a build.
- scripts/checks/client-node-builtin.mjs wired into check:guards: a client
module reaching a Node builtin is a hard failure (0 today).
Left as is: invoices/[id], its credit page and SendInvoiceDialog still
reach the chart through lib/invoices/issue-credit-note -> invoice-entries
-> engine -> account-backfill; splitting the engine is out of scope here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(perf): unambiguous import-edge regex in the closure walker (CodeQL js/redos)
One quantifier per span: a greedy [^'"]* up to the specifier quote, which it
cannot cross, so a run of whitespace has a single parse. Same edges as
before (multi-line named imports, re-exports, side-effect imports; type-only
imports still skipped).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
e7e4efbfbc
commit
1a41119682
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { motion, AnimatePresence, useReducedMotion, animate } from 'framer-motion'
|
||||
import { RotateCw } from 'lucide-react'
|
||||
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { DeepEntity, DeepLedgerContext } from '@/lib/agent-context/ledger-deep'
|
||||
@@ -240,6 +241,9 @@ const PULSE_DUR: Record<Cadence, number> = { weekly: 1.15, monthly: 2.7, irregul
|
||||
|
||||
export function LedgerGraph({ deep, companyName }: { deep: DeepLedgerContext; companyName: string }) {
|
||||
const t = useTranslations('agentKnowledge')
|
||||
// Loads the BAS chart chunk after mount and re-renders once names and
|
||||
// descriptions for non-hardcoded accounts are available.
|
||||
useBasReference()
|
||||
const reduce = useReducedMotion() ?? false
|
||||
const model = useMemo(() => buildModel(deep), [deep])
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Loader2, AlertTriangle } from 'lucide-react'
|
||||
import { isStandardBASAccount } from '@/lib/bookkeeping/bas-reference'
|
||||
import { classifyAccount } from '@/lib/bookkeeping/account-classifier'
|
||||
import { isStandardBASAccountNumber } from '@/lib/bookkeeping/bas-account-numbers'
|
||||
import { classifyAccountClient as classifyAccount } from '@/lib/bookkeeping/account-classifier-client'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import type { BASAccount } from '@/types'
|
||||
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { AccountVatTreatmentSelect } from './AccountVatTreatmentSelect'
|
||||
@@ -49,6 +50,9 @@ export function AddAccountDialog({
|
||||
initialAccountNumber,
|
||||
initialAccountName,
|
||||
}: AddAccountDialogProps) {
|
||||
// Loads the BAS chart chunk after mount so classification and the
|
||||
// standard-account check get the authoritative answer once it lands.
|
||||
useBasReference()
|
||||
const [accountNumber, setAccountNumber] = useState('')
|
||||
const [accountName, setAccountName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
@@ -82,7 +86,7 @@ export function AddAccountDialog({
|
||||
}
|
||||
}, [open, initialAccountNumber, initialAccountName])
|
||||
|
||||
const isBASMatch = accountNumber.length === 4 && isStandardBASAccount(accountNumber)
|
||||
const isBASMatch = accountNumber.length === 4 && isStandardBASAccountNumber(accountNumber)
|
||||
const derived = accountNumber.length === 4 ? classifyAccount(accountNumber) : null
|
||||
|
||||
async function handleCreate() {
|
||||
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { BASAccount } from '@/types'
|
||||
import { useCompanySettings } from '@/lib/reference-data/hooks'
|
||||
import { BAS_REFERENCE, isStandardBASAccount, type BASReferenceAccount } from '@/lib/bookkeeping/bas-reference'
|
||||
import type { BASReferenceAccount } from '@/lib/bookkeeping/bas-reference'
|
||||
import { isStandardBASAccountNumber } from '@/lib/bookkeeping/bas-account-numbers'
|
||||
import { ensureBasLoaded } from '@/lib/bookkeeping/bas-lazy'
|
||||
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -134,6 +136,9 @@ export default function ChartOfAccountsManager() {
|
||||
(a: { account_number: string; is_active: boolean; is_system_account: boolean }) => [a.account_number, a],
|
||||
),
|
||||
)
|
||||
// The full chart is a lazily loaded chunk: only the BAS-katalog tab pays
|
||||
// for it, not every route that renders this component's parent.
|
||||
const BAS_REFERENCE = await ensureBasLoaded()
|
||||
const merged: ReferenceAccount[] = BAS_REFERENCE.map((ref) => {
|
||||
const userAccount = userMap.get(ref.account_number)
|
||||
return {
|
||||
@@ -561,7 +566,7 @@ export default function ChartOfAccountsManager() {
|
||||
{t('system_badge')}
|
||||
</span>
|
||||
)}
|
||||
{!isStandardBASAccount(account.account_number) && (
|
||||
{!isStandardBASAccountNumber(account.account_number) && (
|
||||
<span className="shrink-0 text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
{t('own_badge')}
|
||||
</span>
|
||||
|
||||
@@ -14,7 +14,8 @@ import { AlertTriangle } from 'lucide-react'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
|
||||
import { getBasLoadedByNumber } from '@/lib/bookkeeping/bas-lazy'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import OpeningBalanceRowEditor, {
|
||||
type EditableRow,
|
||||
type OpeningBalanceEditorState,
|
||||
@@ -33,14 +34,15 @@ let seedIdCounter = 0
|
||||
|
||||
// Map the booked IB's lines into editable rows. account_name isn't stored on
|
||||
// the line, so resolve it from BAS for display (cosmetic: only account_number
|
||||
// + amounts are sent on save).
|
||||
// + amounts are sent on save). The chart is a lazily loaded chunk: the
|
||||
// caller re-seeds once it has arrived.
|
||||
function seedRowsFromEntry(entry: JournalEntry): EditableRow[] {
|
||||
const lines = ((entry.lines || []) as JournalEntryLine[])
|
||||
.slice()
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
|
||||
return lines.map((l) => {
|
||||
const bas = BAS_REFERENCE.find((a) => a.account_number === l.account_number)
|
||||
const bas = getBasLoadedByNumber(l.account_number)
|
||||
return {
|
||||
id: l.id || `seed_${++seedIdCounter}`,
|
||||
account_number: l.account_number,
|
||||
@@ -67,7 +69,10 @@ export default function CorrectOpeningBalanceDialog({
|
||||
onCorrected,
|
||||
}: Props) {
|
||||
const { toast } = useToast()
|
||||
const initialRows = useMemo(() => seedRowsFromEntry(entry), [entry])
|
||||
const basReady = useBasReference()
|
||||
// basReady is a re-seed trigger: names fill in once the chart chunk lands.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const initialRows = useMemo(() => seedRowsFromEntry(entry), [entry, basReady])
|
||||
const [state, setState] = useState<OpeningBalanceEditorState | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useFiscalPeriods } from '@/lib/reference-data/hooks'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments'
|
||||
import NoDocRequiredToggle from '@/components/bookkeeping/NoDocRequiredToggle'
|
||||
import CorrectionEntryDialog from '@/components/bookkeeping/CorrectionEntryDialog'
|
||||
@@ -219,6 +220,9 @@ export default function JournalEntryList({
|
||||
const { canWrite } = useCanWrite()
|
||||
const company = useCompanyOptional()?.company ?? null
|
||||
const t = useTranslations('journal_list')
|
||||
// Loads the BAS chart chunk after mount and re-renders once names and
|
||||
// descriptions for non-hardcoded accounts are available.
|
||||
useBasReference()
|
||||
const [entries, setEntries] = useState<JournalEntry[]>([])
|
||||
const [committingId, setCommittingId] = useState<string | null>(null)
|
||||
// Confirm-before-posting (convention 10): the draft the user is about to
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import { DestructiveConfirmDialog } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { getFormat } from '@/lib/import/bank-file/parser'
|
||||
import { getFormat } from '@/lib/import/bank-file/formats'
|
||||
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
import Fuse, { type IFuseOptions } from 'fuse.js'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Plus, Trash2, AlertTriangle, Scale } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
|
||||
import { getBasLoaded } from '@/lib/bookkeeping/bas-lazy'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import type { BASReferenceAccount } from '@/lib/bookkeeping/bas-reference'
|
||||
|
||||
export interface EditableRow {
|
||||
id: string
|
||||
@@ -36,32 +38,32 @@ interface OpeningBalanceRowEditorProps {
|
||||
}
|
||||
|
||||
// Balance-sheet accounts (class 1-2) drive the primary suggestions; numeric
|
||||
// queries fall back to the full chart.
|
||||
const BALANCE_SHEET_ACCOUNTS = BAS_REFERENCE.filter(
|
||||
(a) => a.account_class === 1 || a.account_class === 2,
|
||||
)
|
||||
const ALL_BAS_ACCOUNTS = BAS_REFERENCE
|
||||
// queries fall back to the full chart. The chart is a lazily loaded chunk
|
||||
// (lib/bookkeeping/bas-lazy.ts): the indexes are built on first use after
|
||||
// it has arrived, and the suggestion list is empty until then.
|
||||
const FUSE_OPTIONS: IFuseOptions<BASReferenceAccount> = {
|
||||
keys: ['account_number', 'account_name'],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
}
|
||||
|
||||
let fuseInstance: Fuse<(typeof BAS_REFERENCE)[0]> | null = null
|
||||
function getFuse() {
|
||||
if (!fuseInstance) {
|
||||
fuseInstance = new Fuse(ALL_BAS_ACCOUNTS, {
|
||||
keys: ['account_number', 'account_name'],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
})
|
||||
}
|
||||
let fuseInstance: Fuse<BASReferenceAccount> | null = null
|
||||
function getFuse(): Fuse<BASReferenceAccount> | null {
|
||||
const chart = getBasLoaded()
|
||||
if (!chart) return null
|
||||
if (!fuseInstance) fuseInstance = new Fuse(chart, FUSE_OPTIONS)
|
||||
return fuseInstance
|
||||
}
|
||||
|
||||
let balanceFuseInstance: Fuse<(typeof BAS_REFERENCE)[0]> | null = null
|
||||
function getBalanceFuse() {
|
||||
let balanceFuseInstance: Fuse<BASReferenceAccount> | null = null
|
||||
function getBalanceFuse(): Fuse<BASReferenceAccount> | null {
|
||||
const chart = getBasLoaded()
|
||||
if (!chart) return null
|
||||
if (!balanceFuseInstance) {
|
||||
balanceFuseInstance = new Fuse(BALANCE_SHEET_ACCOUNTS, {
|
||||
keys: ['account_number', 'account_name'],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
})
|
||||
balanceFuseInstance = new Fuse(
|
||||
chart.filter((a) => a.account_class === 1 || a.account_class === 2),
|
||||
FUSE_OPTIONS,
|
||||
)
|
||||
}
|
||||
return balanceFuseInstance
|
||||
}
|
||||
@@ -159,12 +161,16 @@ export default function OpeningBalanceRowEditor({
|
||||
onChangeRef.current({ rows, totals, canSubmit })
|
||||
}, [rows, totals, canSubmit])
|
||||
|
||||
const basReady = useBasReference()
|
||||
const autocompleteResults = useMemo(() => {
|
||||
if (!autocompleteQuery || autocompleteQuery.length < 1) return []
|
||||
const isNumeric = /^\d+$/.test(autocompleteQuery)
|
||||
const fuse = isNumeric ? getFuse() : getBalanceFuse()
|
||||
if (!fuse) return []
|
||||
return fuse.search(autocompleteQuery, { limit: 8 }).map((r) => r.item)
|
||||
}, [autocompleteQuery])
|
||||
// basReady re-runs the search once the chart chunk has arrived.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autocompleteQuery, basReady])
|
||||
|
||||
const updateRow = useCallback((id: string, updates: Partial<EditableRow>) => {
|
||||
setRows((prev) =>
|
||||
@@ -208,7 +214,7 @@ export default function OpeningBalanceRowEditor({
|
||||
}, [])
|
||||
|
||||
const selectAutocompleteItem = useCallback(
|
||||
(rowId: string, account: (typeof BAS_REFERENCE)[0]) => {
|
||||
(rowId: string, account: BASReferenceAccount) => {
|
||||
updateRow(rowId, {
|
||||
account_number: account.account_number,
|
||||
account_name: account.account_name,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2, ShieldCheck, ShieldOff } from 'lucide-react'
|
||||
import { isMfaRequired } from '@/lib/auth/mfa'
|
||||
import { isBankIdEnabled } from '@/lib/auth/bankid'
|
||||
import { isBankIdEnabled } from '@/lib/auth/bankid-flags'
|
||||
import { isSelfHosted as readSelfHostedFlag } from '@/lib/env/public-flags'
|
||||
import { AutoLogoutToggle } from '@/components/settings/AutoLogoutToggle'
|
||||
import { BankIdSettings } from '@/components/settings/BankIdSettings'
|
||||
|
||||
@@ -26,6 +26,7 @@ import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import { SupplierInvoiceReviewContent } from '@/components/suppliers/SupplierInvoiceReviewContent'
|
||||
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
|
||||
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import { formatCounterpartyName } from '@/lib/bookkeeping/counterparty-templates'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/utils'
|
||||
@@ -162,6 +163,9 @@ export default function NewSupplierInvoiceForm({
|
||||
const { canWrite } = useCanWrite()
|
||||
const { toast } = useToast()
|
||||
const t = useTranslations('supplier_invoice_editor')
|
||||
// Loads the BAS chart chunk after mount and re-renders once names and
|
||||
// descriptions for non-hardcoded accounts are available.
|
||||
useBasReference()
|
||||
const ta = useTranslations('accruals')
|
||||
|
||||
// When opened from an invoice-inbox item, every redirect should land the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { getAccountDescription, type AccountType } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
@@ -41,6 +42,9 @@ export function AccountNumber({
|
||||
size = 'default',
|
||||
className,
|
||||
}: AccountNumberProps) {
|
||||
// Loads the BAS chart chunk after mount and re-renders once names and
|
||||
// descriptions for non-hardcoded accounts are available.
|
||||
useBasReference()
|
||||
const desc = getAccountDescription(number)
|
||||
const displayName = desc?.name ?? name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user