mirror of
https://github.com/actualbudget/actual.git
synced 2026-07-21 17:59:54 -05:00
* api: make the browser build work in consumer production bundles
The browser build shipped `new Worker(new URL("./worker.js", import.meta.url))`
in dist/browser.js. A consumer bundler (Vite/Rollup) recognizes that as a worker
entry and re-bundles the already-prebuilt worker.js from scratch, which desyncs
the absurd-sql/connection RPC so `init` throws a structured-clone error. It only
worked in dev and in the package's own e2e because both serve dist verbatim.
Make the browser build fully self-contained instead:
- Inline the worker into browser.js and spawn it from a Blob URL, so consumer
bundlers never see a worker entry to re-bundle. The worker is built as an IIFE
(classic worker), mirroring the web app's kcab.worker.
- Embed the sql.js wasm and the default filesystem data (migrations + default
DB) into the worker so it performs no PUBLIC_URL asset fetches:
- loot-core sqlite gains an opt-in `setWasmBinary` (dormant for web/node);
- the worker installs a scoped fetch shim serving the embedded data/* files
from a sentinel base URL.
- Share the asset-collection logic between the Node disk copy and the embedded
build via scripts/embedded-assets.mjs so the two never drift.
Result: `import '@actual-app/api'` + `init()` works in any bundler with zero
config; only COOP/COEP headers remain required (SharedArrayBuffer).
Adds an e2e (e2e/consumer + e2e/global-setup.mjs) that builds a real consumer
app for production and boots it under COOP/COEP — coverage the verbatim-dist
harness can't provide. Docs note the build is self-contained and that
cross-origin isolation is still required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: address review feedback (guard DOM lookup, revoke blob URL on worker failure)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: address review nitpicks (drop BodyInit cast, reword release note)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: build the browser worker via Vite ?worker&inline
Replace the hand-rolled worker inlining (a custom Vite plugin that read the
prebuilt worker.js off disk and inlined it as a string, plus a second build
config and a build-ordering dependency) with Vite's native `?worker&inline`
import. This collapses the two browser build configs into one, removes the
`virtual:actual-worker-code` module and the manual Blob URL spawn, and drops
the worker-before-facade build step.
Vite's `?worker&inline` sub-build doesn't receive vite-plugin-node-polyfills'
global-shim injection (the plugin writes it to `build.rollupOptions`, but the
worker sub-build reads `worker.rollupOptions`), so the worker crashed on
`process is not defined`. Patch the plugin to also emit the injection on
`worker.rollupOptions` — additive and backwards compatible (the web app build,
including loot-core's nodePolyfills worker, is unaffected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: embed browser assets via Vite, drop the embedded-assets script
Replace scripts/embedded-assets.mjs with Vite-native asset imports in the
worker entry: `?inline` for the sql.js wasm and default DB, `import.meta.glob`
(`?raw`) for the migrations. The worker builds the default-filesystem wire
format from those at module load, so the embedded set comes straight from
loot-core and can't drift.
The Node build only needs migrations + the default DB on disk (it reads them at
runtime); the old script also wrote sql-wasm.wasm and the data/ fetch tree,
which are dead now that the browser worker is self-contained. Replace
writeEmbeddedAssetsToDist with a small closeBundle copy of just those two.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: simplify embedded data map; drop loot-core comment churn
Merge the worker's binData/textData into a single dataFiles map (Response
accepts both bytes and strings), collapsing the fetch shim's two lookups into
one. Revert the unrelated comment edits to loot-core's backend-worker.ts and
fs/index.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] api: rename e2e consumer-dist to consumer/dist; drop a prose comment
Use the standard `dist` directory name for the consumer fixture's build output
(now e2e/consumer/dist), and remove the verbose comment from index.browser.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] loot-core: own the default-filesystem assets; consumers stop reaching in
Add packages/loot-core/default-filesystem.mjs as the single source of truth for
loot-core's runtime assets (sql.js wasm, default DB, migrations) and the
data-file-index wire format, exported as @actual-app/core/default-filesystem.
- @actual-app/api: the browser worker embeds them via the actual-embedded-assets
Vite plugin (which calls collectEmbeddedAssets), replacing the relative
?inline/?raw/import.meta.glob reaches into ../loot-core; the Node build copies
migrations + default DB using the helper's paths. Drops the direct
@jlongster/sql.js devDependency (loot-core resolves the wasm).
- @actual-app/web: stagePublicData copies from the helper's paths instead of
hardcoding loot-core's tree and the sql.js wasm location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [AI] loot-core: scan migrations once when collecting embedded assets
collectEmbeddedAssets() read the migrations directory twice — once in its
own loop and once via buildDataFileIndex(). List the names once and pass
them through, keeping the index wire-format defined in a single place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
defaultDbPath,
|
|
migrationsDir,
|
|
} from '@actual-app/core/default-filesystem';
|
|
import { peggyLoader } from '@actual-app/vite-plugin-peggy';
|
|
import { visualizer } from 'rollup-plugin-visualizer';
|
|
import { defineConfig } from 'vite';
|
|
import { configDefaults } from 'vitest/config';
|
|
|
|
const distDir = path.resolve(__dirname, 'dist');
|
|
const typesDir = path.resolve(__dirname, '@types');
|
|
|
|
function cleanOutputDirs() {
|
|
return {
|
|
name: 'clean-output-dirs',
|
|
buildStart() {
|
|
if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true });
|
|
if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
// The Node build reads migrations + the default DB from disk at runtime (see
|
|
// fs.migrationsPath / bundledDatabasePath in loot-core), so copy them next to
|
|
// the bundle. The browser build embeds its own copies, so nothing else needs to
|
|
// be on disk.
|
|
function copyNodeRuntimeAssets() {
|
|
return {
|
|
name: 'copy-node-runtime-assets',
|
|
closeBundle() {
|
|
fs.cpSync(migrationsDir, path.join(distDir, 'migrations'), {
|
|
recursive: true,
|
|
});
|
|
fs.copyFileSync(defaultDbPath, path.join(distDir, 'default-db.sqlite'));
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
ssr: {
|
|
noExternal: true,
|
|
external: ['better-sqlite3'],
|
|
resolve: { conditions: ['api'] },
|
|
},
|
|
build: {
|
|
ssr: true,
|
|
target: 'node20',
|
|
outDir: distDir,
|
|
emptyOutDir: true,
|
|
sourcemap: true,
|
|
lib: {
|
|
entry: {
|
|
index: path.resolve(__dirname, 'index.ts'),
|
|
models: path.resolve(__dirname, 'models.ts'),
|
|
},
|
|
formats: ['cjs'],
|
|
fileName: (_format, entryName) => `${entryName}.js`,
|
|
},
|
|
},
|
|
plugins: [
|
|
cleanOutputDirs(),
|
|
peggyLoader(),
|
|
copyNodeRuntimeAssets(),
|
|
visualizer({ template: 'raw-data', filename: 'app/stats.json' }),
|
|
],
|
|
resolve: {
|
|
conditions: ['api'],
|
|
},
|
|
test: {
|
|
globals: true,
|
|
// e2e/ holds Playwright tests (yarn e2e), not vitest ones.
|
|
exclude: [...configDefaults.exclude, 'e2e/**'],
|
|
// Each test loads a budget file and runs all DB migrations, which can be
|
|
// slow on busy CI runners; the default 5s timeout is too tight and causes
|
|
// flaky timeouts (and a cascade of unhandled rejections from in-flight work
|
|
// continuing after teardown).
|
|
testTimeout: 20_000,
|
|
hookTimeout: 20_000,
|
|
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
|
// print only console.error
|
|
return type === 'stderr';
|
|
},
|
|
maxWorkers: 2,
|
|
reporters: process.env.CI
|
|
? [
|
|
'default',
|
|
[
|
|
'junit',
|
|
{ outputFile: './test-results/junit.xml', suiteName: 'api' },
|
|
],
|
|
]
|
|
: ['default'],
|
|
},
|
|
});
|