pgserve 1.1.3-rc.2 → 1.1.3-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,26 +38,26 @@ concurrency:
38
38
  jobs:
39
39
  build:
40
40
  name: Build ${{ matrix.platform }}
41
- runs-on: ubuntu-latest
41
+ runs-on: ${{ matrix.os }}
42
42
  strategy:
43
43
  fail-fast: false
44
44
  matrix:
45
45
  include:
46
- - target: bun-linux-x64
47
- platform: linux-x64
46
+ # Linux x64
47
+ - platform: linux-x64
48
+ pg_pkg: linux-x64
48
49
  output: pgserve-linux-x64
49
- - target: bun-linux-arm64
50
- platform: linux-arm64
51
- output: pgserve-linux-arm64
52
- - target: bun-darwin-x64
53
- platform: darwin-x64
54
- output: pgserve-darwin-x64
55
- - target: bun-darwin-arm64
56
- platform: darwin-arm64
50
+ os: ubuntu-latest
51
+ # macOS ARM64 (Apple Silicon) - must build on macOS for native binaries
52
+ - platform: darwin-arm64
53
+ pg_pkg: darwin-arm64
57
54
  output: pgserve-darwin-arm64
58
- - target: bun-windows-x64
59
- platform: windows-x64
55
+ os: macos-latest
56
+ # Windows x64 - must build on Windows for --windows-icon
57
+ - platform: windows-x64
58
+ pg_pkg: windows-x64
60
59
  output: pgserve-windows-x64.exe
60
+ os: windows-latest
61
61
 
62
62
  steps:
63
63
  - name: Checkout
@@ -73,10 +73,33 @@ jobs:
73
73
  - name: Install dependencies
74
74
  run: bun install
75
75
 
76
+ # Bundle PostgreSQL binaries for standalone exe
77
+ - name: Bundle PostgreSQL binaries
78
+ run: |
79
+ echo "Installing @embedded-postgres/${{ matrix.pg_pkg }}..."
80
+ npm install @embedded-postgres/${{ matrix.pg_pkg }}
81
+ mkdir -p embedded-postgres
82
+ cp -r node_modules/@embedded-postgres/${{ matrix.pg_pkg }}/native/* embedded-postgres/
83
+ echo "Bundled PostgreSQL binaries:"
84
+ ls -la embedded-postgres/
85
+ ls -la embedded-postgres/bin/ || true
86
+ shell: bash
87
+
88
+ # Windows: native build with custom icon
89
+ - name: Build for Windows (with icon)
90
+ if: matrix.platform == 'windows-x64'
91
+ run: |
92
+ mkdir -p dist
93
+ bun build --compile --windows-icon=assets/icon.ico bin/pglite-server.js --outfile dist/${{ matrix.output }}
94
+ ls -lh dist/
95
+ shell: bash
96
+
97
+ # Linux/macOS: native build
76
98
  - name: Build for ${{ matrix.platform }}
99
+ if: matrix.platform != 'windows-x64'
77
100
  run: |
78
101
  mkdir -p dist
79
- bun build --compile --target=${{ matrix.target }} bin/pglite-server.js --outfile dist/${{ matrix.output }}
102
+ bun build --compile bin/pglite-server.js --outfile dist/${{ matrix.output }}
80
103
  ls -lh dist/
81
104
 
82
105
  - name: Upload artifact
@@ -129,7 +152,8 @@ jobs:
129
152
  ls -la dist/
130
153
 
131
154
  MISSING=""
132
- for platform in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
155
+ # Supported platforms: linux-x64, darwin-arm64, windows-x64
156
+ for platform in linux-x64 darwin-arm64; do
133
157
  if [ ! -f "dist/pgserve-$platform" ]; then
134
158
  MISSING="$MISSING pgserve-$platform"
135
159
  fi
package/README.md CHANGED
@@ -99,6 +99,17 @@ npm install pgserve
99
99
 
100
100
  > PostgreSQL binaries are automatically downloaded on first run (~100MB).
101
101
 
102
+ ### Windows
103
+
104
+ Download `pgserve-windows-x64.exe` from [GitHub Releases](https://github.com/namastexlabs/pgserve/releases).
105
+
106
+ Double-click to run, or use CLI:
107
+
108
+ ```cmd
109
+ pgserve-windows-x64.exe --port 5432
110
+ pgserve-windows-x64.exe --data C:\pgserve-data
111
+ ```
112
+
102
113
  <br>
103
114
 
104
115
  ## CLI Reference
Binary file
@@ -55,63 +55,61 @@ if (!bunPath) {
55
55
 
56
56
  const scriptPath = path.join(__dirname, 'pglite-server.js');
57
57
 
58
- // Spawn bun with the actual script, inherit all stdio
59
- // IMPORTANT: Do NOT use detached mode - wrapper must wait for child to fully terminate
60
- // Using detached with stdio:'inherit' causes file handle inheritance issues on Windows
61
- const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
62
- stdio: 'inherit',
63
- windowsHide: true
64
- });
65
-
66
- child.on('error', (err) => {
67
- console.error('Failed to start pgserve:', err.message);
68
- process.exit(1);
69
- });
58
+ // Platform-specific spawning strategy:
59
+ // - Windows: Use pipes for explicit handle control (prevents EBUSY errors)
60
+ // - Unix: Use inherit for simplicity (works fine)
70
61
 
71
- // Safety timeout: force exit if 'close' event never fires after 'exit'
72
- let forceExitTimeout = null;
62
+ if (isWindows) {
63
+ // WINDOWS PATH: Explicit pipe control to prevent EBUSY errors
64
+ // Using stdio: 'inherit' causes file handle inheritance that we cannot release,
65
+ // leading to npm cleanup failures. With pipes, we control when handles are destroyed.
73
66
 
74
- child.on('exit', () => {
75
- // Give 5 seconds for 'close' event after 'exit'
76
- forceExitTimeout = setTimeout(() => {
77
- console.error('Warning: Child process did not close cleanly, forcing exit');
78
- process.exit(1);
79
- }, 5000);
80
- });
81
-
82
- // Use 'close' event instead of 'exit' - fires AFTER all stdio streams are closed
83
- // This is critical for Windows where file handles may remain locked after 'exit' fires
84
- child.on('close', (code, signal) => {
85
- // Clear the safety timeout
86
- if (forceExitTimeout) {
87
- clearTimeout(forceExitTimeout);
88
- }
67
+ const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ windowsHide: true
70
+ });
89
71
 
90
- // On Windows, use SYNCHRONOUS delay to ensure all file handles are released
91
- // This prevents EBUSY errors when npx tries to clean up the cache
92
- // NOTE: async/await does NOT work in EventEmitter callbacks - Node ignores the Promise
93
- if (isWindows) {
94
- const delay = 200; // ms - enough for Windows kernel to release handles
95
- const start = Date.now();
96
- while (Date.now() - start < delay) {
97
- // Synchronous busy-wait - actually blocks unlike async setTimeout
98
- }
72
+ // Manually pipe stdio - we now control the handles
73
+ // Handle stdin errors gracefully (may not be connected in some environments)
74
+ process.stdin.on('error', () => {});
75
+ child.stdin.on('error', () => {});
76
+
77
+ // Only pipe stdin if it's readable
78
+ if (process.stdin.readable) {
79
+ process.stdin.pipe(child.stdin);
99
80
  }
81
+ child.stdout.pipe(process.stdout);
82
+ child.stderr.pipe(process.stderr);
100
83
 
101
- if (signal) {
102
- // On Windows, can't reliably re-raise Unix signals
103
- if (isWindows) {
104
- process.exit(1);
105
- } else {
106
- process.kill(process.pid, signal);
84
+ child.on('error', (err) => {
85
+ console.error('Failed to start pgserve:', err.message);
86
+ process.exit(1);
87
+ });
88
+
89
+ child.on('close', (code, signal) => {
90
+ // CRITICAL: Explicitly destroy ALL streams to release file handles
91
+ // This must happen BEFORE process.exit() to prevent EBUSY
92
+ try {
93
+ if (process.stdin.readable) {
94
+ process.stdin.unpipe(child.stdin);
95
+ }
96
+ child.stdin.destroy();
97
+ child.stdout.destroy();
98
+ child.stderr.destroy();
99
+ } catch {
100
+ // Ignore stream destruction errors
107
101
  }
108
- } else {
109
- process.exit(code ?? 0);
110
- }
111
- });
112
102
 
113
- // Platform-specific signal handling
114
- if (isWindows) {
103
+ // Remove all listeners to prevent memory leaks
104
+ child.removeAllListeners();
105
+
106
+ // Use setImmediate to ensure stream destruction completes before exit
107
+ // This gives the event loop one tick to process pending I/O cleanup
108
+ setImmediate(() => {
109
+ process.exit(signal ? 1 : (code ?? 0));
110
+ });
111
+ });
112
+
115
113
  // Windows: use taskkill for reliable process termination
116
114
  // process.kill(pid, 'SIGINT') does NOT work properly on Windows
117
115
  process.on('SIGINT', () => {
@@ -139,13 +137,37 @@ if (isWindows) {
139
137
  rl.on('SIGINT', () => {
140
138
  process.emit('SIGINT');
141
139
  });
140
+ // Clean up readline on close
141
+ child.on('close', () => {
142
+ rl.close();
143
+ });
142
144
  }
145
+
143
146
  } else {
147
+ // UNIX PATH: Simple stdio inheritance (works fine, no EBUSY issues)
148
+ const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
149
+ stdio: 'inherit',
150
+ windowsHide: true
151
+ });
152
+
153
+ child.on('error', (err) => {
154
+ console.error('Failed to start pgserve:', err.message);
155
+ process.exit(1);
156
+ });
157
+
158
+ child.on('close', (code, signal) => {
159
+ if (signal) {
160
+ process.kill(process.pid, signal);
161
+ } else {
162
+ process.exit(code ?? 0);
163
+ }
164
+ });
165
+
144
166
  // Unix: forward signals to child process normally
145
- ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
146
- process.on(signal, () => {
167
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(sig => {
168
+ process.on(sig, () => {
147
169
  if (child.pid) {
148
- process.kill(child.pid, signal);
170
+ process.kill(child.pid, sig);
149
171
  }
150
172
  });
151
173
  });
package/bun.lock CHANGED
@@ -20,7 +20,7 @@
20
20
  "@embedded-postgres/darwin-arm64": "17.7.0-beta.15",
21
21
  "@embedded-postgres/darwin-x64": "17.7.0-beta.15",
22
22
  "@embedded-postgres/linux-x64": "17.7.0-beta.15",
23
- "@embedded-postgres/win32-x64": "17.7.0-beta.15",
23
+ "@embedded-postgres/windows-x64": "17.7.0-beta.15",
24
24
  },
25
25
  },
26
26
  },
@@ -33,6 +33,8 @@
33
33
 
34
34
  "@embedded-postgres/linux-x64": ["@embedded-postgres/linux-x64@17.7.0-beta.15", "", { "os": "linux", "cpu": "x64" }, "sha512-HeaxSHsw6ccVh8l5iC4OgXqvaaCGWnnZR9CpgNgrAfnKPPGiEhUPBmO2XhEsFQIhc+ad/+36h0NTvKo4bdi40w=="],
35
35
 
36
+ "@embedded-postgres/windows-x64": ["@embedded-postgres/windows-x64@17.7.0-beta.15", "", { "os": "win32", "cpu": "x64" }, "sha512-Oq11yyKxISjefuYdKljcp3Q+uxx237zn9YpP9hO43+6Feorq7USuMIDqk5ofLSQ30FAnVyTqaIQK8ZIjW+tQXQ=="],
37
+
36
38
  "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
37
39
 
38
40
  "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.3-rc.2",
3
+ "version": "1.1.3-rc.6",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "@embedded-postgres/darwin-arm64": "17.7.0-beta.15",
45
45
  "@embedded-postgres/darwin-x64": "17.7.0-beta.15",
46
46
  "@embedded-postgres/linux-x64": "17.7.0-beta.15",
47
- "@embedded-postgres/win32-x64": "17.7.0-beta.15"
47
+ "@embedded-postgres/windows-x64": "17.7.0-beta.15"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@electric-sql/pglite": "^0.2.17",
package/src/postgres.js CHANGED
@@ -18,6 +18,124 @@ import path from 'path';
18
18
  import fs from 'fs';
19
19
  import crypto from 'crypto';
20
20
 
21
+ /**
22
+ * Get platform key for binary lookup (e.g., 'windows-x64', 'linux-x64', 'darwin-arm64')
23
+ * @returns {string} Platform key
24
+ */
25
+ function getPlatformKey() {
26
+ const platform = os.platform();
27
+ const arch = os.arch();
28
+
29
+ if (platform === 'win32') return 'windows-x64';
30
+ if (platform === 'linux' && arch === 'x64') return 'linux-x64';
31
+ if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64';
32
+ if (platform === 'darwin' && arch === 'x64') return 'darwin-x64';
33
+ if (platform === 'linux' && arch === 'arm64') return 'linux-arm64';
34
+
35
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
36
+ }
37
+
38
+ /**
39
+ * Get the directory where extracted binaries are cached
40
+ * @returns {string} Cache directory path
41
+ */
42
+ function getBinaryCacheDir() {
43
+ const platformKey = getPlatformKey();
44
+ return path.join(os.homedir(), '.pgserve', 'bin', platformKey);
45
+ }
46
+
47
+ /**
48
+ * Check if bundled PostgreSQL binaries exist (standalone exe mode)
49
+ * When compiled with `bun build --compile`, the embedded-postgres folder
50
+ * should be bundled alongside the executable.
51
+ * @returns {string|null} Path to bundled binaries or null if not found
52
+ */
53
+ function getBundledBinaryDir() {
54
+ // Check for embedded-postgres folder relative to the module
55
+ // This works when binaries are bundled during build
56
+ const bundledDir = path.join(import.meta.dirname, '..', 'embedded-postgres');
57
+ if (fs.existsSync(path.join(bundledDir, 'bin'))) {
58
+ return bundledDir;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Extract bundled PostgreSQL binaries to cache directory.
65
+ * Bun-compiled binaries can read embedded files but cannot execute them directly.
66
+ * We extract to ~/.pgserve/bin/{platform}/ for execution.
67
+ *
68
+ * @returns {Promise<string>} Path to extracted bin directory
69
+ */
70
+ async function extractBundledBinaries() {
71
+ const platform = os.platform();
72
+ const cacheDir = getBinaryCacheDir();
73
+ const cacheBinDir = path.join(cacheDir, 'bin');
74
+ const initdbName = platform === 'win32' ? 'initdb.exe' : 'initdb';
75
+ const postgresName = platform === 'win32' ? 'postgres.exe' : 'postgres';
76
+
77
+ // Check if already extracted
78
+ if (fs.existsSync(path.join(cacheBinDir, initdbName)) &&
79
+ fs.existsSync(path.join(cacheBinDir, postgresName))) {
80
+ return cacheDir;
81
+ }
82
+
83
+ // Get bundled directory
84
+ const bundledDir = getBundledBinaryDir();
85
+ if (!bundledDir) {
86
+ return null;
87
+ }
88
+
89
+ console.log('[pgserve] Extracting PostgreSQL binaries...');
90
+
91
+ // Create cache directory
92
+ fs.mkdirSync(cacheDir, { recursive: true });
93
+
94
+ // Copy bundled binaries to cache
95
+ // Use recursive copy to get bin/, lib/, share/ directories
96
+ await copyRecursive(bundledDir, cacheDir);
97
+
98
+ // Make executables executable (Unix only)
99
+ if (platform !== 'win32') {
100
+ const binDir = path.join(cacheDir, 'bin');
101
+ const files = fs.readdirSync(binDir);
102
+ for (const file of files) {
103
+ const filePath = path.join(binDir, file);
104
+ try {
105
+ fs.chmodSync(filePath, 0o755);
106
+ } catch {
107
+ // Ignore permission errors for non-executables
108
+ }
109
+ }
110
+ }
111
+
112
+ console.log(`[pgserve] Binaries extracted to ${cacheDir}`);
113
+ return cacheDir;
114
+ }
115
+
116
+ /**
117
+ * Recursively copy a directory
118
+ * @param {string} src - Source directory
119
+ * @param {string} dest - Destination directory
120
+ */
121
+ async function copyRecursive(src, dest) {
122
+ const entries = fs.readdirSync(src, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ const srcPath = path.join(src, entry.name);
126
+ const destPath = path.join(dest, entry.name);
127
+
128
+ if (entry.isDirectory()) {
129
+ fs.mkdirSync(destPath, { recursive: true });
130
+ await copyRecursive(srcPath, destPath);
131
+ } else {
132
+ // Read and write file (works with Bun's virtual filesystem)
133
+ const content = fs.readFileSync(srcPath);
134
+ fs.writeFileSync(destPath, content);
135
+ }
136
+ }
137
+ }
138
+
21
139
  /**
22
140
  * Ensure library symlinks exist in the lib directory.
23
141
  * The @embedded-postgres package ships versioned libraries but binaries look for soname versions.
@@ -94,10 +212,47 @@ function ensureLibrarySymlinks(libDir, platform) {
94
212
  }
95
213
 
96
214
  // Resolve binary paths from embedded-postgres platform packages
97
- function getBinaryPaths() {
215
+ // Now async to support bundled binary extraction
216
+ async function getBinaryPaths() {
98
217
  const platform = os.platform();
99
218
  const arch = os.arch();
219
+ const exeSuffix = platform === 'win32' ? '.exe' : '';
220
+
221
+ // Priority 1: Check extracted cache directory (standalone exe mode)
222
+ // This is where bundled binaries are extracted on first run
223
+ const cacheDir = getBinaryCacheDir();
224
+ const cacheBinDir = path.join(cacheDir, 'bin');
225
+ const cachedInitdb = path.join(cacheBinDir, 'initdb' + exeSuffix);
226
+ const cachedPostgres = path.join(cacheBinDir, 'postgres' + exeSuffix);
227
+
228
+ if (fs.existsSync(cachedInitdb) && fs.existsSync(cachedPostgres)) {
229
+ const libDir = path.join(cacheDir, 'lib');
230
+ if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(libDir)) {
231
+ ensureLibrarySymlinks(libDir, platform);
232
+ }
233
+ return { initdb: cachedInitdb, postgres: cachedPostgres, binDir: cacheBinDir, libDir };
234
+ }
235
+
236
+ // Priority 2: Check for bundled binaries and extract if found (standalone exe mode)
237
+ const bundledDir = getBundledBinaryDir();
238
+ if (bundledDir) {
239
+ const extractedDir = await extractBundledBinaries();
240
+ if (extractedDir) {
241
+ const extractedBinDir = path.join(extractedDir, 'bin');
242
+ const extractedInitdb = path.join(extractedBinDir, 'initdb' + exeSuffix);
243
+ const extractedPostgres = path.join(extractedBinDir, 'postgres' + exeSuffix);
244
+
245
+ if (fs.existsSync(extractedInitdb) && fs.existsSync(extractedPostgres)) {
246
+ const libDir = path.join(extractedDir, 'lib');
247
+ if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(libDir)) {
248
+ ensureLibrarySymlinks(libDir, platform);
249
+ }
250
+ return { initdb: extractedInitdb, postgres: extractedPostgres, binDir: extractedBinDir, libDir };
251
+ }
252
+ }
253
+ }
100
254
 
255
+ // Priority 3: Find the package in node_modules (npm install case)
101
256
  let pkgName;
102
257
  if (platform === 'linux' && arch === 'x64') {
103
258
  pkgName = '@embedded-postgres/linux-x64';
@@ -106,12 +261,11 @@ function getBinaryPaths() {
106
261
  } else if (platform === 'darwin' && arch === 'x64') {
107
262
  pkgName = '@embedded-postgres/darwin-x64';
108
263
  } else if (platform === 'win32' && arch === 'x64') {
109
- pkgName = '@embedded-postgres/win32-x64';
264
+ pkgName = '@embedded-postgres/windows-x64';
110
265
  } else {
111
266
  throw new Error(`Unsupported platform: ${platform}-${arch}`);
112
267
  }
113
268
 
114
- // Find the package in node_modules (check multiple locations for npx/pnpm/npm compatibility)
115
269
  const possiblePaths = [
116
270
  path.join(process.cwd(), 'node_modules', pkgName, 'native', 'bin'),
117
271
  path.join(import.meta.dirname, '..', 'node_modules', pkgName, 'native', 'bin'),
@@ -120,8 +274,8 @@ function getBinaryPaths() {
120
274
  ];
121
275
 
122
276
  for (const binDir of possiblePaths) {
123
- const initdb = path.join(binDir, platform === 'win32' ? 'initdb.exe' : 'initdb');
124
- const postgres = path.join(binDir, platform === 'win32' ? 'postgres.exe' : 'postgres');
277
+ const initdb = path.join(binDir, 'initdb' + exeSuffix);
278
+ const postgres = path.join(binDir, 'postgres' + exeSuffix);
125
279
  if (fs.existsSync(initdb) && fs.existsSync(postgres)) {
126
280
  // Resolve the actual binary paths (handles symlinks from package managers)
127
281
  const realInitdb = fs.realpathSync(initdb);
@@ -237,8 +391,8 @@ export class PostgresManager {
237
391
  * Start the embedded PostgreSQL instance
238
392
  */
239
393
  async start() {
240
- // Get binary paths
241
- this.binaries = getBinaryPaths();
394
+ // Get binary paths (may extract bundled binaries on first run)
395
+ this.binaries = await getBinaryPaths();
242
396
 
243
397
  // Make binaries executable
244
398
  await fs.promises.chmod(this.binaries.initdb, '755');