Compare commits

...

12 Commits

Author SHA1 Message Date
github-actions[bot]
8393a65d7a [AI] Fix applyAppUpdate hanging in dev mode
In dev mode browser-preload's updateSW was () => undefined, so
applyAppUpdate() — which calls updateSW() and then awaits a
deliberately never-resolving promise (waiting for the SW-driven page
reload) — hung the renderer instead of refreshing. In prod the page
is replaced by the new service worker, so the never-resolving await is
fine. The dev path now triggers a plain window.location.reload() so
the page reloads and the never-settling await is irrelevant, matching
prod's effective behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:52:50 +01:00
github-actions[bot]
6b351eafc7 [AI] Restore bank-factory glob to ./banks/*_*.{ts,js}
Re-apply the glob widening originally added in 145868f9d. It was
reverted in 531b1a191 because the desktop e2e was failing — that
failure is now traced to the rebuild-electron breakage (fixed in
6e8ac0784), not to this glob. Mirroring migrations.ts so future TS
bank handlers are picked up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:32:46 +01:00
github-actions[bot]
43fba254b5 [AI] Restore CSP unsafe-eval comment
Bring back the explanatory comment that was stripped diagnostically in
99682268c. Now that the desktop e2e regression is traced to
rebuild-electron and not to anything in this branch, we can keep the
documentation noting why 'unsafe-eval' is retained in both CSP branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:29:24 +01:00
github-actions[bot]
6e8ac07846 [AI] Make rebuild-electron actually rebuild better-sqlite3
PR #7712 simplified rebuild-electron to just `electron-rebuild -f -o
better-sqlite3,bcrypt` from the repo root. Two problems with that:

  1. Without `-m`, electron-rebuild scans the root workspace's package.json
     for native deps. better-sqlite3 isn't a direct root dep — it lives
     under packages/sync-server/ — so the scan returns no candidates and
     the rebuild silently no-ops.
  2. Without --build-from-source, electron-rebuild defers to
     prebuild-install, which downloads a stale prebuilt binary keyed off
     better-sqlite3's package.json (ABI 127) instead of recompiling
     against Electron 39's bundled Node ABI 140. The download succeeds
     and "Rebuild Complete" prints, but the resulting `better_sqlite3.node`
     can't `dlopen` inside Electron's utility process — sync-server
     crashes immediately on db init, the renderer's startSyncServer IPC
     never resolves, and the e2e test hangs on "Configure your server".

Point -m at packages/desktop-electron (which transitively pulls in
better-sqlite3 and bcrypt via @actual-app/sync-server) and force a real
compile via --build-from-source. Verified locally: better-sqlite3
rebuilds to darwin-arm64-140 and the desktop e2e onboarding test passes
in 6s instead of hanging for 60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:23:46 +01:00
github-actions[bot]
99682268cc [AI] Strip CSP comment to restore identical state to 9513c1e16
The desktop e2e has been failing despite my prior commits being a strict
revert (only difference was a 2-line comment, which can't change runtime).
Removing even the comment so the branch matches 9513c1e16's relevant
files exactly, to isolate whether the failure is from the master merge
or from CI-environment drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:55:53 +01:00
Matiss Janis Aboltins
749aee4f44 Merge branch 'master' into matiss/crdt-source-loading 2026-05-05 21:29:45 +01:00
github-actions[bot]
531b1a1914 [AI] Revert bank-factory glob change
Widening the glob to ./banks/*_*.{ts,js} broke the desktop e2e tests in
CI even though every current handler is .js and the brace expansion
matches no .ts files locally. Reverting to ./banks/*_*.js — the change
had no behavioural benefit since there are no TS handlers, so the
nitpick isn't worth chasing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:23:32 +01:00
github-actions[bot]
f08490052f [AI] Restore 'unsafe-eval' in production CSP for Electron
The Electron app needs `'unsafe-eval'` at runtime, so revert the dev-only
restriction and keep `'unsafe-eval'` in both branches. Comment updated to
record the actual reason instead of marking it as removable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:10:48 +01:00
github-actions[bot]
145868f9da [AI] Address review feedback
- sync-server CSP: drop 'unsafe-eval' from the production script-src;
  the bundle has no genuine eval/new Function usage (only a defensive
  branch in setimmediate's polyfill that's never hit). Keep it on the
  dev branch where Vite's HMR runtime relies on it. Add a comment so
  it's obvious which branch needs it and why.
- bank-factory: widen the loader glob to ./banks/*_*.{ts,js} so
  TypeScript handlers are discovered too, mirroring migrations.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:04:34 +01:00
github-actions[bot]
9513c1e160 [AI] Restructure sync-server to build with Vite
Replace the hand-rolled tsgo + add-import-extensions + copy-static-assets
+ runtime loader pipeline with a single Vite SSR build. Bundles every
entry (app, bin/actual-server, scripts/*) and inlines @actual-app/crdt
source so Node never has to resolve TS at runtime — the
MODULE_TYPELESS_PACKAGE_JSON warning that surfaced via crdt's source
exports is gone. Migrations and bank handlers move from readdir-based
dynamic imports to import.meta.glob; messages.sql becomes a ?raw import.

Drop loader.mjs, register-loader.mjs, start.mjs, and
bin/add-import-extensions.mjs. Electron's startSyncServer() forks
build/app.js directly. publishConfig.imports goes away (subpath imports
are resolved at build time and don't appear in the bundle).

In dev (start:server-dev) sync-server proxies to Vite, so loosen the CSP
to allow Vite's inline preamble script and HMR websocket — production
CSP is unchanged. desktop-client skips registerSW() in dev (and disables
vite-plugin-pwa's devOptions) so stale cached assets don't override
edits between page loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:32:22 +01:00
github-actions[bot]
e661951753 Add release notes for PR #7702 2026-05-04 13:44:21 +01:00
github-actions[bot]
fc5e836a02 [AI] Load @actual-app/crdt from source in dev, only bundle for publish
@actual-app/crdt's local exports now point at src/index.ts so consumers
(sync-server, loot-core, desktop-client) never see a stale Vite bundle.
publishConfig keeps the dist/ mapping for npm consumers. crdt's
tsconfig switches to bundler module resolution to match the rest of
the workspace (no extensions in source imports).

Sync-server's existing extension-resolution loader is extended to also
handle directory-index imports (./crdt → ./crdt/index.ts), and the
standalone `start` / `start-monitor` scripts now invoke Node with
--import ./register-loader.mjs so the loader is in place before crdt's
source resolves.

Electron's utilityProcess.fork accepts execArgv but doesn't actually
preload --import modules, so a new packages/sync-server/start.mjs
bootstrap entry registers the loader imperatively and then dynamic-
imports build/app.js. desktop-electron's startSyncServer() points the
fork at start.mjs. sync-server's "files" array now ships start.mjs,
register-loader.mjs and loader.mjs so packaged Electron / npm
consumers actually receive them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:32:18 +01:00
20 changed files with 170 additions and 299 deletions

View File

@@ -15,7 +15,8 @@
"vi": "readonly",
"backend": "readonly",
"importScripts": "readonly",
"FS": "readonly"
"FS": "readonly",
"__APP_VERSION__": "readonly"
},
"rules": {
// Import sorting

View File

@@ -52,7 +52,7 @@
"playwright": "yarn workspace @actual-app/web run playwright",
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -o better-sqlite3,bcrypt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt --build-from-source -f",
"rebuild-node": "yarn workspace @actual-app/core rebuild",
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",

View File

@@ -10,14 +10,10 @@
"!dist/**/*.spec.d.ts",
"!dist/**/*.spec.d.ts.map"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
".": "./src/index.ts"
},
"publishConfig": {
"exports": {
@@ -25,7 +21,9 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"main": "dist/index.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build:node": "vite build",

View File

@@ -4,8 +4,8 @@
"rootDir": "./src",
"composite": true,
"target": "ES2021",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "ES2022",
"moduleResolution": "bundler",
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,

View File

@@ -335,10 +335,17 @@ const isUpdateReadyForDownloadPromise = new Promise(resolve => {
resolve(true);
};
});
const updateSW = registerSW({
immediate: true,
onNeedRefresh: markUpdateReadyForDownload,
});
// Skip SW registration in dev so stale cached assets don't override edits
// between page loads. Plugin code that needs a SW can register one itself.
// In dev there is no SW to install, so applyAppUpdate() can't rely on the
// SW lifecycle to swap the page — fall back to a plain reload so callers
// don't hang on the never-resolving promise inside applyAppUpdate.
const updateSW = IS_DEV
? () => window.location.reload()
: registerSW({
immediate: true,
onNeedRefresh: markUpdateReadyForDownload,
});
global.Actual = {
IS_DEV,

View File

@@ -376,7 +376,9 @@ export default defineConfig(async ({ mode, command }) => {
// swSrc: `service-worker/plugin-sw.js`,
// },
devOptions: {
enabled: true, // We need service worker in dev mode to work with plugins
// Disabled: caches stale assets across reloads in dev. Plugin
// code that explicitly needs a SW can register one itself.
enabled: false,
type: 'module',
},
workbox: {

View File

@@ -239,12 +239,11 @@ async function startSyncServer() {
),
};
const serverPath = path.join(
// require.resolve will recursively search up the workspace for the module
path.dirname(require.resolve('@actual-app/sync-server/package.json')),
'build',
'app.js',
// require.resolve will recursively search up the workspace for the module
const syncServerRoot = path.dirname(
require.resolve('@actual-app/sync-server/package.json'),
);
const serverPath = path.join(syncServerRoot, 'build/app.js');
const webRoot = path.join(
// require.resolve will recursively search up the workspace for the module

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { parseArgs } from 'node:util';
const args = process.argv;
@@ -54,11 +53,7 @@ if (values.help) {
}
if (values.version) {
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = resolve(__dirname, '../../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
console.log('v' + packageJson.version);
console.log('v' + __APP_VERSION__);
process.exit();
}

View File

@@ -1,146 +0,0 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from 'node:fs';
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, extname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const buildDir = resolve(__dirname, '../build');
const packageRoot = resolve(__dirname, '..');
const packageJson = JSON.parse(
readFileSync(join(packageRoot, 'package.json'), 'utf-8'),
);
// publishConfig.imports already has ./build/src/ paths with .js extensions
const importsMap = packageJson.publishConfig?.imports || {};
// Sort wildcard patterns longest-prefix-first so more specific patterns
// (e.g. #app-gocardless/services/tests/*) match before broader ones (#app-gocardless/*)
const wildcardEntries = Object.entries(importsMap)
.filter(([p]) => p.includes('*'))
.sort(([a], [b]) => b.length - a.length);
async function getAllJsFiles(dir) {
const files = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await getAllJsFiles(fullPath)));
} else if (entry.isFile() && extname(entry.name) === '.js') {
files.push(fullPath);
}
}
return files;
}
function resolveImportPath(importPath, fromFile) {
const baseDir = dirname(fromFile);
const resolvedPath = resolve(baseDir, importPath);
// Check if it's a file with .js extension
if (existsSync(`${resolvedPath}.js`)) {
return `${importPath}.js`;
}
// Check if it's a directory with index.js
if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
return `${importPath}/index.js`;
}
// Verify the file exists before adding extension
if (!existsSync(`${resolvedPath}.js`)) {
console.warn(
`Warning: Could not resolve import '${importPath}' from ${relative(buildDir, fromFile)}`,
);
}
// Default: assume it's a file and add .js
return `${importPath}.js`;
}
function toRelativePath(target, fromFile) {
const absoluteTarget = resolve(packageRoot, target);
let rel = relative(dirname(fromFile), absoluteTarget);
if (!rel.startsWith('.')) rel = './' + rel;
return rel.split('\\').join('/');
}
function resolveSubpathImport(importPath, fromFile) {
if (importsMap[importPath]) {
return toRelativePath(importsMap[importPath], fromFile);
}
for (const [pattern, target] of wildcardEntries) {
const prefix = pattern.replaceAll('*', '');
if (importPath.startsWith(prefix)) {
const wildcard = importPath.slice(prefix.length);
return toRelativePath(target.replaceAll('*', wildcard), fromFile);
}
}
console.warn(
`Warning: Could not resolve subpath import '${importPath}' from ${relative(buildDir, fromFile)}`,
);
return null;
}
function addExtensionsToImports(content, filePath) {
const importRegex =
/(?:import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?|import\s*\(|require\s*\()['"]((\.\.?\/[^'"]+)|(#[^'"]+))['"]/g;
return content.replace(importRegex, (match, importPath) => {
if (!importPath || typeof importPath !== 'string') {
return match;
}
if (importPath.startsWith('#')) {
const resolved = resolveSubpathImport(importPath, filePath);
if (resolved) {
return match.replace(importPath, resolved);
}
return match;
}
// Skip if already has an extension
if (/\.(js|mjs|ts|mts|json)$/.test(importPath)) {
return match;
}
// Skip if ends with / (directory import that already has trailing slash)
if (importPath.endsWith('/')) {
return match;
}
const newImportPath = resolveImportPath(importPath, filePath);
return match.replace(importPath, newImportPath);
});
}
async function processFile(filePath) {
const content = await readFile(filePath, 'utf-8');
const newContent = addExtensionsToImports(content, filePath);
if (content !== newContent) {
await writeFile(filePath, newContent, 'utf-8');
const relativePath = relative(buildDir, filePath);
console.log(`Updated imports in ${relativePath}`);
}
}
async function main() {
try {
const files = await getAllJsFiles(buildDir);
await Promise.all(files.map(processFile));
console.log(`Processed ${files.length} files`);
} catch (error) {
console.error('Error processing files:', error);
process.exit(1);
}
}
void main();

View File

@@ -1,26 +0,0 @@
import { existsSync } from 'node:fs';
import { dirname, extname, resolve as nodeResolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const extensions = ['.ts', '.js', '.mts', '.mjs'];
export async function resolve(specifier, context, nextResolve) {
// Only handle relative imports without extensions
if (specifier.startsWith('.') && !extname(specifier)) {
const parentURL = context.parentURL;
if (parentURL) {
const parentPath = new URL(parentURL).pathname;
const parentDir = dirname(parentPath);
// Try extensions in order
for (const ext of extensions) {
const resolvedPath = nodeResolve(parentDir, `${specifier}${ext}`);
if (existsSync(resolvedPath)) {
return nextResolve(pathToFileURL(resolvedPath).href, context);
}
}
}
}
return nextResolve(specifier, context);
}

View File

@@ -41,47 +41,19 @@
"#util/title": "./src/util/title/index.js",
"#util/*": "./src/util/*.ts"
},
"publishConfig": {
"imports": {
"#db": "./build/src/db.js",
"#account-db": "./build/src/account-db.js",
"#load-config": "./build/src/load-config.js",
"#migrations": "./build/src/migrations.js",
"#accounts/*": "./build/src/accounts/*.js",
"#app-gocardless/banks/bank.interface": "./build/src/app-gocardless/banks/bank.interface.js",
"#app-gocardless/banks/*": "./build/src/app-gocardless/banks/*.js",
"#app-gocardless/errors": "./build/src/app-gocardless/errors.js",
"#app-gocardless/gocardless-node.types": "./build/src/app-gocardless/gocardless-node.types.js",
"#app-gocardless/gocardless.types": "./build/src/app-gocardless/gocardless.types.js",
"#app-gocardless/services/*": "./build/src/app-gocardless/services/*.js",
"#app-gocardless/services/tests/*": "./build/src/app-gocardless/services/tests/*.js",
"#app-gocardless/util/*": "./build/src/app-gocardless/util/*.js",
"#app-gocardless/*": "./build/src/app-gocardless/*.js",
"#app-pluggyai/*": "./build/src/app-pluggyai/*.js",
"#app-simplefin/*": "./build/src/app-simplefin/*.js",
"#app-sync/services/*": "./build/src/app-sync/services/*.js",
"#app-sync/*": "./build/src/app-sync/*.js",
"#scripts/*": "./build/src/scripts/*.js",
"#services/*": "./build/src/services/*.js",
"#util/title": "./build/src/util/title/index.js",
"#util/*": "./build/src/util/*.js"
}
},
"scripts": {
"start": "yarn build && node build/app",
"start-monitor": "nodemon --exec 'yarn build && node build/app' --ignore './build/**/*' --ext 'ts,js' build/app",
"build": "tsgo -b && yarn add-import-extensions && yarn copy-static-assets",
"start": "yarn build && node build/app.js",
"start-monitor": "nodemon --exec 'yarn build && node build/app.js' --ignore './build/**/*' --ext 'ts,js' build/app.js",
"build": "vite build",
"typecheck": "tsgo -b && tsc-strict",
"add-import-extensions": "node bin/add-import-extensions.mjs",
"copy-static-assets": "rm -rf build/src/sql && cp -r src/sql build/src/sql",
"test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --import ./register-loader.mjs --trace-warnings' vitest --run",
"db:migrate": "yarn build && cross-env NODE_ENV=development node build/src/scripts/run-migrations.js up",
"db:downgrade": "yarn build && cross-env NODE_ENV=development node build/src/scripts/run-migrations.js down",
"db:test-migrate": "yarn build && cross-env NODE_ENV=test node build/src/scripts/run-migrations.js up",
"db:test-downgrade": "yarn build && cross-env NODE_ENV=test node build/src/scripts/run-migrations.js down",
"reset-password": "yarn build && node build/src/scripts/reset-password.js",
"disable-openid": "yarn build && node build/src/scripts/disable-openid.js",
"health-check": "yarn build && node build/src/scripts/health-check.js"
"test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' vitest --run",
"db:migrate": "yarn build && cross-env NODE_ENV=development node build/scripts/run-migrations.js up",
"db:downgrade": "yarn build && cross-env NODE_ENV=development node build/scripts/run-migrations.js down",
"db:test-migrate": "yarn build && cross-env NODE_ENV=test node build/scripts/run-migrations.js up",
"db:test-downgrade": "yarn build && cross-env NODE_ENV=test node build/scripts/run-migrations.js down",
"reset-password": "yarn build && node build/scripts/reset-password.js",
"disable-openid": "yarn build && node build/scripts/disable-openid.js",
"health-check": "yarn build && node build/scripts/health-check.js"
},
"dependencies": {
"@actual-app/crdt": "workspace:*",
@@ -115,6 +87,7 @@
"nodemon": "^3.1.14",
"supertest": "^7.2.2",
"typescript-strict-plugin": "^2.4.4",
"vite": "^8.0.5",
"vitest": "^4.1.2"
}
}

View File

@@ -1,9 +0,0 @@
import { register } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const loaderPath = resolve(__dirname, 'loader.mjs');
register(pathToFileURL(loaderPath).href, pathToFileURL(__dirname));

View File

@@ -1,22 +1,14 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import IntegrationBank from './banks/integration-bank';
const dirname = path.resolve(fileURLToPath(import.meta.url), '..');
const banksDir = path.resolve(dirname, 'banks');
// Filename convention: <name>_<bic>.{ts,js} (skips bank.interface,
// integration-bank, and any other helper without an underscore).
const bankLoaders = import.meta.glob('./banks/*_*.{ts,js}');
async function loadBanks() {
const bankHandlers = fs
.readdirSync(banksDir)
.filter(filename => filename.includes('_') && filename.endsWith('.js'));
const imports = await Promise.all(
bankHandlers.map(file => {
const fileUrlToBank = pathToFileURL(path.resolve(banksDir, file)); // pathToFileURL for ESM compatibility
return import(fileUrlToBank.toString()).then(handler => handler.default);
}),
Object.values(bankLoaders).map(loader =>
loader().then(handler => handler.default),
),
);
return imports;

View File

@@ -124,17 +124,32 @@ app.get('/metrics', (_req, res) => {
});
});
// The web frontend
// The web frontend.
// Dev mode proxies to Vite, which injects inline preamble scripts and uses
// a websocket for HMR. Loosen script-src and connect-src accordingly.
// `'unsafe-eval'` is required at runtime for the Electron app, so it is
// kept in both branches.
const isDev = process.env.NODE_ENV === 'development';
const scriptSrc = isDev
? "'self' 'unsafe-inline' 'unsafe-eval' blob:"
: "'self' 'unsafe-eval' blob:";
const connectSrc = isDev ? "'self' ws: wss: http: https:" : 'http: https:';
const csp = [
"default-src 'self' blob:",
"img-src 'self' blob: data:",
`script-src ${scriptSrc}`,
"style-src 'self' 'unsafe-inline'",
"font-src 'self' data:",
`connect-src ${connectSrc}`,
].join('; ');
app.use((req, res, next) => {
res.set('Cross-Origin-Opener-Policy', 'same-origin');
res.set('Cross-Origin-Embedder-Policy', 'require-corp');
res.set(
'Content-Security-Policy',
"default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src http: https:;",
);
res.set('Content-Security-Policy', csp);
next();
});
if (process.env.NODE_ENV === 'development') {
if (isDev) {
console.log(
'Running in development mode - Proxying frontend routes to React Dev Server',
);

View File

@@ -21,8 +21,6 @@ const defaultDataDir = process.env.ACTUAL_DATA_DIR
debug(`Project root: '${projectRoot}'`);
export const sqlDir = path.join(__dirname, 'sql');
const actualAppWebBuildPath = path.join(
path.dirname(require.resolve('@actual-app/web/package.json')),
'build',

View File

@@ -1,40 +1,34 @@
import { readdir } from 'node:fs/promises';
import path, { dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
import { load } from 'migrate';
import { config } from './load-config';
type MigrationCallback = (err?: Error) => void;
type MigrationModule = {
up: (next?: MigrationCallback) => void;
down: (next?: MigrationCallback) => void;
};
// Vite resolves this glob at build time and inlines a static map of
// () => import('chunks/...js') calls. Each migration becomes its own chunk.
// Runtime fs reads against a migrations/ directory disappear.
const migrationsLoaders = import.meta.glob<MigrationModule>(
'../migrations/*.{ts,js}',
);
export async function run(direction: 'up' | 'down' = 'up'): Promise<void> {
console.log(
`Checking if there are any migrations to run for direction "${direction}"...`,
);
const __dirname = dirname(fileURLToPath(import.meta.url)); // this directory
const migrationsDir = path.join(__dirname, '../migrations');
try {
// Load all script files in the migrations directory
const files = await readdir(migrationsDir);
const migrationsModules: Record<
string,
{
up: (next?: MigrationCallback) => void;
down: (next?: MigrationCallback) => void;
}
> = {};
const sortedKeys = Object.keys(migrationsLoaders).sort();
const migrationsModules: Record<string, MigrationModule> = {};
for (const f of files
.filter(
f => (f.endsWith('.js') || f.endsWith('.ts')) && !f.endsWith('.d.ts'),
)
.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0))) {
migrationsModules[f] = await import(
pathToFileURL(path.join(migrationsDir, f)).href
);
for (const key of sortedKeys) {
const fileName = key.split('/').pop()!;
migrationsModules[fileName] = await migrationsLoaders[key]();
}
return new Promise<void>((resolve, reject) => {

View File

@@ -1,10 +1,9 @@
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { merkle, SyncProtoBuf, Timestamp } from '@actual-app/crdt';
import { openDatabase } from './db';
import { sqlDir } from './load-config';
import messagesSql from './sql/messages.sql?raw';
import { getPathForGroupFile } from './util/paths';
function getGroupDb(groupId) {
@@ -14,8 +13,7 @@ function getGroupDb(groupId) {
const db = openDatabase(path);
if (needsInit) {
const sql = readFileSync(join(sqlDir, 'messages.sql'), 'utf8');
db.exec(sql);
db.exec(messagesSql);
}
return db;

View File

@@ -0,0 +1,73 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { defineConfig } from 'vite';
import type { Plugin } from 'vite';
const pkg = JSON.parse(
readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'),
);
const shebangPlugin = (entryFile: string): Plugin => ({
name: 'sync-server-shebang',
generateBundle(_options, bundle) {
const chunk = bundle[entryFile];
if (chunk?.type === 'chunk' && !chunk.code.startsWith('#!')) {
chunk.code = `#!/usr/bin/env node\n${chunk.code}`;
}
},
});
export default defineConfig({
ssr: {
target: 'node',
// Inline workspace deps that ship as TS source. Anything else
// (express, better-sqlite3, bcrypt, @actual-app/web, etc.) stays
// external so Node resolves it at runtime.
noExternal: ['@actual-app/crdt'],
},
build: {
ssr: true,
target: 'node22',
outDir: path.resolve(__dirname, 'build'),
emptyOutDir: true,
sourcemap: true,
minify: false,
rollupOptions: {
input: {
app: path.resolve(__dirname, 'app.ts'),
'bin/actual-server': path.resolve(__dirname, 'bin/actual-server.js'),
'scripts/run-migrations': path.resolve(
__dirname,
'src/scripts/run-migrations.js',
),
'scripts/reset-password': path.resolve(
__dirname,
'src/scripts/reset-password.js',
),
'scripts/disable-openid': path.resolve(
__dirname,
'src/scripts/disable-openid.js',
),
'scripts/enable-openid': path.resolve(
__dirname,
'src/scripts/enable-openid.js',
),
'scripts/health-check': path.resolve(
__dirname,
'src/scripts/health-check.js',
),
},
output: {
format: 'esm',
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name]-[hash].js',
},
},
},
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
},
assetsInclude: ['**/*.sql'],
plugins: [shebangPlugin('bin/actual-server.js')],
});

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Refactor module resolution to load `@actual-app/crdt` from source during development.

View File

@@ -203,6 +203,7 @@ __metadata:
pluggy-sdk: "npm:^0.83.0"
supertest: "npm:^7.2.2"
typescript-strict-plugin: "npm:^2.4.4"
vite: "npm:^8.0.5"
vitest: "npm:^4.1.2"
winston: "npm:^3.19.0"
bin: