mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-27 17:48:17 -05:00
refactor(api): defineConfig vitest, api-helpers, drop vite.api build
- Wrap api vitest.config with defineConfig for typing/IDE - Add loot-core api-helpers, use in YNAB4/YNAB5 importers - Remove vite.api.config, build-api, injected.js; simplify api package
This commit is contained in:
@@ -4,16 +4,16 @@ import type {
|
||||
} from 'node-fetch';
|
||||
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
import {
|
||||
init as initLootCore,
|
||||
lib,
|
||||
type InitConfig,
|
||||
} from 'loot-core/server/main';
|
||||
|
||||
// oxlint-disable-next-line typescript/ban-ts-comment
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
import * as injected from './injected';
|
||||
import { validateNodeVersion } from './validateNodeVersion';
|
||||
|
||||
let actualApp: null | typeof bundle.lib;
|
||||
export const internal = bundle.lib;
|
||||
let actualApp: null | typeof lib;
|
||||
export const internal = lib;
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
@@ -33,11 +33,10 @@ export async function init(config: InitConfig = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
await initLootCore(config);
|
||||
actualApp = lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
return lib;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// TODO: comment on why it works this way
|
||||
|
||||
export let send;
|
||||
|
||||
export function override(sendImplementation) {
|
||||
send = sendImplementation;
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import type {
|
||||
APIPayeeEntity,
|
||||
APIScheduleEntity,
|
||||
} from 'loot-core/server/api-models';
|
||||
import { lib } from 'loot-core/server/main';
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { ImportTransactionsOpts } from 'loot-core/types/api-handlers';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
@@ -14,15 +16,13 @@ import type {
|
||||
TransactionEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
export { q } from './app/query';
|
||||
|
||||
function send<K extends keyof Handlers, T extends Handlers[K]>(
|
||||
name: K,
|
||||
args?: Parameters<T>[0],
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
return injected.send(name, args);
|
||||
return lib.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(
|
||||
@@ -125,10 +125,7 @@ export function addTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export type ImportTransactionsOpts = {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
export type { ImportTransactionsOpts };
|
||||
|
||||
export function importTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
|
||||
@@ -10,25 +10,26 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
|
||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp default-db.sqlite dist/",
|
||||
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run clean && yarn run build:app && yarn run build:crdt && vitest --run",
|
||||
"build": "yarn run clean && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run clean && yarn run build:crdt && vitest --run",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"loot-core": "workspace:^",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-plugin-peggy-loader": "^2.0.1",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/ban-ts-comment
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
import { lib } from 'loot-core/server/main';
|
||||
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
export const amountToInteger = lib.amountToInteger;
|
||||
export const integerToAmount = lib.integerToAmount;
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
export default {
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.api.js',
|
||||
'.api.ts',
|
||||
'.api.tsx',
|
||||
'.electron.js',
|
||||
'.electron.ts',
|
||||
'.electron.tsx',
|
||||
'.js',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.json',
|
||||
],
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
test: {
|
||||
globals: true,
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
4
packages/api/vitest.setup.ts
Normal file
4
packages/api/vitest.setup.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Ensure CRDT proto file is loaded before tests run
|
||||
// This ensures the proto namespace is set up on globalThis before
|
||||
// CRDT exports try to access it
|
||||
import '@actual-app/crdt/src/proto/sync_pb.js';
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
yarn tsc -p tsconfig.api.json --outDir ../api/@types/loot-core/
|
||||
# Copy existing handwritten .d.ts files, as tsc doesn't move them for us
|
||||
dest="../../api/@types/loot-core"
|
||||
cd src
|
||||
find . -type f -name "*.d.ts" | while read -r f
|
||||
do
|
||||
d=$(dirname "${f}")
|
||||
d="${dest}/${d}"
|
||||
mkdir -p "${d}"
|
||||
cp "${f}" "${d}"
|
||||
done
|
||||
cd "$ROOT"
|
||||
yarn vite build --config ./vite.api.config.ts;
|
||||
./bin/copy-migrations ../api
|
||||
@@ -54,7 +54,6 @@
|
||||
"scripts": {
|
||||
"build:node": "cross-env NODE_ENV=production vite build --config ./vite.desktop.config.ts",
|
||||
"watch:node": "cross-env NODE_ENV=development vite build --config ./vite.desktop.config.ts --watch",
|
||||
"build:api": "cross-env NODE_ENV=development ./bin/build-api",
|
||||
"build:browser": "cross-env NODE_ENV=production ./bin/build-browser",
|
||||
"watch:browser": "cross-env NODE_ENV=development ./bin/build-browser",
|
||||
"generate:i18n": "i18next",
|
||||
@@ -85,7 +84,6 @@
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actual-app/api": "workspace:^",
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"@actual-app/web": "workspace:^",
|
||||
"@swc/core": "^1.15.8",
|
||||
|
||||
100
packages/loot-core/src/server/importers/api-helpers.ts
Normal file
100
packages/loot-core/src/server/importers/api-helpers.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// @ts-strict-ignore
|
||||
// Local API helper module for importers
|
||||
// This provides the same interface as @actual-app/api/methods but uses handlers directly
|
||||
// to avoid cyclic dependency between loot-core and @actual-app/api
|
||||
|
||||
import { type Handlers } from '../../types/handlers';
|
||||
import type { ImportTransactionEntity } from '../../types/models';
|
||||
import type {
|
||||
APIAccountEntity,
|
||||
APICategoryEntity,
|
||||
APICategoryGroupEntity,
|
||||
APIPayeeEntity,
|
||||
} from '../api-models';
|
||||
import { app } from '../main-app';
|
||||
import { runHandler } from '../mutators';
|
||||
|
||||
// Send function that calls handlers directly
|
||||
export function send<K extends keyof Handlers>(
|
||||
name: K,
|
||||
args?: Parameters<Handlers[K]>[0],
|
||||
): Promise<Awaited<ReturnType<Handlers[K]>>> {
|
||||
return runHandler(app.handlers[name], args) as Promise<
|
||||
Awaited<ReturnType<Handlers[K]>>
|
||||
>;
|
||||
}
|
||||
|
||||
// API methods used by importers
|
||||
export async function createAccount(
|
||||
account: Omit<APIAccountEntity, 'id'>,
|
||||
initialBalance?: number,
|
||||
) {
|
||||
return send('api/account-create', { account, initialBalance });
|
||||
}
|
||||
|
||||
export async function getAccounts() {
|
||||
return send('api/accounts-get');
|
||||
}
|
||||
|
||||
export async function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
}
|
||||
|
||||
export async function createCategoryGroup(
|
||||
group: Omit<APICategoryGroupEntity, 'id'>,
|
||||
) {
|
||||
return send('api/category-group-create', { group });
|
||||
}
|
||||
|
||||
export async function createCategory(category: Omit<APICategoryEntity, 'id'>) {
|
||||
return send('api/category-create', { category });
|
||||
}
|
||||
|
||||
export async function createPayee(payee: Omit<APIPayeeEntity, 'id'>) {
|
||||
return send('api/payee-create', { payee });
|
||||
}
|
||||
|
||||
export async function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
|
||||
export async function addTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
transactions: Omit<ImportTransactionEntity, 'account'>[],
|
||||
{
|
||||
learnCategories = false,
|
||||
runTransfers = false,
|
||||
}: { learnCategories?: boolean; runTransfers?: boolean } = {},
|
||||
) {
|
||||
return send('api/transactions-add', {
|
||||
accountId,
|
||||
transactions,
|
||||
learnCategories,
|
||||
runTransfers,
|
||||
});
|
||||
}
|
||||
|
||||
export async function batchBudgetUpdates(func: () => Promise<void>) {
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
await func();
|
||||
} finally {
|
||||
await send('api/batch-budget-end');
|
||||
}
|
||||
}
|
||||
|
||||
export async function setBudgetAmount(
|
||||
month: string,
|
||||
categoryId: APICategoryEntity['id'],
|
||||
value: number,
|
||||
) {
|
||||
return send('api/budget-set-amount', { month, categoryId, amount: value });
|
||||
}
|
||||
|
||||
export async function setBudgetCarryover(
|
||||
month: string,
|
||||
categoryId: APICategoryEntity['id'],
|
||||
flag: boolean,
|
||||
) {
|
||||
return send('api/budget-set-carryover', { month, categoryId, flag });
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
import { send } from '@actual-app/api/injected';
|
||||
import * as actual from '@actual-app/api/methods';
|
||||
import AdmZip from 'adm-zip';
|
||||
import normalizePathSep from 'slash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -13,6 +6,12 @@ import { logger } from '../../platform/server/log';
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { amountToInteger, groupBy, sortByKey } from '../../shared/util';
|
||||
|
||||
// @ts-strict-ignore
|
||||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We use local API helpers that call handlers directly
|
||||
// to avoid cyclic dependency between loot-core and @actual-app/api
|
||||
import { send } from './api-helpers';
|
||||
import * as actual from './api-helpers';
|
||||
import type * as YNAB4 from './ynab4-types';
|
||||
|
||||
// Importer
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// @ts-strict-ignore
|
||||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
import { send } from '@actual-app/api/injected';
|
||||
import * as actual from '@actual-app/api/methods';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { logger } from '../../platform/server/log';
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { groupBy, sortByKey } from '../../shared/util';
|
||||
|
||||
// @ts-strict-ignore
|
||||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We use local API helpers that call handlers directly
|
||||
// to avoid cyclic dependency between loot-core and @actual-app/api
|
||||
import { send } from './api-helpers';
|
||||
import * as actual from './api-helpers';
|
||||
import type * as YNAB5 from './ynab5-types';
|
||||
|
||||
function amountFromYnab(amount: number) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import './polyfills';
|
||||
import * as injectAPI from '@actual-app/api/injected';
|
||||
|
||||
import * as asyncStorage from '../platform/server/asyncStorage';
|
||||
import * as connection from '../platform/server/connection';
|
||||
import * as fs from '../platform/server/fs';
|
||||
@@ -126,8 +124,6 @@ handlers['app-focused'] = async function () {
|
||||
|
||||
handlers = installAPI(handlers) as Handlers;
|
||||
|
||||
injectAPI.override((name, args) => runHandler(app.handlers[name], args));
|
||||
|
||||
// A hack for now until we clean up everything
|
||||
app.handlers = handlers;
|
||||
app.combine(
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { type ImportTransactionsOpts } from '@actual-app/api';
|
||||
|
||||
import type { ImportTransactionsResult } from '../server/accounts/app';
|
||||
import type {
|
||||
APIAccountEntity,
|
||||
@@ -22,6 +20,11 @@ import type {
|
||||
TransactionEntity,
|
||||
} from './models';
|
||||
|
||||
export type ImportTransactionsOpts = {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export type ApiHandlers = {
|
||||
'api/batch-budget-start': () => Promise<void>;
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { defineConfig } from 'vite';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const outDir = path.resolve(__dirname, '../api/app');
|
||||
const crdtDir = path.resolve(__dirname, '../crdt');
|
||||
|
||||
return {
|
||||
mode,
|
||||
ssr: { noExternal: true, external: ['better-sqlite3'] },
|
||||
build: {
|
||||
target: 'node18',
|
||||
outDir,
|
||||
emptyOutDir: false,
|
||||
ssr: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/server/main.ts'),
|
||||
formats: ['cjs'],
|
||||
},
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'bundle.api.js',
|
||||
format: 'cjs',
|
||||
name: 'api',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.api.js',
|
||||
'.api.ts',
|
||||
'.api.tsx',
|
||||
'.electron.js',
|
||||
'.electron.ts',
|
||||
'.electron.tsx',
|
||||
'.js',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.json',
|
||||
],
|
||||
alias: [
|
||||
{
|
||||
find: 'handlebars',
|
||||
replacement: require.resolve('handlebars/dist/handlebars.js'),
|
||||
},
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve(crdtDir, 'src') + '$1',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
peggyLoader(),
|
||||
visualizer({ template: 'raw-data', filename: `${outDir}/stats.json` }),
|
||||
],
|
||||
};
|
||||
});
|
||||
6
upcoming-release-notes/6809.md
Normal file
6
upcoming-release-notes/6809.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Remove cyclic dependency between API and loot-core
|
||||
@@ -19,17 +19,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@actual-app/api@workspace:^, @actual-app/api@workspace:packages/api":
|
||||
"@actual-app/api@workspace:packages/api":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/api@workspace:packages/api"
|
||||
dependencies:
|
||||
"@actual-app/crdt": "workspace:^"
|
||||
better-sqlite3: "npm:^12.5.0"
|
||||
compare-versions: "npm:^6.1.1"
|
||||
loot-core: "workspace:^"
|
||||
node-fetch: "npm:^3.3.2"
|
||||
tsc-alias: "npm:^1.8.16"
|
||||
typescript: "npm:^5.9.3"
|
||||
uuid: "npm:^13.0.0"
|
||||
vite-plugin-peggy-loader: "npm:^2.0.1"
|
||||
vitest: "npm:^4.0.16"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -19213,11 +19215,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loot-core@workspace:*, loot-core@workspace:packages/loot-core":
|
||||
"loot-core@workspace:*, loot-core@workspace:^, loot-core@workspace:packages/loot-core":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "loot-core@workspace:packages/loot-core"
|
||||
dependencies:
|
||||
"@actual-app/api": "workspace:^"
|
||||
"@actual-app/crdt": "workspace:^"
|
||||
"@actual-app/web": "workspace:^"
|
||||
"@jlongster/sql.js": "npm:^1.6.7"
|
||||
|
||||
Reference in New Issue
Block a user