@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,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Custom install script that handles Windows architecture defines
5
+ * when node-gyp-build needs to compile from source
6
+ */
7
+
8
+ const { spawn } = require("child_process");
9
+ const { platform, arch } = require("os");
10
+
11
+ // If in CI and on Windows, set architecture defines
12
+ if (process.env.CI && platform() === "win32") {
13
+ const currentArch = arch();
14
+
15
+ // Set architecture-specific defines for Windows
16
+ if (currentArch === "x64") {
17
+ process.env.CL = "/D_M_X64 /D_WIN64 /D_AMD64_";
18
+ } else if (currentArch === "arm64") {
19
+ process.env.CL = "/D_M_ARM64 /D_WIN64";
20
+ }
21
+
22
+ console.log(`Windows CI detected: arch=${currentArch}, CL=${process.env.CL}`);
23
+ }
24
+
25
+ // Run node-gyp-build
26
+ const child = spawn("npx", ["node-gyp-build"], {
27
+ stdio: "inherit",
28
+ shell: true,
29
+ env: process.env,
30
+ });
31
+
32
+ child.on("error", (error) => {
33
+ console.error("Failed to run node-gyp-build:", error);
34
+ process.exit(1);
35
+ });
36
+
37
+ child.on("exit", (code) => {
38
+ if (code !== 0) {
39
+ console.error(`node-gyp-build exited with code ${code}`);
40
+ process.exit(code || 1);
41
+ }
42
+ });
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { platform } from "os";
2
+ import { platform } from "node:os";
3
3
 
4
4
  const targetPlatform = process.argv[2];
5
5
  if (!targetPlatform) {
@@ -0,0 +1,155 @@
1
+ #!/bin/bash
2
+ # macOS AddressSanitizer test script
3
+
4
+ set -euo pipefail
5
+
6
+ # Colors for output
7
+ RED='\033[0;31m'
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[1;33m'
10
+ NC='\033[0m' # No Color
11
+
12
+ echo -e "${YELLOW}=== macOS AddressSanitizer Memory Test ===${NC}"
13
+
14
+ # Check if we're on macOS
15
+ if [[ "$(uname)" != "Darwin" ]]; then
16
+ echo -e "${YELLOW}Not on macOS. Skipping macOS-specific memory tests.${NC}"
17
+ exit 0
18
+ fi
19
+
20
+ # Clean and rebuild with AddressSanitizer
21
+ echo -e "${YELLOW}Cleaning previous builds...${NC}"
22
+ npm run clean:native
23
+
24
+ # Configure build with ASan flags
25
+ echo -e "${YELLOW}Building with AddressSanitizer enabled...${NC}"
26
+ export CFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer"
27
+ export CXXFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer"
28
+ export LDFLAGS="-fsanitize=address"
29
+
30
+ # Set ASan options
31
+ export ASAN_OPTIONS="detect_leaks=1:check_initialization_order=1:strict_init_order=1:print_stats=1:halt_on_error=0"
32
+ export MallocScribble=1
33
+ export MallocGuardEdges=1
34
+
35
+ # Find and set the ASan library path for macOS
36
+ # First try to find the most recent version
37
+ ASAN_LIB=$(find /Library/Developer/CommandLineTools/usr/lib/clang/*/lib/darwin -name "libclang_rt.asan_osx_dynamic.dylib" 2>/dev/null | sort -V | tail -1)
38
+ if [[ -z "$ASAN_LIB" ]]; then
39
+ # Try alternative location
40
+ ASAN_LIB=$(find /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/*/lib/darwin -name "libclang_rt.asan_osx_dynamic.dylib" 2>/dev/null | sort -V | tail -1)
41
+ fi
42
+
43
+ if [[ -n "$ASAN_LIB" ]]; then
44
+ export DYLD_INSERT_LIBRARIES="$ASAN_LIB"
45
+ echo -e "${GREEN}Using ASan library: $ASAN_LIB${NC}"
46
+ else
47
+ echo -e "${RED}Warning: Could not find ASan library. Tests may not run properly.${NC}"
48
+ fi
49
+
50
+ # Build the native module
51
+ npm run node-gyp-rebuild
52
+
53
+ # Run tests with ASan
54
+ echo -e "${YELLOW}Running tests with AddressSanitizer...${NC}"
55
+
56
+ # Note: On macOS with SIP enabled, DYLD_INSERT_LIBRARIES is stripped from
57
+ # child processes. Jest uses worker processes, so we need to run tests
58
+ # in a single process to ensure ASAN works correctly.
59
+
60
+ # Run the test and capture output
61
+ TEST_OUTPUT=$(npm test -- --runInBand 2>&1)
62
+ TEST_EXIT_CODE=$?
63
+
64
+ if [[ $TEST_EXIT_CODE -eq 0 ]]; then
65
+ echo -e "${GREEN}✓ Tests passed with AddressSanitizer${NC}"
66
+ else
67
+ # Check if the failure is due to SIP interceptor issues
68
+ if echo "$TEST_OUTPUT" | grep -q "interceptors not installed"; then
69
+ echo -e "${YELLOW}⚠ Tests completed but AddressSanitizer interceptors not installed${NC}"
70
+ echo -e "${YELLOW} This is due to macOS System Integrity Protection (SIP) stripping${NC}"
71
+ echo -e "${YELLOW} DYLD_INSERT_LIBRARIES from child processes. This is expected behavior.${NC}"
72
+ echo -e "${YELLOW} To run ASAN tests properly, you may need to disable SIP temporarily.${NC}"
73
+ # Don't treat this as a failure
74
+ else
75
+ echo -e "${RED}✗ Tests failed with AddressSanitizer${NC}"
76
+ echo "$TEST_OUTPUT"
77
+ # This is a real failure, exit with error
78
+ exit 1
79
+ fi
80
+ fi
81
+
82
+ # Run memory leak check using leaks tool
83
+ echo -e "${YELLOW}Running macOS leaks tool...${NC}"
84
+ if command -v leaks >/dev/null 2>&1; then
85
+ # Get the project root directory
86
+ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
87
+
88
+ # Check if the native module exists
89
+ if [ ! -f "${PROJECT_ROOT}/build/Release/fs_metadata.node" ]; then
90
+ echo -e "${RED}Native module not found. Skipping leaks test.${NC}"
91
+ exit 1
92
+ fi
93
+
94
+ # Create a simple test script
95
+ cat > /tmp/test-leaks.js << EOF
96
+ const fs = require('fs');
97
+ const binding = require('${PROJECT_ROOT}/build/Release/fs_metadata.node');
98
+
99
+ async function testVolumeMountPoints() {
100
+ for (let i = 0; i < 100; i++) {
101
+ await binding.getVolumeMountPoints();
102
+ }
103
+ }
104
+
105
+ async function testVolumeMetadata() {
106
+ const mountPoints = await binding.getVolumeMountPoints();
107
+ if (mountPoints.length > 0) {
108
+ for (let i = 0; i < 10; i++) {
109
+ await binding.getVolumeMetadata({ mountPoint: mountPoints[0].mountPoint });
110
+ }
111
+ }
112
+ }
113
+
114
+ async function runTests() {
115
+ await testVolumeMountPoints();
116
+ await testVolumeMetadata();
117
+
118
+ // Force garbage collection if available
119
+ if (global.gc) {
120
+ global.gc();
121
+ }
122
+ }
123
+
124
+ runTests().then(() => {
125
+ console.log('Tests completed');
126
+ // Give time for cleanup
127
+ setTimeout(() => process.exit(0), 1000);
128
+ }).catch(err => {
129
+ console.error('Test failed:', err);
130
+ process.exit(1);
131
+ });
132
+ EOF
133
+
134
+ # Run with leaks detection
135
+ echo -e "${YELLOW}Executing memory leak test...${NC}"
136
+ if leaks --atExit -- node --expose-gc /tmp/test-leaks.js > /tmp/leaks-output.txt 2>&1; then
137
+ echo -e "${GREEN}✓ No memory leaks detected${NC}"
138
+ # Show summary if available
139
+ if grep -q "Process" /tmp/leaks-output.txt; then
140
+ echo -e "${YELLOW}Summary:${NC}"
141
+ grep -E "(Process|leaks for|total leaked bytes)" /tmp/leaks-output.txt || true
142
+ fi
143
+ else
144
+ echo -e "${RED}✗ Memory leaks detected or leaks tool failed:${NC}"
145
+ cat /tmp/leaks-output.txt
146
+ # Don't exit with failure - leaks tool can have false positives
147
+ echo -e "${YELLOW}Note: The leaks tool may report false positives from Node.js internals.${NC}"
148
+ fi
149
+
150
+ rm -f /tmp/test-leaks.js /tmp/leaks-output.txt
151
+ else
152
+ echo -e "${YELLOW}leaks tool not available. Skipping native leak detection.${NC}"
153
+ fi
154
+
155
+ echo -e "${GREEN}=== All macOS memory tests passed! ===${NC}"
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copyFile } from "fs/promises";
4
- import { dirname, join } from "path";
5
- import { fileURLToPath } from "url";
3
+ import { copyFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const distDir = join(__dirname, "..", "dist");
@@ -0,0 +1,119 @@
1
+ #!/bin/bash
2
+ # Build native module with portable GLIBC compatibility
3
+ # Uses Debian 11 Bullseye (GLIBC 2.31) to balance compatibility and modern toolchain
4
+ #
5
+ # GLIBC versions in official Node.js Docker images:
6
+ # - node:*-buster: GLIBC 2.28 (Debian 10) - Python 3.7, GCC 8.3 (too old)
7
+ # - node:*-bullseye: GLIBC 2.31 (Debian 11) - Python 3.9, GCC 10.2 (good balance)
8
+ # - node:*-bookworm: GLIBC 2.36 (Debian 12) - Python 3.11, GCC 12.2
9
+ # - node:* (default): GLIBC 2.36 (Debian 12 Bookworm)
10
+ # - node:*-alpine: musl libc (not GLIBC)
11
+ #
12
+ # Other common environments:
13
+ # - Ubuntu 20.04 LTS: GLIBC 2.31 (matches our target)
14
+ # - Ubuntu 22.04 LTS: GLIBC 2.35
15
+ # - CentOS 7: GLIBC 2.17 (EOL, not supported)
16
+ # - Amazon Linux 2: GLIBC 2.26 (older, but should work)
17
+ #
18
+ # By targeting GLIBC 2.31, we support Ubuntu 20.04 LTS and newer,
19
+ # while having a modern enough toolchain for Node.js requirements.
20
+
21
+ set -euo pipefail
22
+
23
+ # Allow architecture override (for CI cross-compilation)
24
+ TARGET_ARCH="${TARGET_ARCH:-}"
25
+
26
+ # If no target architecture specified, detect from host
27
+ if [ -z "$TARGET_ARCH" ]; then
28
+ ARCH=$(uname -m)
29
+ if [ "$ARCH" = "x86_64" ]; then
30
+ TARGET_ARCH="x64"
31
+ elif [ "$ARCH" = "aarch64" ]; then
32
+ TARGET_ARCH="arm64"
33
+ else
34
+ echo "Unsupported architecture: $ARCH"
35
+ exit 1
36
+ fi
37
+ fi
38
+
39
+ # Map to Docker platform architecture
40
+ if [ "$TARGET_ARCH" = "x64" ]; then
41
+ DOCKER_ARCH="amd64"
42
+ elif [ "$TARGET_ARCH" = "arm64" ]; then
43
+ DOCKER_ARCH="arm64"
44
+ else
45
+ echo "Unsupported target architecture: $TARGET_ARCH"
46
+ exit 1
47
+ fi
48
+
49
+ # Determine which build command to use
50
+ BUILD_CMD="${BUILD_CMD:-npm run build:native}"
51
+
52
+ # Check if we're already in a compatible environment
53
+ if [ -f /etc/os-release ]; then
54
+ . /etc/os-release
55
+ if [[ "${ID:-}" == "debian" && "${VERSION_ID:-}" == "11" ]]; then
56
+ echo "Already in Debian 11 Bullseye, building directly..."
57
+ $BUILD_CMD
58
+ exit 0
59
+ fi
60
+ fi
61
+
62
+ # Check if Docker is available
63
+ if ! command -v docker &> /dev/null; then
64
+ echo "Docker not found, falling back to local build"
65
+ echo "Warning: Binary may not be portable due to GLIBC version"
66
+ $BUILD_CMD
67
+ exit 0
68
+ fi
69
+
70
+ echo "Building native module in Debian 11 Bullseye container for GLIBC 2.31 compatibility..."
71
+ echo "Target architecture: $TARGET_ARCH (Docker platform: linux/$DOCKER_ARCH)"
72
+
73
+ # Create a container, build inside it, then copy artifacts out
74
+ CONTAINER_NAME="fs-metadata-build-$$"
75
+
76
+ # Start container in background
77
+ docker run -d \
78
+ --name "$CONTAINER_NAME" \
79
+ --platform "linux/$DOCKER_ARCH" \
80
+ node:20-bullseye \
81
+ sleep 3600
82
+
83
+ # Copy project files into container
84
+ docker cp . "$CONTAINER_NAME:/tmp/project"
85
+
86
+ # Run build inside container
87
+ # Debian 11 has Python 3.9 and GCC 10.2 which support our requirements
88
+ docker exec "$CONTAINER_NAME" sh -c "
89
+ cd /tmp/project && \
90
+ apt-get update -qq && \
91
+ apt-get install -y -qq build-essential python3 libglib2.0-dev libblkid-dev uuid-dev git && \
92
+ # Verify versions
93
+ echo 'Python version:' && python3 --version && \
94
+ echo 'GCC version:' && gcc --version | head -1 && \
95
+ # Build the project
96
+ npm ci --ignore-scripts && \
97
+ $BUILD_CMD
98
+ "
99
+
100
+ # Copy artifacts back
101
+ docker cp "$CONTAINER_NAME:/tmp/project/prebuilds" . 2>/dev/null || true
102
+ docker cp "$CONTAINER_NAME:/tmp/project/build" . 2>/dev/null || true
103
+ docker cp "$CONTAINER_NAME:/tmp/project/config.gypi" . 2>/dev/null || true
104
+
105
+ # Fix ownership (docker cp preserves container's root ownership)
106
+ if [ -d prebuilds ]; then
107
+ chown -R "$(id -u):$(id -g)" prebuilds
108
+ fi
109
+ if [ -d build ]; then
110
+ chown -R "$(id -u):$(id -g)" build
111
+ fi
112
+ if [ -f config.gypi ]; then
113
+ chown "$(id -u):$(id -g)" config.gypi
114
+ fi
115
+
116
+ # Clean up container
117
+ docker rm -f "$CONTAINER_NAME" >/dev/null
118
+
119
+ echo "Portable build complete!"
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { arch, platform } from "node:os";
5
+
6
+ /**
7
+ * Wrapper for prebuildify to ensure architecture is explicitly passed This
8
+ * works around the issue where prebuildify doesn't properly evaluate
9
+ * binding.gyp conditions for Windows architecture defines
10
+ *
11
+ * NOTE: if you don't include <windows.h> in your binding.gyp, this script is
12
+ * unnecessary.
13
+ */
14
+
15
+ // Get the current architecture and platform
16
+ const currentArch = arch(); // 'x64', 'arm64', etc.
17
+ const currentPlatform = platform(); // 'win32', 'darwin', 'linux'
18
+
19
+ console.log(`Building for platform: ${currentPlatform}, arch: ${currentArch}`);
20
+
21
+ // Set up environment variables to help node-gyp
22
+ const env = { ...process.env };
23
+
24
+ // Set architecture-specific defines for Windows
25
+ if (currentPlatform === "win32") {
26
+ // Try various environment variables that might work
27
+ env.npm_config_arch = currentArch;
28
+ env.npm_config_target_arch = currentArch;
29
+ env.PREBUILD_ARCH = currentArch;
30
+
31
+ // Try setting compiler flags directly
32
+ if (currentArch === "x64") {
33
+ env.CL = "/D_M_X64 /D_WIN64 /D_AMD64_";
34
+ } else if (currentArch === "arm64") {
35
+ env.CL = "/D_M_ARM64 /D_WIN64";
36
+ }
37
+ }
38
+
39
+ // Build the prebuildify command with explicit architecture
40
+ const args = [
41
+ "--napi",
42
+ "--tag-libc",
43
+ "--strip",
44
+ "--arch",
45
+ currentArch,
46
+ "--platform",
47
+ currentPlatform,
48
+ ];
49
+
50
+ // Add any additional arguments passed to this script
51
+ if (process.argv.length > 2) {
52
+ args.push(...process.argv.slice(2));
53
+ }
54
+
55
+ console.log(`Running: prebuildify ${args.join(" ")}`);
56
+ if (currentPlatform === "win32" && env.CL) {
57
+ console.log(`CL environment variable: ${env.CL}`);
58
+ }
59
+
60
+ // Spawn prebuildify with the arguments
61
+ const child = spawn("prebuildify", args, {
62
+ stdio: "inherit",
63
+ shell: true,
64
+ env,
65
+ });
66
+
67
+ child.on("error", (error) => {
68
+ console.error("Failed to start prebuildify:", error);
69
+ process.exit(1);
70
+ });
71
+
72
+ child.on("exit", (code) => {
73
+ if (code !== 0) {
74
+ console.error(`prebuildify exited with code ${code}`);
75
+ process.exit(code || 1);
76
+ }
77
+ });
@@ -0,0 +1,70 @@
1
+ import { execSync } from "node:child_process";
2
+ import { rmSync } from "node:fs";
3
+ import { platform } from "node:os";
4
+ import { exit } from "node:process";
5
+
6
+ const isLinux = platform() === "linux";
7
+ const isMacOS = platform() === "darwin";
8
+
9
+ function run({
10
+ cmd,
11
+ desc,
12
+ exitOnFail: shouldExit = true,
13
+ }: {
14
+ cmd: string;
15
+ desc: string;
16
+ exitOnFail?: boolean;
17
+ }) {
18
+ console.log(`\n▶ ${desc ?? cmd}`);
19
+ try {
20
+ execSync(cmd, { stdio: "inherit" });
21
+ } catch (error) {
22
+ console.error(`✗ Failed: ${desc ?? cmd}: ` + error);
23
+ if (shouldExit) exit(1);
24
+ }
25
+ }
26
+
27
+ run({ cmd: "npm install", desc: "Installing dependencies" });
28
+ run({ cmd: "npm run update", desc: "Updating dependencies" });
29
+ rmSync("package-lock.json", { force: true });
30
+ run({ cmd: "npm install", desc: "Updating dependencies" });
31
+ run({ cmd: "npm run clean", desc: "Start fresh" });
32
+ run({ cmd: "npm run fmt", desc: "Formatting code" });
33
+ run({ cmd: "npm run lint", desc: "Running linting checks" });
34
+ run({ cmd: "npm run docs", desc: "TypeDoc generation" });
35
+ run({ cmd: "npm run build:dist", desc: "Building distribution files" });
36
+
37
+ // Detect if we're using glibc (vs musl)
38
+ // Check process.report for musl loader - if not found, assume glibc
39
+ const isGlibc = (() => {
40
+ if (!isLinux) return false;
41
+ const report = process.report?.getReport() as any;
42
+ return !report?.sharedObjects?.some((lib: string) => /ld-musl/.test(lib));
43
+ })();
44
+
45
+ // Build native module with portable GLIBC
46
+ if (isLinux && isGlibc) {
47
+ run({
48
+ cmd: "npm run build:linux-glibc",
49
+ desc: "Building native project with portable GLIBC",
50
+ });
51
+ } else {
52
+ // Clean old native builds to ensure fresh compilation
53
+ run({ cmd: "npm run clean:native", desc: "Cleaning old native builds" });
54
+ run({ cmd: "npm run build:native", desc: "Building native module" });
55
+ }
56
+
57
+ run({ cmd: "npm run tests", desc: "Running tests in ESM & CJS mode" });
58
+
59
+ // Platform-specific checks
60
+ if (isLinux || isMacOS) {
61
+ // Remove stale compile_commands.json to ensure it's regenerated with current settings
62
+ rmSync("compile_commands.json", { force: true });
63
+ run({ cmd: "npm run lint:native", desc: "Running clang-tidy" });
64
+ }
65
+
66
+ // Run comprehensive memory tests (cross-platform)
67
+ // This includes Windows debug memory check on Windows
68
+ run({ cmd: "npm run check:memory", desc: "Comprehensive memory tests" });
69
+
70
+ console.log("\n✅ All precommit checks passed!");
@@ -76,7 +76,8 @@ fi
76
76
 
77
77
  # Build the native module
78
78
  echo "Building with AddressSanitizer..."
79
- npm run configure:native
79
+ npm run setup:native
80
+ npm run clean:native
80
81
  npm run node-gyp-rebuild
81
82
 
82
83
  # Run tests and capture output
@@ -148,4 +149,9 @@ if [[ "$VERBOSE" -eq 1 ]] && grep -q "Stats:" "$OUTPUT_FILE"; then
148
149
  grep -A 20 "Stats:" "$OUTPUT_FILE" | head -20
149
150
  fi
150
151
 
152
+ # Clean build artifacts to ensure no ASAN-compiled code remains
153
+ echo -e "\n${YELLOW}Cleaning build artifacts...${NC}"
154
+ npm run clean:native > /dev/null 2>&1
155
+ echo -e "${GREEN}✓ Build artifacts cleaned${NC}"
156
+
151
157
  exit $EXIT_CODE
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // scripts/configure.mjs
3
+ // scripts/setup-native.mjs
4
+
5
+ // This script sets up the config.gypi file to include GIO support when available.
6
+ // It should be run before building native modules with node-gyp.
4
7
 
5
8
  import { execSync } from "node:child_process";
6
9
  import { writeFileSync } from "node:fs";
package/src/binding.cpp CHANGED
@@ -93,6 +93,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
93
93
  return exports;
94
94
  }
95
95
 
96
- NODE_API_MODULE(node_fs_meta, Init)
96
+ NODE_API_MODULE(fs_metadata, Init)
97
97
 
98
98
  } // namespace
@@ -18,12 +18,6 @@ inline std::string CreateErrorMessage(const char *operation, int error) {
18
18
  " failed with error: " + std::to_string(error);
19
19
  }
20
20
 
21
- // Human-readable error with code (used by most callers)
22
- inline std::string CreateErrorMessageWithStrerror(const char *operation,
23
- int error) {
24
- return std::string(strerror(error)) + " (" + std::to_string(error) + ")";
25
- }
26
-
27
21
  // Convenience function for common pattern: "operation failed for 'path': error"
28
22
  inline std::string CreatePathErrorMessage(const char *operation,
29
23
  const std::string &path, int error) {
@@ -8,6 +8,8 @@ struct VolumeMetadataOptions {
8
8
  std::string mountPoint; // Required mount point path
9
9
  uint32_t timeoutMs = 5000; // Optional timeout with default
10
10
  std::string device; // Optional device path
11
+ bool skipNetworkVolumes =
12
+ false; // Skip detailed info for network volumes to avoid blocking
11
13
 
12
14
  static VolumeMetadataOptions FromObject(const Napi::Object &obj) {
13
15
  VolumeMetadataOptions options;
@@ -25,6 +27,10 @@ struct VolumeMetadataOptions {
25
27
  if (obj.Has("device")) {
26
28
  options.device = obj.Get("device").As<Napi::String>();
27
29
  }
30
+ if (obj.Has("skipNetworkVolumes")) {
31
+ options.skipNetworkVolumes =
32
+ obj.Get("skipNetworkVolumes").As<Napi::Boolean>().Value();
33
+ }
28
34
 
29
35
  return options;
30
36
  }