mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-22 00:13:45 -05:00
Compare commits
13 Commits
dependabot
...
claude/bro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ed7d88a3 | ||
|
|
1d34d15f12 | ||
|
|
e512ff7312 | ||
|
|
f0470a37de | ||
|
|
b662205691 | ||
|
|
4a5b572baf | ||
|
|
5b179b0672 | ||
|
|
645623a6c9 | ||
|
|
1442747896 | ||
|
|
3e2303e5dc | ||
|
|
332db28e2e | ||
|
|
6d6e032429 | ||
|
|
399e59c088 |
25
packages/api/index.browser.ts
Normal file
25
packages/api/index.browser.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { init as initLootCore } from '@actual-app/core/server/main';
|
||||
import type { InitConfig, lib } from '@actual-app/core/server/main';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
let internal: typeof lib | null = null;
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
internal = await initLootCore(config);
|
||||
return internal;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (internal) {
|
||||
try {
|
||||
await internal.send('sync');
|
||||
} catch {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
|
||||
await internal.send('close-budget');
|
||||
internal = null;
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,42 @@
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/browser.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/browser.js"
|
||||
},
|
||||
"default": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/browser.js"
|
||||
},
|
||||
"default": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest --run",
|
||||
"build": "npm-run-all -cp 'build:*'",
|
||||
"build:node": "vite build",
|
||||
"build:browser": "vite build --config vite.browser.config.ts",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:node": "vitest --run",
|
||||
"test:browser": "vitest --run -c vite.browser.config.ts",
|
||||
"typecheck": "tsgo -b && tsc-strict"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -38,6 +56,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup-plugin-visualizer": "^6.0.11",
|
||||
"typescript-strict-plugin": "^2.4.4",
|
||||
"vite": "^8.0.0",
|
||||
|
||||
@@ -1,67 +1,15 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { RuleEntity } from '@actual-app/core/types/models';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
|
||||
// Mock the fs so path constants point at loot-core package root where migrations live.
|
||||
vi.mock(
|
||||
'../loot-core/src/platform/server/fs/index.api',
|
||||
async importOriginal => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
const pathMod = await import('path');
|
||||
const lootCoreRoot = pathMod.join(__dirname, '..', 'loot-core');
|
||||
return {
|
||||
...actual,
|
||||
migrationsPath: pathMod.join(lootCoreRoot, 'migrations'),
|
||||
bundledDatabasePath: pathMod.join(lootCoreRoot, 'default-db.sqlite'),
|
||||
demoBudgetPath: pathMod.join(lootCoreRoot, 'demo-budget'),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
global.IS_TESTING = true;
|
||||
|
||||
beforeEach(async () => {
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
await createTestBudget('default-budget-template', budgetName);
|
||||
await api.init({
|
||||
dataDir: path.join(__dirname, '/mocks/budgets/'),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
global.currentMonth = null;
|
||||
await api.shutdown();
|
||||
});
|
||||
|
||||
async function createTestBudget(templateName: string, name: string) {
|
||||
const templatePath = path.join(
|
||||
__dirname,
|
||||
'/../loot-core/src/mocks/files',
|
||||
templateName,
|
||||
);
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', name);
|
||||
|
||||
await fs.mkdir(budgetPath);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'metadata.json'),
|
||||
path.join(budgetPath, 'metadata.json'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'db.sqlite'),
|
||||
path.join(budgetPath, 'db.sqlite'),
|
||||
if (!globalThis.__test_api) {
|
||||
throw new Error(
|
||||
'Test setup did not run — __test_api is not defined. ' +
|
||||
'Ensure a setupFile (setup.node.ts or setup.browser.ts) is configured.',
|
||||
);
|
||||
}
|
||||
|
||||
const api = globalThis.__test_api;
|
||||
const budgetName = globalThis.__test_budget_name;
|
||||
|
||||
describe('API setup and teardown', () => {
|
||||
// apis: loadBudget, getBudgetMonths
|
||||
test('successfully loads budget', async () => {
|
||||
@@ -93,7 +41,7 @@ describe('API CRUD operations', () => {
|
||||
// apis: getCategoryGroups, createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
|
||||
test('CategoryGroups: successfully update category groups', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
globalThis.currentMonth = month;
|
||||
|
||||
// get existing category groups
|
||||
const groups = await api.getCategoryGroups();
|
||||
@@ -164,7 +112,7 @@ describe('API CRUD operations', () => {
|
||||
// apis: createCategory, getCategories, updateCategory, deleteCategory
|
||||
test('Categories: successfully update categories', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
globalThis.currentMonth = month;
|
||||
|
||||
// create our test category group
|
||||
const mainGroupId = await api.createCategoryGroup({
|
||||
@@ -246,7 +194,7 @@ describe('API CRUD operations', () => {
|
||||
// apis: setBudgetAmount, setBudgetCarryover, getBudgetMonth
|
||||
test('Budgets: successfully update budgets', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
globalThis.currentMonth = month;
|
||||
|
||||
// create some new categories to test with
|
||||
const groupId = await api.createCategoryGroup({
|
||||
12
packages/api/test/globals.d.ts
vendored
Normal file
12
packages/api/test/globals.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import type * as BrowserApi from '../index.browser';
|
||||
|
||||
declare global {
|
||||
// oxlint-disable-next-line no-var
|
||||
var __test_api: typeof BrowserApi;
|
||||
// oxlint-disable-next-line no-var
|
||||
var __test_budget_name: string;
|
||||
// oxlint-disable-next-line no-var
|
||||
var IS_TESTING: boolean;
|
||||
// oxlint-disable-next-line no-var
|
||||
var currentMonth: string | null;
|
||||
}
|
||||
92
packages/api/test/setup.browser.ts
Normal file
92
packages/api/test/setup.browser.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'fake-indexeddb/auto';
|
||||
import * as nodeFs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { afterEach, beforeAll, vi } from 'vitest';
|
||||
|
||||
import type * as BrowserFs from '@actual-app/core/platform/server/fs';
|
||||
|
||||
import * as api from '../index.browser';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
const lootCoreRoot = path.join(__dirname, '..', '..', 'loot-core');
|
||||
|
||||
globalThis.IS_TESTING = true;
|
||||
|
||||
// Populate the emscripten virtual FS with migration files and default-db.sqlite
|
||||
// (normally done by populateDefaultFilesystem() which is skipped in test mode).
|
||||
async function populateDefaultFiles(fs: typeof BrowserFs) {
|
||||
if (!(await fs.exists('/migrations'))) {
|
||||
await fs.mkdir('/migrations');
|
||||
}
|
||||
|
||||
const migrationsDir = path.join(lootCoreRoot, 'migrations');
|
||||
const migrationFiles = await nodeFs.readdir(migrationsDir);
|
||||
for (const file of migrationFiles) {
|
||||
if (file.endsWith('.sql') || file.endsWith('.js')) {
|
||||
const contents = await nodeFs.readFile(path.join(migrationsDir, file));
|
||||
await fs.writeFile(`/migrations/${file}`, new Uint8Array(contents));
|
||||
}
|
||||
}
|
||||
|
||||
const defaultDb = await nodeFs.readFile(
|
||||
path.join(lootCoreRoot, 'default-db.sqlite'),
|
||||
);
|
||||
await fs.writeFile('/default-db.sqlite', new Uint8Array(defaultDb));
|
||||
}
|
||||
|
||||
// Write the test budget template into the virtual FS.
|
||||
async function writeBudgetFiles(fs: typeof BrowserFs) {
|
||||
const templatePath = path.join(
|
||||
lootCoreRoot,
|
||||
'src/mocks/files/default-budget-template',
|
||||
);
|
||||
const metadataContents = await nodeFs.readFile(
|
||||
path.join(templatePath, 'metadata.json'),
|
||||
'utf8',
|
||||
);
|
||||
const dbContents = await nodeFs.readFile(
|
||||
path.join(templatePath, 'db.sqlite'),
|
||||
);
|
||||
|
||||
const budgetDir = `/documents/${budgetName}`;
|
||||
await fs.mkdir(budgetDir);
|
||||
await fs.writeFile(`${budgetDir}/metadata.json`, metadataContents);
|
||||
await fs.writeFile(`${budgetDir}/db.sqlite`, new Uint8Array(dbContents));
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const baseURL = `${__dirname}/../../../node_modules/@jlongster/sql.js/dist/`;
|
||||
process.env.PUBLIC_URL = baseURL;
|
||||
|
||||
// Patch fetch so sql.js WASM loader reads from disk instead of HTTP
|
||||
vi.spyOn(global, 'fetch').mockImplementation(async input => {
|
||||
const url =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.href
|
||||
: input.url;
|
||||
if (url.startsWith(baseURL)) {
|
||||
return new Response(new Uint8Array(await nodeFs.readFile(url)), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: { 'Content-Type': 'application/wasm' },
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`fetch not mocked for ${url}`));
|
||||
});
|
||||
|
||||
await api.init({ dataDir: '/documents' });
|
||||
|
||||
const fs = await import('@actual-app/core/platform/server/fs');
|
||||
await populateDefaultFiles(fs);
|
||||
await writeBudgetFiles(fs);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
globalThis.currentMonth = null;
|
||||
});
|
||||
|
||||
globalThis.__test_api = api;
|
||||
globalThis.__test_budget_name = budgetName;
|
||||
64
packages/api/test/setup.node.ts
Normal file
64
packages/api/test/setup.node.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { afterEach, beforeEach, vi } from 'vitest';
|
||||
|
||||
import * as api from '../index';
|
||||
|
||||
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
|
||||
// Mock the fs so path constants point at loot-core package root where migrations live.
|
||||
vi.mock(
|
||||
'../../loot-core/src/platform/server/fs/index.api',
|
||||
async importOriginal => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
const pathMod = await import('path');
|
||||
const lootCoreRoot = pathMod.join(__dirname, '..', '..', 'loot-core');
|
||||
return {
|
||||
...actual,
|
||||
migrationsPath: pathMod.join(lootCoreRoot, 'migrations'),
|
||||
bundledDatabasePath: pathMod.join(lootCoreRoot, 'default-db.sqlite'),
|
||||
demoBudgetPath: pathMod.join(lootCoreRoot, 'demo-budget'),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
globalThis.IS_TESTING = true;
|
||||
|
||||
async function createTestBudget(templateName: string, name: string) {
|
||||
const templatePath = path.join(
|
||||
__dirname,
|
||||
'/../../loot-core/src/mocks/files',
|
||||
templateName,
|
||||
);
|
||||
const budgetPath = path.join(__dirname, '/../mocks/budgets/', name);
|
||||
|
||||
await fs.mkdir(budgetPath);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'metadata.json'),
|
||||
path.join(budgetPath, 'metadata.json'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'db.sqlite'),
|
||||
path.join(budgetPath, 'db.sqlite'),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const budgetPath = path.join(__dirname, '/../mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
await createTestBudget('default-budget-template', budgetName);
|
||||
await api.init({
|
||||
dataDir: path.join(__dirname, '/../mocks/budgets/'),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
globalThis.currentMonth = null;
|
||||
await api.shutdown();
|
||||
});
|
||||
|
||||
globalThis.__test_api = api;
|
||||
globalThis.__test_budget_name = budgetName;
|
||||
@@ -18,5 +18,5 @@
|
||||
},
|
||||
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts", "*.config.ts"]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
}
|
||||
|
||||
1
packages/api/typings/vite-plugin-peggy-loader.d.ts
vendored
Normal file
1
packages/api/typings/vite-plugin-peggy-loader.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'vite-plugin-peggy-loader';
|
||||
33
packages/api/vite.browser.config.ts
Normal file
33
packages/api/vite.browser.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import path from 'path';
|
||||
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'esnext',
|
||||
outDir: distDir,
|
||||
emptyOutDir: false,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'index.browser.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'browser.js',
|
||||
},
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
resolve: {
|
||||
// Default extensions — picks up browser implementations (index.ts)
|
||||
// instead of .api.ts (which resolves to Node.js/Electron code)
|
||||
extensions: ['.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./test/setup.browser.ts'],
|
||||
include: ['test/**/*.test.ts'],
|
||||
maxWorkers: 2,
|
||||
},
|
||||
});
|
||||
@@ -2,9 +2,9 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const lootCoreRoot = path.resolve(__dirname, '../loot-core');
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
@@ -84,6 +84,8 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
setupFiles: ['./test/setup.node.ts'],
|
||||
include: ['test/**/*.test.ts'],
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
|
||||
@@ -304,7 +304,7 @@ export function registerQueryCommand(program: Command) {
|
||||
? buildQueryFromFile(parsed, cmdOpts.table)
|
||||
: buildQueryFromFlags(cmdOpts);
|
||||
|
||||
const result = await api.aqlQuery(queryObj);
|
||||
const result = (await api.aqlQuery(queryObj)) as { data: unknown };
|
||||
|
||||
if (cmdOpts.count) {
|
||||
printOutput({ count: result.data }, opts.format);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"./platform/server/connection": "./src/platform/server/connection/index.ts",
|
||||
"./platform/server/fetch": "./src/platform/server/fetch/index.ts",
|
||||
"./platform/server/fs": "./src/platform/server/fs/index.ts",
|
||||
"./platform/server/indexeddb": "./src/platform/server/indexeddb/index.ts",
|
||||
"./platform/server/log": "./src/platform/server/log/index.ts",
|
||||
"./platform/server/sqlite": "./src/platform/server/sqlite/index.ts",
|
||||
"./server/budget/types/*": "./src/server/budget/types/*.d.ts",
|
||||
|
||||
6
upcoming-release-notes/7247.md
Normal file
6
upcoming-release-notes/7247.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
api: add browser support
|
||||
@@ -28,7 +28,9 @@ __metadata:
|
||||
"@typescript/native-preview": "npm:^7.0.0-dev.20260309.1"
|
||||
better-sqlite3: "npm:^12.6.2"
|
||||
compare-versions: "npm:^6.1.1"
|
||||
fake-indexeddb: "npm:^6.2.5"
|
||||
node-fetch: "npm:^3.3.2"
|
||||
npm-run-all: "npm:^4.1.5"
|
||||
rollup-plugin-visualizer: "npm:^6.0.11"
|
||||
typescript-strict-plugin: "npm:^2.4.4"
|
||||
uuid: "npm:^13.0.0"
|
||||
|
||||
Reference in New Issue
Block a user