mirror of
https://github.com/actualbudget/actual.git
synced 2026-07-12 05:00:59 -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>
43 lines
952 B
TypeScript
43 lines
952 B
TypeScript
import { startBackendWorker } from '@actual-app/core/platform/client/backend-worker';
|
|
import { send } from '@actual-app/core/platform/client/connection';
|
|
import type { InitConfig } from '@actual-app/core/server/main';
|
|
|
|
import InlineWorker from './browser-worker?worker&inline';
|
|
|
|
export * from './methods';
|
|
export * as utils from './utils';
|
|
|
|
let worker: Worker | null = null;
|
|
|
|
export async function init(
|
|
config: InitConfig = {},
|
|
): Promise<{ send: typeof send }> {
|
|
worker = new InlineWorker();
|
|
|
|
try {
|
|
await startBackendWorker(worker, config);
|
|
} catch (error) {
|
|
worker.terminate();
|
|
worker = null;
|
|
throw error;
|
|
}
|
|
|
|
return { send };
|
|
}
|
|
|
|
export async function shutdown() {
|
|
if (worker) {
|
|
try {
|
|
await send('sync');
|
|
} catch {
|
|
// most likely that no budget is loaded, so the sync failed
|
|
}
|
|
try {
|
|
await send('close-budget');
|
|
} finally {
|
|
worker.terminate();
|
|
worker = null;
|
|
}
|
|
}
|
|
}
|