@photostructure/fs-metadata 0.6.0 → 0.7.0

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 (98) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/CLAUDE.md +160 -136
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +2 -2
  5. package/README.md +34 -84
  6. package/binding.gyp +98 -23
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +53 -22
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +5 -0
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.mjs +52 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +505 -0
  17. package/doc/MACOS_API_REFERENCE.md +469 -0
  18. package/doc/SECURITY_AUDIT_2025.md +809 -0
  19. package/doc/SSH_RELEASE_HOWTO.md +207 -0
  20. package/doc/WINDOWS_API_REFERENCE.md +422 -0
  21. package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
  22. package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
  23. package/doc/examples.md +267 -0
  24. package/doc/gotchas.md +297 -0
  25. package/doc/logo.png +0 -0
  26. package/doc/logo.svg +85 -0
  27. package/doc/macos-asan-sip-issue.md +71 -0
  28. package/doc/social.png +0 -0
  29. package/doc/social.svg +125 -0
  30. package/doc/windows-build.md +226 -0
  31. package/doc/windows-clang-tidy.md +72 -0
  32. package/doc/windows-memory-testing.md +108 -0
  33. package/doc/windows-prebuildify-arm64.md +232 -0
  34. package/jest.config.cjs +24 -0
  35. package/package.json +68 -44
  36. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  37. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  38. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  39. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  40. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  41. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  42. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  43. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  44. package/scripts/check-memory.ts +186 -0
  45. package/scripts/clang-tidy.ts +832 -0
  46. package/scripts/install.cjs +42 -0
  47. package/scripts/is-platform.mjs +1 -1
  48. package/scripts/macos-asan.sh +155 -0
  49. package/scripts/post-build.mjs +3 -3
  50. package/scripts/prebuild-linux-glibc.sh +119 -0
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +70 -0
  53. package/scripts/sanitizers-test.sh +7 -1
  54. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  55. package/src/binding.cpp +1 -1
  56. package/src/common/error_utils.h +0 -6
  57. package/src/common/volume_metadata.h +6 -0
  58. package/src/darwin/hidden.cpp +73 -25
  59. package/src/darwin/path_security.h +149 -0
  60. package/src/darwin/raii_utils.h +104 -4
  61. package/src/darwin/volume_metadata.cpp +132 -58
  62. package/src/darwin/volume_mount_points.cpp +80 -47
  63. package/src/hidden.ts +36 -13
  64. package/src/linux/gio_mount_points.cpp +17 -18
  65. package/src/linux/gio_utils.cpp +92 -37
  66. package/src/linux/gio_utils.h +11 -5
  67. package/src/linux/gio_volume_metadata.cpp +111 -48
  68. package/src/linux/volume_metadata.cpp +67 -4
  69. package/src/object.ts +1 -0
  70. package/src/options.ts +6 -0
  71. package/src/path.ts +11 -0
  72. package/src/platform.ts +25 -0
  73. package/src/remote_info.ts +5 -3
  74. package/src/stack_path.ts +8 -6
  75. package/src/string_enum.ts +1 -0
  76. package/src/test-utils/benchmark-harness.ts +192 -0
  77. package/src/test-utils/debuglog-child.ts +30 -2
  78. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  79. package/src/test-utils/jest-setup.ts +14 -0
  80. package/src/test-utils/memory-test-core.ts +336 -0
  81. package/src/test-utils/memory-test-runner.ts +108 -0
  82. package/src/test-utils/platform.ts +46 -1
  83. package/src/test-utils/worker-thread-helper.cjs +157 -26
  84. package/src/types/native_bindings.ts +1 -1
  85. package/src/types/options.ts +6 -0
  86. package/src/windows/drive_status.h +133 -163
  87. package/src/windows/error_utils.h +54 -3
  88. package/src/windows/fs_meta.h +1 -1
  89. package/src/windows/hidden.cpp +60 -43
  90. package/src/windows/security_utils.h +250 -0
  91. package/src/windows/string.h +68 -11
  92. package/src/windows/system_volume.h +1 -1
  93. package/src/windows/thread_pool.h +206 -0
  94. package/src/windows/volume_metadata.cpp +11 -6
  95. package/src/windows/volume_mount_points.cpp +8 -7
  96. package/src/windows/windows_arch.h +39 -0
  97. package/scripts/check-memory.mjs +0 -123
  98. package/scripts/clang-tidy.mjs +0 -73
@@ -0,0 +1,226 @@
1
+ # Windows Build Issues and Solutions
2
+
3
+ ## Overview
4
+
5
+ This document summarizes the Windows-specific build issues encountered with fs-metadata, particularly the "No Target Architecture" error from the Windows SDK, and various approaches to resolving it.
6
+
7
+ ## The Core Issue
8
+
9
+ When building on Windows with node-gyp/prebuildify, we encounter:
10
+
11
+ ```
12
+ C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winnt.h(173,1): fatal error C1189: #error: "No Target Architecture"
13
+ ```
14
+
15
+ This error occurs because:
16
+
17
+ 1. The Windows SDK headers require architecture macros (`_M_X64`, `_WIN64`, etc.) to be defined before including `<windows.h>`
18
+ 2. Node-gyp doesn't always properly pass these defines when using prebuildify
19
+ 3. The `target_arch` variable in binding.gyp conditions might not be evaluated correctly
20
+
21
+ ## Why This Happens
22
+
23
+ ### Root Cause
24
+
25
+ - Prebuildify calls node-gyp with `--arch` (previously `--target-arch`)
26
+ - The architecture should default to `os.arch()` (which correctly returns `x64` on Windows)
27
+ - However, the architecture defines aren't being set properly in the MSVC project generated by node-gyp
28
+
29
+ ### Why Other Projects Don't Have This Issue
30
+
31
+ Projects like node-sqlite3 don't typically encounter this because:
32
+
33
+ 1. They don't define architecture-specific macros in binding.gyp (these should come from the compiler)
34
+ 2. They may not be using prebuildify, or are using different build configurations
35
+ 3. They might be including Windows headers in a different order
36
+
37
+ ## Attempted Solutions
38
+
39
+ ### 1. Architecture Defines in binding.gyp (Not Sufficient)
40
+
41
+ Attempted to add architecture defines conditionally:
42
+
43
+ ```json
44
+ "conditions": [
45
+ [
46
+ "target_arch=='x64'",
47
+ {
48
+ "defines": [
49
+ "_M_X64",
50
+ "_WIN64"
51
+ ]
52
+ }
53
+ ]
54
+ ]
55
+ ```
56
+
57
+ **Result**: Doesn't reliably fix prebuildify builds
58
+
59
+ ### 2. Windows Header Wrapper (Failed)
60
+
61
+ Created `windows_headers.h` to force architecture defines:
62
+
63
+ ```cpp
64
+ #ifndef _M_X64
65
+ #define _M_X64 1
66
+ #define _AMD64_ 1
67
+ #define _WIN64 1
68
+ #endif
69
+ #include <windows.h>
70
+ ```
71
+
72
+ **Result**: Didn't resolve the issue for all files
73
+
74
+ ### 3. Direct Source File Defines (Don't Use - Breaks ARM64)
75
+
76
+ Adding architecture defines at the top of each Windows source file:
77
+
78
+ ```cpp
79
+ // Force architecture defines for Windows SDK
80
+ #ifndef _M_X64
81
+ #define _M_X64 1
82
+ #endif
83
+ #ifndef _AMD64_
84
+ #define _AMD64_ 1
85
+ #endif
86
+ #ifndef _WIN64
87
+ #define _WIN64 1
88
+ #endif
89
+ ```
90
+
91
+ **Result**: This hardcodes x64 defines and would break ARM64 builds
92
+
93
+ ### 4. CL Environment Variable (Current Solution)
94
+
95
+ Created `scripts/prebuildify-wrapper.ts` that sets the `CL` environment variable:
96
+
97
+ ```typescript
98
+ if (currentArch === "x64") {
99
+ env.CL = "/D_M_X64 /D_WIN64 /D_AMD64_";
100
+ } else if (currentArch === "arm64") {
101
+ env.CL = "/D_M_ARM64 /D_WIN64";
102
+ }
103
+ ```
104
+
105
+ **Result**: Works correctly for both x64 and ARM64 architectures
106
+
107
+ ## Current Status
108
+
109
+ ### What Works
110
+
111
+ - Building with the prebuildify wrapper that sets CL environment variable
112
+ - This solution properly handles both x64 and ARM64 architectures
113
+
114
+ ### What Doesn't Work
115
+
116
+ - Relying on node-gyp/prebuildify to properly evaluate binding.gyp conditions
117
+ - Direct source file defines (would break ARM64 builds)
118
+ - windows_compat.h wrapper (can't distinguish architectures at compile time)
119
+
120
+ ### Debug Builds Removed
121
+
122
+ - Debug builds have been removed from the project as they provided no value
123
+ - Windows debug CRT builds cannot be loaded by Node.js due to missing runtime dependencies
124
+ - JavaScript-based memory testing provides adequate leak detection
125
+
126
+ ## Other Important Findings
127
+
128
+ ### Node.js Version Compatibility
129
+
130
+ - Jest 30 doesn't support Node.js 23
131
+ - CI/CD workflows use Node.js 20, 22, and 24
132
+ - Removed Node.js 23 from test matrices
133
+
134
+ ### Related Issues
135
+
136
+ 1. **Memory Test Failures**: Initially appeared as "unsupported engine" warnings
137
+ 2. **Architecture Detection**: `process.arch` correctly returns `x64` on Windows
138
+ 3. **Prebuildify Defaults**: Should use `os.arch()` and `os.platform()` but something in the chain breaks
139
+
140
+ ## Potential Future Solutions
141
+
142
+ ### 1. Wrapper Script for Prebuildify
143
+
144
+ Create a TypeScript script to explicitly pass architecture:
145
+
146
+ ```typescript
147
+ import { arch, platform } from "os";
148
+ import { spawn } from "child_process";
149
+
150
+ spawn(
151
+ "prebuildify",
152
+ [
153
+ "--napi",
154
+ "--tag-libc",
155
+ "--strip",
156
+ "--arch",
157
+ arch(),
158
+ "--platform",
159
+ platform(),
160
+ ],
161
+ { stdio: "inherit" },
162
+ );
163
+ ```
164
+
165
+ ### 2. Environment Variables
166
+
167
+ Set architecture explicitly:
168
+
169
+ ```bash
170
+ set npm_config_arch=x64
171
+ npm run build
172
+ ```
173
+
174
+ ### 3. Investigation Areas
175
+
176
+ - Why `target_arch` isn't being evaluated in binding.gyp conditions
177
+ - Why prebuildify + node-gyp isn't passing architecture defines to MSVC
178
+ - Whether this is a node-gyp bug or a configuration issue
179
+
180
+ ## References
181
+
182
+ ### GitHub Issues
183
+
184
+ - nodejs/node-gyp#600: "gyp: name 'target_arch' is not defined"
185
+ - nodejs/node-gyp#2813: "Build defaults to x86 on a x64 Windows machine"
186
+ - prebuild/prebuildify#53: Changed from `--target-arch` to `--arch`
187
+
188
+ ### Similar Projects
189
+
190
+ - node-sqlite3: Uses simpler binding.gyp without architecture defines
191
+ - node-canvas: Uses `WIN32_LEAN_AND_MEAN` but not architecture defines
192
+ - serialport: Focus on exception handling and warning suppression
193
+
194
+ ## Why a Wrapper Header Won't Work
195
+
196
+ **Important Discovery**: A single `windows_compat.h` wrapper cannot solve this issue because:
197
+
198
+ 1. At compile time, we can't distinguish between x64 and ARM64 (both define `_WIN64`)
199
+ 2. The architecture information (`target_arch`) is only available in binding.gyp
200
+ 3. Prebuildify doesn't properly pass the conditional defines from binding.gyp to the compiler
201
+
202
+ This means any compile-time logic would incorrectly set x64 defines on ARM64 builds or vice versa.
203
+
204
+ ## Conclusion
205
+
206
+ The "No Target Architecture" error is a known limitation of how prebuildify and node-gyp handle Windows builds. The current workaround of adding architecture defines directly to source files is the most reliable solution.
207
+
208
+ **Why Other Projects Don't Have This Issue**:
209
+
210
+ - Projects like node-sqlite avoid `<windows.h>` by using cross-platform APIs
211
+ - Most Node.js addons don't directly interface with Windows system APIs
212
+
213
+ **The Solution**:
214
+ Use `scripts/prebuildify-wrapper.ts` which sets the CL environment variable with architecture-specific defines. This approach:
215
+
216
+ 1. Works reliably with prebuildify
217
+ 2. Correctly handles both x64 and ARM64 architectures
218
+ 3. Doesn't require modifying source files
219
+ 4. Avoids hardcoding architecture assumptions
220
+
221
+ ## Next Steps
222
+
223
+ 1. Consider opening an issue with prebuildify about Windows architecture detection
224
+ 2. Test if explicit `--arch x64` flags to prebuildify resolve the issue
225
+ 3. Investigate why other native modules don't encounter this problem
226
+ 4. Consider if our include order or binding.gyp structure is unusual
@@ -0,0 +1,72 @@
1
+ # Windows clang-tidy Support
2
+
3
+ ## Status
4
+
5
+ clang-tidy has limited support on Windows due to fundamental incompatibilities between clang and MSVC headers. While we've implemented Windows support in our unified `scripts/clang-tidy.ts`, users should be aware of the limitations.
6
+
7
+ ## Current Implementation
8
+
9
+ 1. **Unified Script**: `scripts/clang-tidy.ts` works on all platforms including Windows
10
+ 2. **Automatic Detection**: Finds Node.js headers, MSVC includes, and Windows SDK paths
11
+ 3. **Configuration**: Windows-specific checks in `src/windows/.clang-tidy`
12
+ 4. **Fallback**: Minimal configuration (`.clang-tidy-windows-minimal`) for basic checks
13
+
14
+ ## Known Issues
15
+
16
+ ### Header Compatibility
17
+
18
+ - clang-tidy cannot fully parse MSVC STL headers, resulting in errors like:
19
+ - `no member named 'max' in namespace 'std'`
20
+ - `unknown type name 'namespace'`
21
+ - `no template named 'pointer_traits'`
22
+
23
+ ### Root Cause
24
+
25
+ - MSVC headers use Microsoft-specific extensions that clang doesn't fully support
26
+ - Node.js native addon headers add additional complexity
27
+ - Even with clang-cl mode, full compatibility isn't achieved
28
+
29
+ ## Recommendations
30
+
31
+ ### For Windows Developers
32
+
33
+ 1. **Use Visual Studio Code Analysis**: The built-in Code Analysis in Visual Studio provides better Windows-specific checking
34
+ 2. **Focus on Warnings**: Despite header errors, clang-tidy still catches many issues:
35
+ - Uninitialized variables
36
+ - RAII violations
37
+ - Member initialization issues
38
+ - Ownership problems
39
+
40
+ 3. **Run Anyway**: Even with errors, the warnings are valuable:
41
+ ```bash
42
+ npm run lint:native
43
+ ```
44
+
45
+ ### For CI/CD
46
+
47
+ Consider skipping clang-tidy on Windows in CI to avoid noise:
48
+
49
+ ```bash
50
+ # In CI scripts
51
+ if [ "$OS" != "Windows_NT" ]; then
52
+ npm run lint:native
53
+ fi
54
+ ```
55
+
56
+ ## Future Improvements
57
+
58
+ - Monitor clang-tidy development for better MSVC support
59
+ - Consider using Visual Studio's built-in clang-tidy integration
60
+ - Investigate using `clangd` as an alternative
61
+
62
+ ## What Still Works
63
+
64
+ Despite the header issues, clang-tidy on Windows can still detect:
65
+
66
+ - Uninitialized variables
67
+ - Missing RAII usage
68
+ - Resource leaks in your code (not system headers)
69
+ - Code style issues
70
+ - Many security vulnerabilities
71
+
72
+ The key is to focus on warnings in your own code files, not system header errors.
@@ -0,0 +1,108 @@
1
+ # Windows Memory Testing
2
+
3
+ ## Overview
4
+
5
+ On Windows, debug builds with CRT memory leak detection face a loading issue where Node.js cannot load native modules that use Windows long paths (`\\?\` prefix). This document describes our approach to memory leak detection on Windows.
6
+
7
+ ## The Problem
8
+
9
+ When building with `node-gyp build --debug`, the debug build cannot be loaded by Node.js due to:
10
+
11
+ 1. **Missing Debug Runtime Dependencies**: Debug builds require debug versions of the Visual C++ runtime libraries (`ucrtbased.dll`, `vcruntime*d.dll`)
12
+ 2. **UNC Path Issues**: Node.js module loader has issues with Windows extended-length paths (`\\?\C:\...`)
13
+
14
+ ## Our Solution
15
+
16
+ We've split Windows security testing into three categories:
17
+
18
+ ### 1. Input Security Tests (`windows-input-security.test.ts`)
19
+
20
+ Tests for handling malicious inputs:
21
+
22
+ - Path traversal protection
23
+ - Buffer overflow protection
24
+ - Invalid UTF-8 handling
25
+ - Device name rejection
26
+ - Alternate data stream rejection
27
+
28
+ These tests run with regular Release builds and don't require debug memory detection.
29
+
30
+ ### 2. Resource Security Tests (`windows-resource-security.test.ts`)
31
+
32
+ Tests for resource/handle leaks during operations:
33
+
34
+ - Concurrent operations safety
35
+ - Handle cleanup on timeout
36
+ - Basic memory pattern monitoring
37
+
38
+ These tests validate functionality with Release builds.
39
+
40
+ ### 3. Memory Leak Detection (`windows-memory-check.test.ts`)
41
+
42
+ JavaScript-based memory monitoring that works with Release builds:
43
+
44
+ - Heap memory usage tracking with forced garbage collection
45
+ - Memory growth pattern analysis
46
+ - Handle count monitoring (via `process.report`)
47
+ - Per-operation memory cost calculation
48
+
49
+ ## Alternative Windows Memory Leak Detection Tools
50
+
51
+ **Note: Traditional Windows memory debugging tools have significant compatibility issues with Node.js native modules.**
52
+
53
+ ### Why These Tools Don't Work Well with Node.js
54
+
55
+ 1. **Dr. Memory**: Known incompatibility with Node.js - fails with "Unable to load client library: ucrtbase.dll" error (GitHub issue #2531)
56
+ 2. **Visual Leak Detector (VLD)**: Requires debug builds which have loading issues with Node.js native modules
57
+ 3. **Application Verifier**: Cannot properly hook into Node.js's memory management system
58
+ 4. **Windows CRT Debug Heap**: Requires debug builds that face the UNC path loading issue
59
+
60
+ ### Our Approach
61
+
62
+ Instead of these traditional tools, we use:
63
+
64
+ - JavaScript-based memory monitoring with forced garbage collection
65
+ - Process handle tracking via Node.js's built-in `process.report`
66
+ - Heap usage patterns analysis over multiple iterations
67
+ - This approach provides adequate memory leak detection without the compatibility issues
68
+
69
+ ## Running Memory Tests
70
+
71
+ ```bash
72
+ # Run all memory tests (JavaScript-based)
73
+ npm test -- src/windows-memory-check.test.ts
74
+
75
+ # Run resource security tests
76
+ npm test -- src/windows-resource-security.test.ts
77
+
78
+ # Run input security tests
79
+ npm test -- src/windows-input-security.test.ts
80
+
81
+ # Run full memory check suite (includes debug build attempt)
82
+ npm run check:memory
83
+ ```
84
+
85
+ ## Key Metrics Monitored
86
+
87
+ 1. **Heap Memory Growth**: Should be < 100KB per operation (Windows APIs may have higher baseline)
88
+ 2. **Total Memory Retention**: Should be < 5MB after GC
89
+ 3. **Handle Count**: Should not increase by more than 10
90
+ 4. **Concurrent Operation Memory**: Should be < 10MB for 100 operations
91
+
92
+ ## Future Improvements
93
+
94
+ 1. Investigate fixing debug build loading:
95
+ - Ship debug CRT dependencies
96
+ - Use manifest embedding for dependency resolution
97
+ - Investigate short path alternatives to avoid UNC paths
98
+
99
+ 2. Enhanced JavaScript monitoring:
100
+ - Native memory tracking via N-API
101
+ - V8 heap snapshots comparison
102
+ - Event loop lag correlation
103
+ - Windows performance counters integration
104
+
105
+ 3. Native-level monitoring:
106
+ - Custom memory allocator wrappers
107
+ - Direct Windows API handle tracking
108
+ - Integration with Node.js's built-in diagnostics
@@ -0,0 +1,232 @@
1
+ # Windows ARM64 Development and Prebuildify Setup
2
+
3
+ This document provides guidance for Windows ARM64 native module development, prebuildify configuration, and troubleshooting common issues.
4
+
5
+ ## Overview
6
+
7
+ Windows ARM64 support for native Node.js modules is a rapidly evolving area. This document captures key learnings from investigating build failures and researching the ecosystem.
8
+
9
+ ## Prebuildify Architecture Support
10
+
11
+ ### How Prebuildify Works
12
+
13
+ 1. **Build Time**: Creates prebuilt binaries for different platforms/architectures
14
+ 2. **Package Time**: Stores binaries in `./prebuilds/[platform]-[arch]/`
15
+ 3. **Runtime**: Uses `node-gyp-build` to load the appropriate binary
16
+
17
+ ### Platform Naming Conventions
18
+
19
+ - Windows x64: `win32-x64`
20
+ - Windows ARM64: `win32-arm64`
21
+ - Linux x64: `linux-x64`
22
+ - macOS x64: `darwin-x64`
23
+ - macOS ARM64: `darwin-arm64`
24
+
25
+ ### Scoped Package Handling
26
+
27
+ For scoped npm packages like `@photostructure/fs-metadata`:
28
+
29
+ - Package scope uses `+` instead of `/` in filenames
30
+ - Example: `@photostructure+fs-metadata.glibc.node`
31
+ - This is standard prebuildify behavior
32
+
33
+ ## Windows ARM64 Specific Considerations
34
+
35
+ ### Current State of the Ecosystem (2025)
36
+
37
+ 1. **GitHub Actions Support**: Windows ARM64 runners are in public preview
38
+ - Free for public repositories
39
+ - Run Windows 11 Desktop image
40
+ - Native ARM64 compilation (no emulation)
41
+
42
+ 2. **Common Issues in Other Projects**:
43
+ - **Cypress**: Requires manual addition of "win32-arm64" as valid OS
44
+ - **LMDB-JS**: Reports "No native build was found for platform=win32 arch=arm64"
45
+ - **Cloudflare workerd**: Requires manual workarounds in install scripts
46
+ - Many projects fall back to x64 binaries (run via emulation)
47
+
48
+ ### Architecture Detection Challenges
49
+
50
+ Windows SDK headers require architecture-specific defines before including `<windows.h>`:
51
+
52
+ - x64: `/D_M_X64 /D_WIN64 /D_AMD64_`
53
+ - ARM64: `/D_M_ARM64 /D_WIN64`
54
+
55
+ Our solution: Set `CL` environment variable in build scripts (see `scripts/prebuildify-wrapper.ts`)
56
+
57
+ ## GitHub Actions Configuration
58
+
59
+ ### Best Practices
60
+
61
+ ```yaml
62
+ prebuild-win-arm64:
63
+ runs-on: windows-11-arm # Native ARM64 runner
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+ - uses: actions/setup-node@v4
67
+ with:
68
+ node-version: 20
69
+ cache: "npm"
70
+ - run: npm ci --ignore-scripts
71
+ - run: npm run build:native
72
+ - uses: actions/upload-artifact@v4
73
+ with:
74
+ name: prebuilds-windows-11-arm
75
+ path: prebuilds/
76
+ ```
77
+
78
+ ### Testing Configuration
79
+
80
+ ```yaml
81
+ test-win-arm64:
82
+ needs: [prebuild-win-arm64]
83
+ runs-on: windows-11-arm
84
+ steps:
85
+ - uses: actions/download-artifact@v4
86
+ with:
87
+ path: ./prebuilds
88
+ merge-multiple: true
89
+ - run: npm ci
90
+ - run: npm run tests
91
+ ```
92
+
93
+ ## Common Problems and Solutions
94
+
95
+ ### Problem: Jest Worker Process Failures
96
+
97
+ **Symptoms**:
98
+
99
+ - "Jest worker encountered 4 child process exceptions, exceeding retry limit"
100
+ - Tests fail only in CI, not locally
101
+
102
+ **Root Causes**:
103
+
104
+ 1. Module resolution differs in worker threads
105
+ 2. `__dirname` context changes in Jest environment
106
+ 3. Relative paths may not resolve correctly
107
+
108
+ **Solutions**:
109
+
110
+ 1. Use multiple fallback paths when loading native modules
111
+ 2. Try `process.cwd()` in addition to relative paths
112
+ 3. Ensure prebuilds are in expected locations
113
+
114
+ ### Problem: Native Module Not Found
115
+
116
+ **Symptoms**:
117
+
118
+ - "No native build was found for platform=win32 arch=arm64"
119
+ - Module loads on x64 but not ARM64
120
+
121
+ **Root Causes**:
122
+
123
+ 1. Missing prebuilt binary for the platform
124
+ 2. Incorrect platform detection
125
+ 3. node-gyp-build not finding the prebuild
126
+
127
+ **Solutions**:
128
+
129
+ 1. Verify prebuild exists in `prebuilds/win32-arm64/`
130
+ 2. Check that `process.platform` === "win32" and `process.arch` === "arm64"
131
+ 3. Use `node-gyp-build` diagnostic output to debug loading
132
+
133
+ ### Problem: Build Failures on Windows ARM64
134
+
135
+ **Symptoms**:
136
+
137
+ - "No Target Architecture" errors
138
+ - Windows SDK header compilation errors
139
+
140
+ **Root Causes**:
141
+
142
+ - Missing architecture defines for Windows SDK
143
+ - Prebuildify not passing through defines from binding.gyp
144
+
145
+ **Solution**:
146
+ Set environment variables before building:
147
+
148
+ ```javascript
149
+ if (process.platform === "win32" && process.arch === "arm64") {
150
+ process.env.CL = "/D_M_ARM64 /D_WIN64";
151
+ }
152
+ ```
153
+
154
+ ## Testing Strategies
155
+
156
+ ### 1. Memory Testing
157
+
158
+ Traditional Windows memory tools don't work with Node.js:
159
+
160
+ - Dr. Memory: "Unable to load client library"
161
+ - Debug CRT: Cannot be loaded by Node.js
162
+ - Visual Leak Detector: Requires debug builds
163
+
164
+ Use JavaScript-based testing instead (see `src/windows-memory-check.test.ts`)
165
+
166
+ ### 2. Worker Thread Testing
167
+
168
+ - Test native module loading in both main and worker threads
169
+ - Use integration tests, not mocks
170
+ - Verify actual functionality, not just loading
171
+
172
+ ### 3. Cross-Platform Testing
173
+
174
+ - Test on actual ARM64 hardware when possible
175
+ - Use GitHub Actions for CI testing
176
+ - Be aware of performance differences (ARM64 can be slower)
177
+
178
+ ## Debugging Tips
179
+
180
+ ### 1. Verify Prebuild Location
181
+
182
+ ```bash
183
+ # List prebuilds
184
+ ls -la prebuilds/
185
+
186
+ # Check specific platform
187
+ ls -la prebuilds/win32-arm64/
188
+ ```
189
+
190
+ ### 2. Test Native Module Loading
191
+
192
+ ```javascript
193
+ // test-native-load.js
194
+ const nodeGypBuild = require("node-gyp-build");
195
+ try {
196
+ const binding = nodeGypBuild(process.cwd());
197
+ console.log("✓ Native module loaded");
198
+ console.log("Functions:", Object.keys(binding));
199
+ } catch (error) {
200
+ console.error("✗ Failed to load:", error.message);
201
+ }
202
+ ```
203
+
204
+ ### 3. Check Process Information
205
+
206
+ ```javascript
207
+ console.log("Platform:", process.platform);
208
+ console.log("Architecture:", process.arch);
209
+ console.log("Node version:", process.version);
210
+ ```
211
+
212
+ ## Future Considerations
213
+
214
+ 1. **Windows ARM64 Adoption**: Expected to grow with Qualcomm-based laptops
215
+ 2. **Tooling Improvements**: Build tools catching up with ARM64 support
216
+ 3. **Performance**: Native ARM64 binaries avoid x64 emulation overhead
217
+ 4. **Testing**: More ARM64 CI runners becoming available
218
+
219
+ ## References
220
+
221
+ - [Prebuildify Documentation](https://github.com/prebuild/prebuildify)
222
+ - [Node-gyp-build](https://github.com/prebuild/node-gyp-build)
223
+ - [Windows ARM64 GitHub Actions](https://github.blog/changelog/2025-04-14-windows-arm64-hosted-runners-now-available-in-public-preview/)
224
+ - [Node.js Native Addons](https://nodejs.org/api/addons.html)
225
+ - [Windows on ARM Documentation](https://docs.microsoft.com/en-us/windows/arm/)
226
+
227
+ ## Related Files
228
+
229
+ - `scripts/prebuildify-wrapper.ts` - Handles architecture-specific build configuration
230
+ - `scripts/install.cjs` - Sets up environment for Windows builds
231
+ - `.github/workflows/build.yml` - CI configuration including ARM64 jobs
232
+ - `CLAUDE.md` - Project-specific build notes
package/jest.config.cjs CHANGED
@@ -10,10 +10,33 @@ const isESM =
10
10
  process.env.TEST_ESM === "1" ||
11
11
  process.env.NODE_OPTIONS?.includes("--experimental-vm-modules");
12
12
 
13
+ const nodeVersion = parseInt(process.version.slice(1).split(".")[0], 10);
14
+
15
+ const isNode24ESM = nodeVersion >= 24 && isESM;
16
+ const isWindowsCI = platform === "win32" && process.env.CI;
17
+
18
+ // Determine maxWorkers based on environment
19
+ let maxWorkers = undefined; // undefined means Jest uses default (number of cores - 1)
20
+ let workerIdleMemoryLimit = undefined; // undefined means Jest uses default
21
+
22
+ if (isWindowsCI) {
23
+ console.log("[Jest Config] Windows CI detected, applying workarounds:");
24
+ maxWorkers = 1;
25
+ workerIdleMemoryLimit = "1GB";
26
+ } else if (isNode24ESM) {
27
+ console.log(
28
+ "[Jest Config] Node 24+ with ESM detected, applying workarounds:",
29
+ );
30
+ // Node 24 + ESM detection (avoid "module is already linked")
31
+ maxWorkers = 1;
32
+ }
33
+
13
34
  /** @type {import('ts-jest').JestConfigWithTsJest} */
14
35
  const config = {
15
36
  displayName: `@photostructure/fs-metadata (${isESM ? "ESM" : "CJS"})`,
16
37
  testEnvironment: "jest-environment-node",
38
+ ...(maxWorkers != null ? { maxWorkers } : {}),
39
+ ...(workerIdleMemoryLimit != null ? { workerIdleMemoryLimit } : {}),
17
40
  roots: ["<rootDir>/src"],
18
41
  coverageProvider: "v8",
19
42
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
@@ -23,6 +46,7 @@ const config = {
23
46
  setupFilesAfterEnv: [
24
47
  "jest-extended/all",
25
48
  "<rootDir>/src/test-utils/jest-matchers.ts",
49
+ "<rootDir>/src/test-utils/jest-setup.ts",
26
50
  ],
27
51
  collectCoverage: !argv.includes("--no-coverage"),
28
52
  coverageDirectory: "coverage",