Skip to content

Commit 45f4d61

Browse files
committed
Web: Merge workers, simplify custom build (#1008)
1 parent 57373f9 commit 45f4d61

27 files changed

Lines changed: 481 additions & 447 deletions

.changeset/many-boxes-deny.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@powersync/web': major
3+
---
4+
5+
To reduce setup complexity, the database and sync workers have moved to a single file.
6+
7+
The UMD build for the main library has been removed, import `@powersync/web` directly and
8+
remove import mappings pointing to `@powersync/web/umd`.
9+
10+
If you were using the pre-bundled sync or web workers, run `powersync-web copy-assets` again
11+
and update URLs from `/@powersync/worker/WASQLiteDB.umd.js` and `/@powersync/worker/SharedSyncImplementation.umd.js` to `/@powersync/worker.js`.

demos/angular-supabase-todolist/src/app/powersync.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,17 @@ export class PowerSyncService {
6666
dbFilename: 'test.db',
6767
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
6868
// Specify the path to the worker script
69-
worker: 'assets/@powersync/worker/WASQLiteDB.umd.js',
69+
worker: 'assets/@powersync/worker.js',
7070
databaseWorkerLogLevel: LogLevels.debug
7171
}
7272
});
7373

7474
this.db = new PowerSyncDatabase({
7575
schema: AppSchema,
7676
factory,
77-
7877
sync: {
7978
// Specify the path to the worker script
80-
worker: 'assets/@powersync/worker/SharedSyncImplementation.umd.js'
79+
worker: 'assets/@powersync/worker.js'
8180
}
8281
});
8382
}

demos/example-nextjs/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
"lint": "next lint",
1010
"clean": "rm -rf .next",
1111
"generate-keys": "node --experimental-strip-types scripts/generate-keys.mts",
12-
"copy-assets": "powersync-web copy-assets -o public",
13-
"postinstall": "pnpm copy-assets",
1412
"local:up": "powersync docker start --directory powersync",
1513
"local:down": "powersync docker stop --directory powersync",
1614
"test:build": "pnpm build"

demos/example-nextjs/src/library/powersync/powersync-provider.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@ function getDB(): PowerSyncDatabase {
1818
logger,
1919
open: {
2020
dbFilename: 'powersync-nextjs.db',
21-
worker: '/@powersync/worker/WASQLiteDB.umd.js',
2221
databaseWorkerLogLevel: LogLevels.debug,
2322
disableSSRWarning: true
2423
}
2524
}),
2625
schema: AppSchema,
27-
sync: { worker: '/@powersync/worker/SharedSyncImplementation.umd.js' },
2826
logger
2927
});
3028

demos/react-native-web-supabase-todolist/library/powersync/system.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import '@azure/core-asynciterator-polyfill';
22

33
import React from 'react';
4-
import { PowerSyncDatabase as PowerSyncDatabaseNative, AbstractPowerSyncDatabase } from '@powersync/react-native';
4+
import { PowerSyncDatabase as PowerSyncDatabaseNative } from '@powersync/react-native';
55
import {
66
PowerSyncDatabase as PowerSyncDatabaseWeb,
77
WASQLiteOpenFactory,
@@ -11,6 +11,7 @@ import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-stor
1111
import {
1212
type AttachmentRecord,
1313
AttachmentQueue,
14+
CommonPowerSyncDatabase,
1415
createConsoleLogger,
1516
LogLevels,
1617
WatchedAttachmentItem
@@ -27,7 +28,7 @@ const logger = createConsoleLogger({ minLevel: LogLevels.debug });
2728
export class System {
2829
kvStorage: ExpoKVStorage | WebKVStorage;
2930
supabaseConnector: SupabaseConnector;
30-
powersync: AbstractPowerSyncDatabase;
31+
powersync: CommonPowerSyncDatabase;
3132
photoAttachmentQueue: AttachmentQueue | undefined = undefined;
3233

3334
constructor() {
@@ -51,17 +52,17 @@ export class System {
5152
open: {
5253
dbFilename: 'sqlite.db',
5354
// You can specify a path to the db worker
54-
worker: '/@powersync/worker/WASQLiteDB.umd.js'
55+
worker: '/@powersync/worker.js'
5556

5657
// Or provide a factory function to create the worker.
5758
// The worker name should be unique for the database filename to avoid conflicts if multiple clients with different databases are present.
5859
// worker: (options) => {
5960
// if (options?.flags?.enableMultiTabs) {
60-
// return new SharedWorker(`/@powersync/worker/WASQLiteDB.umd.js`, {
61+
// return new SharedWorker(`/@powersync/worker.js`, {
6162
// name: `shared-DB-worker-${options?.dbFilename}`
6263
// });
6364
// } else {
64-
// return new Worker(`/@powersync/worker/WASQLiteDB.umd.js`, {
65+
// return new Worker(`/@powersync/worker.js`, {
6566
// name: `DB-worker-${options?.dbFilename}`
6667
// });
6768
// }
@@ -73,12 +74,12 @@ export class System {
7374
factory: factory,
7475
sync: {
7576
// You can specify a path to the sync worker
76-
worker: '/@powersync/worker/SharedSyncImplementation.umd.js'
77+
worker: '/@powersync/worker.js'
7778

7879
// Or provide a factory function to create the worker.
7980
// The worker name should be unique for the database filename to avoid conflicts if multiple clients with different databases are present.
8081
// worker: (options) => {
81-
// return new SharedWorker(`/@powersync/worker/SharedSyncImplementation.umd.js`, {
82+
// return new SharedWorker(`/@powersync/worker.js`, {
8283
// name: `shared-sync-${options?.dbFilename}`
8384
// });
8485
// }

demos/react-native-web-supabase-todolist/metro.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ const config = getDefaultConfig(__dirname);
55
// Needed to make `@powersync/web/umd` imports work
66
config.resolver.unstable_enablePackageExports = true;
77

8+
// Expo removes the react-native export condition for React Native web: https://github.com/expo/expo/blob/874ddfc86c0bdaeeb43f56b4386d271ba869ad47/packages/%40expo/metro-config/src/ExpoMetroConfig.ts#L309
9+
// The PowerSync web SKD needs to be resolved with an RN-specific export condition to work with Metro.
10+
config.resolver.unstable_conditionsByPlatform.web.push('react-native-web');
11+
812
config.resolver.resolveRequest = (context, moduleName, platform) => {
913
if (platform === 'web') {
1014
if (['react-native-prompt-android', '@powersync/react-native'].includes(moduleName)) {
1115
return {
1216
type: 'empty'
1317
};
1418
}
15-
const mapping = { 'react-native': 'react-native-web', '@powersync/web': '@powersync/web/umd' };
19+
const mapping = { 'react-native': 'react-native-web' };
1620
if (mapping[moduleName]) {
1721
console.log('remapping', moduleName);
1822
return context.resolveRequest(context, mapping[moduleName], platform);

packages/shared-internals/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
"@types/uuid": "catalog:",
5656
"buffer": "^6.0.3",
5757
"cross-fetch": "^4.1.0",
58-
"estree-walker": "^3.0.3",
59-
"magic-string": "^0.30.21",
58+
"estree-walker": "catalog:",
59+
"magic-string": "catalog:",
6060
"rollup": "catalog:",
6161
"rollup-plugin-dts": "catalog:",
6262
"rsocket-core": "1.0.0-alpha.3",

packages/web/bin/powersync.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ program
1313
.description('Copy assets to the specified output directory')
1414
.option('-o, --output <directory>', 'output directory for assets', 'public')
1515
.action(async (options) => {
16-
const resolvedPath = require.resolve('@powersync/web/umd');
16+
const resolvedPath = require.resolve('@powersync/web/bundled_worker');
1717
const outputDir = options.output;
1818

1919
console.log(`Resolved input path: ${resolvedPath}`);

packages/web/package.json

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"name": "@powersync/web",
33
"version": "1.38.6",
44
"description": "PowerSync Web SDK",
5-
"main": "lib/src/index.js",
6-
"module": "lib/src/index.js",
5+
"main": "lib/index.js",
6+
"module": "lib/index.js",
77
"type": "module",
8-
"types": "lib/src/index.d.ts",
8+
"types": "lib/index.d.ts",
99
"bin": {
1010
"powersync-web": "bin/powersync.cjs"
1111
},
@@ -18,22 +18,14 @@
1818
],
1919
"exports": {
2020
".": {
21-
"types": "./lib/src/index.d.ts",
22-
"default": "./lib/src/index.js"
21+
"types": "./lib/index.d.ts",
22+
"react-native-web": "./dist/index.react_native_web.js",
23+
"react-native": "./dist/index.react_native_web.js",
24+
"default": "./lib/index.js"
2325
},
24-
"./umd": {
25-
"types": "./lib/src/index.d.ts",
26-
"default": "./dist/index.umd.js"
27-
},
28-
"./umd/worker/db": {
29-
"types": "./lib/src/index.d.ts",
30-
"import": "./dist/worker/WASQLiteDB.umd.js",
31-
"require": "./dist/worker/WASQLiteDB.umd.js"
32-
},
33-
"./umd/worker/sync": {
34-
"types": "./lib/src/index.d.ts",
35-
"import": "./dist/worker/SharedSyncImplementation.umd.js",
36-
"require": "./dist/worker/SharedSyncImplementation.umd.js"
26+
"./bundled_worker": {
27+
"types": "./lib/worker/worker.d.ts",
28+
"default": "./dist/worker/worker.js"
3729
}
3830
},
3931
"repository": "https://github.com/powersync-ja/powersync-js",
@@ -47,10 +39,8 @@
4739
"homepage": "https://docs.powersync.com",
4840
"scripts": {
4941
"build:tsc": "tsc --build",
50-
"build:webpack-main": "webpack",
51-
"build:webpack-workers": "webpack --config webpack.workers.config.js",
52-
"build": "pnpm run build:tsc && pnpm run build:webpack-main && pnpm run build:webpack-workers",
53-
"build:prod": "pnpm run build:tsc --sourceMap false && pnpm run build:webpack-main && pnpm run build:webpack-workers",
42+
"build": "pnpm run build:tsc && rollup --config",
43+
"build:prod": "pnpm run build",
5444
"clean": "rm -rf lib dist tsconfig.tsbuildinfo",
5545
"watch": "tsc --build -w",
5646
"test": "pnpm build && vitest",
@@ -83,11 +73,12 @@
8373
"glob": "catalog:",
8474
"p-defer": "catalog:",
8575
"source-map-loader": "^5.0.0",
86-
"terser-webpack-plugin": "^5.3.9",
8776
"uuid": "catalog:",
8877
"vite": "catalog:",
89-
"webpack": "^5.90.1",
90-
"webpack-cli": "^5.1.4",
91-
"webpack-node-externals": "^3.0.0"
78+
"rollup": "catalog:",
79+
"@rollup/plugin-node-resolve": "catalog:",
80+
"@rollup/plugin-terser": "catalog:",
81+
"estree-walker": "catalog:",
82+
"magic-string": "catalog:"
9283
}
9384
}

packages/web/rollup.config.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import * as fs from 'node:fs/promises';
2+
import * as path from 'node:path';
3+
import { fileURLToPath, pathToFileURL } from 'node:url';
4+
5+
import type { RollupOptions, Plugin } from 'rollup';
6+
import type { Node } from 'estree-walker';
7+
import { asyncWalk, walk } from 'estree-walker';
8+
import MagicString from 'magic-string';
9+
10+
import nodeResolve from '@rollup/plugin-node-resolve';
11+
import terser from '@rollup/plugin-terser';
12+
13+
const workerFileUri: Plugin = {
14+
// In non-modular workers, the script URL is always globalThis.location.href.
15+
name: 'workerFileUrl',
16+
resolveFileUrl({ relativePath }) {
17+
return `new URL('${relativePath}', globalThis.location.href)`;
18+
}
19+
};
20+
21+
function bundleWorker(): RollupOptions {
22+
const plugins = [nodeResolve({ preferBuiltins: false, browser: true }), includeUriBundles(), workerFileUri, terser()];
23+
24+
return {
25+
input: 'lib/worker/worker.js',
26+
output: {
27+
dir: `dist/worker/`,
28+
format: 'esm',
29+
sourcemap: true
30+
},
31+
plugins
32+
};
33+
}
34+
35+
function bundledModuleForReactNativeWeb(): RollupOptions {
36+
return {
37+
input: 'lib/index.js',
38+
output: {
39+
file: 'dist/index.react_native_web.js',
40+
format: 'esm',
41+
sourcemap: true
42+
},
43+
external: ['@powersync/common', '@powersync/shared-internals', 'comlink'],
44+
plugins: disableDefaultWorkers()
45+
};
46+
}
47+
48+
const options: RollupOptions[] = [bundledModuleForReactNativeWeb(), bundleWorker()];
49+
50+
function disableDefaultWorkers(): Plugin {
51+
return {
52+
name: 'disableSpawnDefaultPowerSyncWorker',
53+
transform(code, id) {
54+
if (!code.includes('function spawnDefaultPowerSyncWorker')) return;
55+
56+
// In the build for React Native web, import.meta.url must not appear anywhere in the bundled output as Metro does
57+
// not generate ESM modules.
58+
// For details, see the comment on spawnDefaultPowerSyncWorker in src/worker/client.ts.
59+
const ms = new MagicString(code);
60+
61+
const ast = this.parse(code);
62+
walk(ast, {
63+
enter(node) {
64+
if (node.type === 'FunctionDeclaration' && node.id.name === 'spawnDefaultPowerSyncWorker') {
65+
const body = node.body;
66+
ms.overwrite(
67+
(body as any).start,
68+
(body as any).end,
69+
`{
70+
throw new Error('You are using the React Native web build of the PowerSync SDK, which requires custom worker URLs. Please see the documentation at https://docs.powersync.com/client-sdks/frameworks/react-native-web-support for details.');
71+
}
72+
`
73+
);
74+
this.skip();
75+
}
76+
}
77+
});
78+
79+
return {
80+
code: ms.toString(),
81+
map: ms.generateMap()
82+
};
83+
}
84+
};
85+
}
86+
87+
// Plugin that replaces `new URL(x, import.meta.url)` with a resolved asset URL.
88+
//
89+
// We need to do this to ensure WebAssembly modules referenced by @journeyapps/wa-sqlite are copied into dist/.
90+
function includeUriBundles(): Plugin {
91+
function isImportMetaUrl(node: Node): boolean {
92+
if (node.type !== 'MemberExpression') return false;
93+
94+
const { object, property } = node;
95+
if (property.type !== 'Identifier' || property.name !== 'url') return false;
96+
97+
return object.type === 'MetaProperty' && object.meta.name === 'import' && object.property.name === 'meta';
98+
}
99+
100+
function extractUriImport(node: Node): string | undefined {
101+
// Do we have a new URI(x, y) expression?
102+
if (
103+
node.type != 'NewExpression' ||
104+
node.callee.type !== 'Identifier' ||
105+
node.callee.name != 'URL' ||
106+
node.arguments.length != 2
107+
) {
108+
return;
109+
}
110+
111+
const [importDesc, base] = node.arguments;
112+
if (importDesc.type !== 'Literal' || typeof (importDesc as any).value !== 'string') {
113+
return;
114+
}
115+
if (!isImportMetaUrl(base)) {
116+
return;
117+
}
118+
return (importDesc as any).value as string;
119+
}
120+
121+
return {
122+
name: 'includeUriBundles',
123+
async transform(code, id) {
124+
if (!code.includes('import.meta.url')) return;
125+
126+
let ms: MagicString | undefined;
127+
128+
const parsed = this.parse(code);
129+
const plugin = this;
130+
await asyncWalk(parsed, {
131+
async enter(node) {
132+
const importedUri = extractUriImport(node);
133+
if (importedUri != null) {
134+
const resolvedUrl = new URL(importedUri, pathToFileURL(id));
135+
const resolvedPath = fileURLToPath(resolvedUrl);
136+
137+
// See https://rollupjs.org/plugin-development/#file-urls
138+
const referenceId = plugin.emitFile({
139+
type: 'asset',
140+
name: path.basename(resolvedPath),
141+
source: await fs.readFile(resolvedPath)
142+
});
143+
144+
ms ??= new MagicString(code);
145+
const start = (node as any).start as number;
146+
const end = (node as any).end as number;
147+
ms.overwrite(start, end, `import.meta.ROLLUP_FILE_URL_OBJ_${referenceId}`);
148+
this.skip();
149+
}
150+
}
151+
});
152+
153+
if (ms) {
154+
return {
155+
code: ms.toString(),
156+
map: ms.generateMap()
157+
};
158+
}
159+
}
160+
};
161+
}
162+
163+
export default options;

0 commit comments

Comments
 (0)