@photostructure/fs-metadata 0.6.0 → 0.6.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +5 -6
  2. package/CLAUDE.md +279 -81
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +20 -1
  5. package/binding.gyp +1 -1
  6. package/dist/index.cjs +2 -1
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +1 -0
  9. package/dist/index.mjs.map +1 -1
  10. package/doc/GPG_RELEASE_HOWTO.md +474 -0
  11. package/doc/SSH_RELEASE_HOWTO.md +203 -0
  12. package/doc/WINDOWS_DEBUG_GUIDE.md +89 -0
  13. package/jest.config.cjs +1 -0
  14. package/package.json +22 -23
  15. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  16. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  17. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  18. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  20. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  21. package/scripts/clang-tidy.ts +241 -0
  22. package/scripts/prebuild-linux-glibc.sh +108 -0
  23. package/scripts/precommit.ts +45 -0
  24. package/scripts/sanitizers-test.sh +8 -2
  25. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  26. package/src/binding.cpp +1 -1
  27. package/src/common/error_utils.h +0 -6
  28. package/src/platform.ts +25 -0
  29. package/src/test-utils/benchmark-harness.ts +192 -0
  30. package/src/test-utils/debuglog-child.ts +30 -2
  31. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  32. package/src/test-utils/jest-setup.ts +14 -0
  33. package/src/test-utils/worker-thread-helper.cjs +4 -0
  34. package/scripts/clang-tidy.mjs +0 -73
  35. /package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +0 -0
package/CHANGELOG.md CHANGED
@@ -14,7 +14,7 @@ Fixed for any bug fixes.
14
14
  Security in case of vulnerabilities.
15
15
  -->
16
16
 
17
- ## [0.6.0] - 2025-06-03
17
+ ## [0.6.0] - 2025-06-09
18
18
 
19
19
  ### Added
20
20
 
@@ -24,6 +24,8 @@ Security in case of vulnerabilities.
24
24
  - Clang-tidy integration for C++ code analysis
25
25
  - Worker thread helper for volume metadata operations
26
26
  - NAPI_VERSION=9 definition for improved compatibility
27
+ - Dynamic test timeout configuration based on environment (CI, platform, architecture)
28
+ - Prebuild script for Linux GLIBC compatibility in Docker environments
27
29
 
28
30
  ### Breaking
29
31
 
@@ -36,6 +38,7 @@ Security in case of vulnerabilities.
36
38
  - Updated all imports to use `node:` prefix for built-in modules
37
39
  - Reorganized build and test scripts for better clarity
38
40
  - Improved memory test workflow for cross-platform compatibility
41
+ - Renamed native module target from `node_fs_meta` to `fs_metadata` for consistency
39
42
 
40
43
  ### Fixed
41
44
 
@@ -44,13 +47,9 @@ Security in case of vulnerabilities.
44
47
  - Fixed buffer allocation issues in Windows networking and volume operations
45
48
  - Enhanced resource management with better validation for empty mount points
46
49
  - Made `SystemPathPatternsDefault` values visible in TypeScript typings
47
- - Improved test reliability across different CI environments
48
50
  - Added Napi::HandleScope to OnOK and OnError methods for proper scope management
49
51
  - Removed unnecessary std::move operations in worker implementations
50
- - Updated filesystem metadata tests to verify type and existence instead of specific values
51
- - Improved error handling and messages in various components
52
- - Enhanced error message validation for file access in worker thread tests
53
- - Updated tests to use healthy mount points for volume metadata retrieval
52
+ - Resolved CI test reliability issues across different environments, particularly Alpine ARM64 emulation timeouts
54
53
 
55
54
  ## [0.4.0] - 2025-01-09
56
55
 
package/CLAUDE.md CHANGED
@@ -22,86 +22,6 @@ This is @photostructure/fs-metadata - a cross-platform native Node.js module for
22
22
  - **Windows**: Uses separate threads per mountpoint for health checks to handle blocked system calls
23
23
  - **System Volumes**: Each platform handles system volumes differently; the library uses heuristics to identify them
24
24
 
25
- ## Common Commands
26
-
27
- ### Build and Development
28
- ```bash
29
- # Install dependencies and build native modules
30
- npm install
31
-
32
- # Configure platform-specific build settings
33
- npm run configure:native
34
-
35
- # Build native bindings
36
- npm run node-gyp-rebuild
37
-
38
- # Create prebuilds for distribution
39
- npm run prebuild
40
-
41
- # Assemble TypeScript to dist/
42
- npm run build:dist
43
- ```
44
-
45
- ### Testing
46
- ```bash
47
- # Run all tests with coverage (includes memory tests on Linux)
48
- npm run test:all
49
-
50
- # Run CommonJS tests
51
- npm test:cjs
52
-
53
- # Run ESM tests
54
- npm test:esm
55
-
56
- # Test memory leaks (JavaScript)
57
- npm run test:memory
58
-
59
- # Run valgrind memory analysis (Linux only)
60
- bash scripts/valgrind-test.sh
61
-
62
- # Run AddressSanitizer and LeakSanitizer (Linux only)
63
- bash scripts/sanitizers-test.sh
64
-
65
- # Run comprehensive memory tests (JavaScript + valgrind + sanitizers)
66
- npm run test:memory
67
-
68
- # Run a specific test file (no coverage)
69
- npm test volume_metadata
70
- ```
71
-
72
- ### Code Quality
73
- ```bash
74
- # Run ESLint
75
- npm run lint
76
-
77
- # Fix ESLint issues
78
- npm run lint:fix
79
-
80
- # Format code (all formats)
81
- npm run fmt
82
-
83
- # Format C++ code only
84
- npm run fmt:cpp
85
-
86
- # Format TypeScript only
87
- npm run fmt:ts
88
-
89
- # Type checking
90
- npm run build:ts
91
- ```
92
-
93
- ### Pre-commit
94
- ```bash
95
- # Full precommit check (fmt, clean, prebuild, tests)
96
- npm run precommit
97
- ```
98
-
99
- ### Documentation
100
- ```bash
101
- # Generate API documentation
102
- npm run docs
103
- ```
104
-
105
25
  ## Architecture Overview
106
26
 
107
27
  ### Core Structure
@@ -157,6 +77,34 @@ npm run docs
157
77
  - Debug messages from both JavaScript and native code are sent to `stderr`
158
78
  - Uses native Node.js debuglog for determining if logging is enabled
159
79
 
80
+ ## Release Process
81
+
82
+ The project uses a vanilla npm/git release workflow with GPG signed commits for security:
83
+
84
+ ### Prerequisites
85
+ - Repository secrets must be configured:
86
+ - `NPM_TOKEN`: Authentication token for npm publishing
87
+ - `GPG_PRIVATE_KEY`: ASCII-armored GPG private key for signing commits
88
+ - `GPG_PASSPHRASE`: Passphrase for the GPG key (if applicable)
89
+
90
+ ### Automated Release
91
+ 1. Trigger via GitHub Actions workflow dispatch with version type (patch/minor/major)
92
+ 2. Builds all prebuilds for supported platforms
93
+ 3. Runs comprehensive test suite across platforms
94
+ 4. Uses `npm version` to bump version and create signed git tags
95
+ 5. Publishes to npm registry
96
+ 6. Creates GitHub release with auto-generated notes
97
+ 7. All commits and tags are GPG signed for verification
98
+
99
+ ### Manual Release (if needed)
100
+ ```bash
101
+ npm run prepare-release
102
+ git config commit.gpgsign true
103
+ npm version patch|minor|major
104
+ npm publish
105
+ git push origin main --follow-tags
106
+ ```
107
+
160
108
  ## Example Usage
161
109
 
162
110
  ```typescript
@@ -173,4 +121,254 @@ console.dir({ volumeMetadata });
173
121
  // Check if a file is hidden
174
122
  import { isHidden } from "@photostructure/fs-metadata";
175
123
  const hidden = await isHidden("/path/to/file");
176
- ```
124
+ ```
125
+
126
+ ## CI/CD Test Reliability Guidelines
127
+
128
+ Based on analysis of recent test failures, here are critical patterns to avoid flaky tests:
129
+
130
+ ### 1. Benchmark and Performance Tests
131
+ - **Problem**: "Cannot log after tests are done" errors in worker_threads.test.ts
132
+ - **Solution**:
133
+ - Always await all async operations before test completion
134
+ - Use proper test lifecycle hooks (afterEach/afterAll) for cleanup
135
+ - Avoid console.log in async contexts without proper synchronization
136
+ - Consider using test.concurrent with explicit done() callbacks
137
+
138
+ ### 2. Alpine Linux ARM64 Issues
139
+ - **Problem**: Tests timeout on emulated Alpine ARM64 environments
140
+ - **Solution**:
141
+ - Skip process-spawning tests on Alpine ARM64 (`if (isAlpine && isARM64)`)
142
+ - Use increased timeout multipliers (20x) for emulated environments
143
+ - Detect emulation via `/proc/cpuinfo` or environment checks
144
+ - Consider separate test suites for native vs emulated environments
145
+
146
+ ### 3. Worker Thread Management
147
+ - **Problem**: Race conditions in concurrent worker operations
148
+ - **Solution**:
149
+ - Implement proper worker pool management with size limits
150
+ - Use Promise.allSettled() instead of Promise.all() for parallel operations
151
+ - Add explicit cleanup in test teardown to terminate all workers
152
+ - Set reasonable concurrency limits based on environment (CPU cores)
153
+
154
+ ### 4. Timeout Test Reliability
155
+ - **Problem**: Timeout tests fail due to timing precision issues
156
+ - **Solution**:
157
+ - Never use exact timing assertions (e.g., expect 100ms)
158
+ - Use ranges with adequate margins (e.g., 90-110ms)
159
+ - Account for CI environment variability (slower machines)
160
+ - Consider mocking timers for deterministic behavior
161
+
162
+ ### 5. File System Operations
163
+ - **Problem**: ENOENT errors for test directories, permission issues
164
+ - **Solution**:
165
+ - Always use unique temporary directories per test
166
+ - Clean up test artifacts in afterEach hooks
167
+ - Check directory existence before operations
168
+ - Handle platform-specific path separators
169
+
170
+ ### 6. Memory and Resource Leaks
171
+ - **Problem**: Tests don't properly clean up resources
172
+ - **Solution**:
173
+ - Explicitly close all file handles, network connections
174
+ - Use try-finally blocks for resource cleanup
175
+ - Monitor memory usage in long-running tests
176
+ - Implement proper garbage collection triggers
177
+
178
+ ### 7. Platform-Specific Failures
179
+ - **Problem**: Different behavior across Windows/macOS/Linux
180
+ - **Solution**:
181
+ - Use platform detection helpers consistently
182
+ - Skip platform-specific tests appropriately
183
+ - Account for filesystem differences (case sensitivity, path formats)
184
+ - Test with platform-specific CI matrices
185
+
186
+ ### 8. Jest Configuration
187
+ - **Problem**: Tests interfere with each other
188
+ - **Solution**:
189
+ - Use `--runInBand` for tests with shared resources
190
+ - Clear module cache between tests when needed
191
+ - Isolate tests that spawn processes
192
+ - Configure proper test timeouts per environment
193
+
194
+ ### Async Cleanup Anti-Patterns
195
+
196
+ **IMPORTANT**: The following approaches are NOT valid solutions for async cleanup issues:
197
+
198
+ ```javascript
199
+ // BAD: Arbitrary timeouts in tests
200
+ await new Promise((resolve) => setTimeout(resolve, 100));
201
+
202
+ // BAD: Forcing garbage collection
203
+ if (global.gc) {
204
+ global.gc();
205
+ }
206
+
207
+ // BAD: Adding setImmediate in afterAll to "fix" hanging tests
208
+ afterAll(async () => {
209
+ await new Promise((resolve) => setImmediate(resolve));
210
+ });
211
+ ```
212
+
213
+ **Why these are problematic:**
214
+
215
+ 1. **Arbitrary timeouts** are race conditions waiting to happen. They might work on fast machines but fail on slower CI runners.
216
+ 2. **Forcing GC** should never be required for correct behavior. If your code depends on GC for correctness, it has a fundamental design flaw.
217
+ 3. **setImmediate/nextTick delays** in cleanup hooks don't fix the root cause - they just paper over the real issue.
218
+ 4. These approaches mask the real problem instead of fixing it.
219
+
220
+ **Note**: This is different from legitimate uses of timeouts, such as:
221
+
222
+ - Waiting for time to pass to test timestamp changes
223
+ - Rate limiting or throttling tests
224
+ - Testing timeout behavior itself
225
+
226
+ The anti-pattern is using timeouts or GC to "fix" async cleanup issues.
227
+
228
+ **What to do instead:**
229
+
230
+ 1. Find the actual resource that's keeping the process alive (use `--detectOpenHandles`)
231
+ 2. Ensure all database connections are properly closed
232
+ 3. Ensure all file handles are closed
233
+ 4. Cancel or await all pending async operations
234
+ 5. Use proper resource management patterns (RAII, try-finally, using statements)
235
+
236
+ ### Windows-Compatible Directory Cleanup
237
+
238
+ **IMPORTANT**: Never use `fs.rmSync()` or `fs.rm()` without proper Windows retry logic for directory cleanup in tests.
239
+
240
+ **Problem**: On Windows, file handles and directory locks can remain active longer than on Unix systems, causing `EBUSY` errors during cleanup.
241
+
242
+ **Proper Solution**: Use `fsp.rm()` (async) with retry options:
243
+
244
+ ```typescript
245
+ await fsp.rm(tempDir, {
246
+ recursive: true,
247
+ force: true,
248
+ maxRetries: process.platform === "win32" ? 3 : 1,
249
+ retryDelay: process.platform === "win32" ? 100 : 0,
250
+ });
251
+ ```
252
+
253
+ **Best Practice**: Use existing test utilities that handle Windows-compatible cleanup patterns. Don't manually clean up temp directories - let the test framework handle it with proper retry logic.
254
+
255
+ ### Adaptive Timeout Testing
256
+
257
+ **Problem**: Fixed timeouts don't account for varying CI environment performance.
258
+
259
+ **Root Causes**:
260
+
261
+ - Alpine Linux (musl libc) is 2x slower than glibc
262
+ - ARM64 emulation on x64 runners is 5x slower
263
+ - Windows process operations are 4x slower
264
+ - macOS VMs are 4x slower
265
+ - CI environments have resource constraints
266
+
267
+ **Solutions**:
268
+
269
+ ```typescript
270
+ // DON'T: Use fixed timeouts
271
+ test("my test", async () => {
272
+ // Test code
273
+ }, 10000);
274
+
275
+ // DO: Use adaptive timeouts based on environment
276
+ import { getTestTimeout } from "./test-utils/test-timeout-config";
277
+
278
+ test(
279
+ "my test",
280
+ async () => {
281
+ // Test code
282
+ },
283
+ getTestTimeout(10000),
284
+ );
285
+
286
+ // DO: Account for platform timing differences
287
+ const timingMultiplier = process.platform === "win32" ? 4 :
288
+ process.platform === "darwin" ? 4 :
289
+ process.env.CI ? 2 : 1;
290
+ ```
291
+
292
+ ### Multi-Process Test Synchronization
293
+
294
+ **Problem**: Multi-process tests failing due to race conditions between process startup and test assertions.
295
+
296
+ **Root Cause**: The timing between process startup and resource acquisition varies significantly by platform.
297
+
298
+ **Solutions**:
299
+
300
+ ```typescript
301
+ // DON'T: Assume immediate process readiness
302
+ const proc = spawn(nodeCmd, [script]);
303
+ const result = await waitForProcessResult(proc);
304
+ expect(result).toBe("expected_outcome"); // May fail due to timing
305
+
306
+ // DO: Use explicit synchronization signals
307
+ const script = `
308
+ // Setup code
309
+ console.log("READY"); // Signal readiness
310
+ // Main test logic
311
+ console.log("RESULT:" + outcome);
312
+ `;
313
+
314
+ const proc = spawn(process.execPath, ["-e", script]);
315
+ await waitForOutput(proc, "READY"); // Wait for process to be ready
316
+ const result = await waitForOutput(proc, "RESULT:");
317
+ expect(result.split(":")[1]).toBe("expected_outcome");
318
+ ```
319
+
320
+ ### Wait-for-Condition Pattern
321
+
322
+ **Problem**: Tests failing because they don't wait for asynchronous conditions to be met.
323
+
324
+ **Solution**: Implement robust condition waiting with platform-aware timing:
325
+
326
+ ```typescript
327
+ async function waitForCondition(
328
+ check: () => boolean | Promise<boolean>,
329
+ options: {
330
+ maxAttempts?: number;
331
+ delay?: number;
332
+ timeoutMs?: number;
333
+ } = {}
334
+ ) {
335
+ const {
336
+ maxAttempts = 50,
337
+ delay = 100,
338
+ timeoutMs = 30000
339
+ } = options;
340
+
341
+ const startTime = Date.now();
342
+
343
+ for (let i = 0; i < maxAttempts; i++) {
344
+ if (Date.now() - startTime > timeoutMs) {
345
+ throw new Error(`Condition not met within ${timeoutMs}ms`);
346
+ }
347
+
348
+ if (await check()) return true;
349
+ await new Promise((resolve) => setTimeout(resolve, delay));
350
+ }
351
+
352
+ return false;
353
+ }
354
+
355
+ // Usage example
356
+ await waitForCondition(
357
+ () => fs.existsSync(expectedFile),
358
+ { timeoutMs: getTestTimeout(10000) }
359
+ );
360
+ ```
361
+
362
+ ### Best Practices Summary
363
+ 1. **Always clean up**: Resources, timers, workers, file handles
364
+ 2. **Never assume timing**: Use ranges and adaptive timeouts, not exact values
365
+ 3. **Isolate tests**: Each test should be independent
366
+ 4. **Platform awareness**: Skip tests that can't work on certain platforms
367
+ 5. **Proper async handling**: Always await or return promises
368
+ 6. **Resource limits**: Don't spawn unlimited workers/processes
369
+ 7. **Environment detection**: Adjust behavior for CI vs local
370
+ 8. **Deterministic tests**: Mock external dependencies when possible
371
+ 9. **Explicit synchronization**: Use signals for multi-process coordination
372
+ 10. **Robust waiting**: Use condition-based waiting instead of arbitrary timeouts
373
+ 11. **Windows compatibility**: Use retry logic for file operations on Windows
374
+ 12. **Anti-pattern awareness**: Avoid masking problems with timeouts or forced GC
package/CONTRIBUTING.md CHANGED
@@ -106,7 +106,7 @@ Actions that have multiple targets can be run in parallel using wildcards:
106
106
 
107
107
  ### Naming Guidelines
108
108
 
109
- - Use explicit names to avoid ambiguity (e.g., `configure:native` instead of just `configure`)
109
+ - Use explicit names to avoid ambiguity (e.g., `setup:native` instead of just `setup`)
110
110
  - Group related scripts by action prefix for easy wildcard execution
111
111
  - Avoid names that could cause npm lifecycle conflicts (e.g., `prebuild` vs `build`)
112
112
  - Use descriptive suffixes that clearly indicate the target or purpose
package/README.md CHANGED
@@ -16,7 +16,8 @@ Built and supported by [PhotoStructure](https://photostructure.com).
16
16
  - Cross-platform support:
17
17
  - Windows 10+ (x64)
18
18
  - macOS 14+
19
- - Ubuntu 22+ (x64, arm64) (with Gnome GIO/`GVfs` mount support where available)
19
+ - Debian 11/Ubuntu 20.04+ (x64, arm64) (with Gnome GIO/`GVfs` mount support where available)
20
+ - Alpine 3.21+ (x64, arm64)
20
21
 
21
22
  - [List all mounted volumes/drives](https://photostructure.github.io/fs-metadata/functions/getVolumeMountPoints.html)
22
23
 
@@ -41,6 +42,24 @@ Built and supported by [PhotoStructure](https://photostructure.com).
41
42
 
42
43
  - Comprehensive test coverage
43
44
 
45
+ ## Supported Platforms
46
+
47
+ Prebuilt binaries are provided for the following platforms:
48
+
49
+ | Platform | Architecture | Node.js | Minimum OS Version |
50
+ |----------|--------------|---------|-------------------|
51
+ | Windows | x64 | 20+ | Windows 10 |
52
+ | macOS | x64 | 20+ | macOS 14 (Sonoma) |
53
+ | macOS | arm64 | 20+ | macOS 14 (Sonoma) |
54
+ | Linux (glibc) | x64 | 20+ | Debian 11 (Bullseye), Ubuntu 20.04, GLIBC 2.31+ |
55
+ | Linux (glibc) | arm64 | 20+ | Debian 11 (Bullseye), Ubuntu 20.04, GLIBC 2.31+ |
56
+ | Linux (musl) | x64 | 20+ | Alpine 3.21 |
57
+ | Linux (musl) | arm64 | 20+ | Alpine 3.21 |
58
+
59
+ Notes:
60
+ - Linux binaries require GLIBC 2.31+ (Debian 11 Bullseye or newer). **Note**: this means the `node:20` docker image is **not** supported, due to the last several major versions of `node-gyp-build` requiring both a newer python and newer C++ version.
61
+ - Electron is supported via [Node-API](https://nodejs.org/api/n-api.html) compatibility
62
+
44
63
  ## Installation
45
64
 
46
65
  ```bash
package/binding.gyp CHANGED
@@ -4,7 +4,7 @@
4
4
  },
5
5
  "targets": [
6
6
  {
7
- "target_name": "node_fs_meta",
7
+ "target_name": "fs_metadata",
8
8
  "sources": [
9
9
  "src/binding.cpp"
10
10
  ],
package/dist/index.cjs CHANGED
@@ -97,6 +97,7 @@ function defer2(thunk) {
97
97
  var import_node_path = require("path");
98
98
 
99
99
  // src/platform.ts
100
+ var import_node_fs = require("fs");
100
101
  var import_node_process = require("process");
101
102
  var isLinux = import_node_process.platform === "linux";
102
103
  var isWindows = import_node_process.platform === "win32";
@@ -196,7 +197,7 @@ function _dirname() {
196
197
  }
197
198
 
198
199
  // src/fs.ts
199
- var import_node_fs = require("fs");
200
+ var import_node_fs2 = require("fs");
200
201
  var import_promises = require("fs/promises");
201
202
  var import_node_path2 = require("path");
202
203