Skip to content

Commit c2fc236

Browse files
committed
feat(dlx): add executable type detection utilities
Add utilities to detect whether a path points to a Node.js package or native binary executable. Features: - detectExecutableType() generic entry point for any path - detectDlxExecutableType() DLX cache specific detection - detectLocalExecutableType() local filesystem detection - isNodeJsExtension() validates .js, .mjs, .cjs extensions - isNodePackage() simplified helper for package detection - isNativeBinary() simplified helper for binary detection Detection strategies: - DLX cache: Check for node_modules/ directory presence - Local paths: Check for package.json with bin field, then file extension Includes comprehensive test suite with 25 passing tests.
1 parent b070a78 commit c2fc236

3 files changed

Lines changed: 610 additions & 0 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@
227227
"types": "./dist/dlx/cache.d.ts",
228228
"default": "./dist/dlx/cache.js"
229229
},
230+
"./dlx/detect": {
231+
"types": "./dist/dlx/detect.d.ts",
232+
"default": "./dist/dlx/detect.js"
233+
},
230234
"./dlx/dir": {
231235
"types": "./dist/dlx/dir.d.ts",
232236
"default": "./dist/dlx/dir.js"

src/dlx/detect.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* @fileoverview Executable type detection for DLX and local filesystem paths.
3+
*
4+
* Provides utilities to detect whether a path is a Node.js package or native
5+
* binary executable. Supports both DLX cache paths and local filesystem paths.
6+
*
7+
* Key Functions:
8+
* - detectExecutableType: Generic entry point for any path
9+
* - detectDlxExecutableType: DLX cache specific detection
10+
* - detectLocalExecutableType: Local filesystem specific detection
11+
*
12+
* Detection Strategies:
13+
* - DLX cache: Check for node_modules/ directory
14+
* - Local paths: Check for package.json with bin field, then file extension
15+
*/
16+
17+
import { isInSocketDlx } from './paths'
18+
import { getSocketDlxDir } from '../paths/socket'
19+
20+
let _fs: typeof import('node:fs') | undefined
21+
let _path: typeof import('node:path') | undefined
22+
23+
/**
24+
* Lazily load the fs module to avoid Webpack errors.
25+
* @private
26+
*/
27+
/*@__NO_SIDE_EFFECTS__*/
28+
function getFs() {
29+
if (_fs === undefined) {
30+
_fs = /*@__PURE__*/ require('fs')
31+
}
32+
return _fs as typeof import('node:fs')
33+
}
34+
35+
/**
36+
* Lazily load the path module to avoid Webpack errors.
37+
* @private
38+
*/
39+
/*@__NO_SIDE_EFFECTS__*/
40+
function getPath() {
41+
if (_path === undefined) {
42+
_path = /*@__PURE__*/ require('path')
43+
}
44+
return _path as typeof import('node:path')
45+
}
46+
47+
/**
48+
* Node.js script file extensions.
49+
*/
50+
const NODE_JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs'] as const)
51+
52+
export type ExecutableType = 'package' | 'binary' | 'unknown'
53+
54+
export interface ExecutableDetectionResult {
55+
type: ExecutableType
56+
method: 'dlx-cache' | 'package-json' | 'file-extension'
57+
packageJsonPath?: string
58+
inDlxCache?: boolean
59+
}
60+
61+
/**
62+
* Detect if a path is a Node.js package or native binary executable.
63+
* Works for both DLX cache paths and local filesystem paths.
64+
*
65+
* Detection strategy:
66+
* 1. If in DLX cache: Use detectDlxExecutableType()
67+
* 2. Otherwise: Use detectLocalExecutableType()
68+
*
69+
* @param filePath - Path to executable (DLX cache or local filesystem)
70+
* @returns Detection result with type, method, and metadata
71+
*
72+
* @example
73+
* ```typescript
74+
* const result = detectExecutableType('/path/to/tool')
75+
* if (result.type === 'package') {
76+
* spawnNode([filePath, ...args])
77+
* } else {
78+
* spawn(filePath, args)
79+
* }
80+
* ```
81+
*/
82+
export function detectExecutableType(
83+
filePath: string,
84+
): ExecutableDetectionResult {
85+
if (isInSocketDlx(filePath)) {
86+
return detectDlxExecutableType(filePath)
87+
}
88+
89+
return detectLocalExecutableType(filePath)
90+
}
91+
92+
/**
93+
* Detect executable type for paths in DLX cache.
94+
* Uses filesystem structure (node_modules/ presence).
95+
*
96+
* @param filePath - Path within DLX cache (~/.socket/_dlx/)
97+
* @returns Detection result
98+
*/
99+
export function detectDlxExecutableType(
100+
filePath: string,
101+
): ExecutableDetectionResult {
102+
const fs = getFs()
103+
const path = getPath()
104+
105+
const dlxDir = getSocketDlxDir()
106+
const absolutePath = path.resolve(filePath)
107+
const relativePath = path.relative(dlxDir, absolutePath)
108+
const cacheKey = relativePath.split(path.sep)[0]
109+
const cacheDir = path.join(dlxDir, cacheKey)
110+
111+
// Packages have node_modules/, binaries don't.
112+
if (fs.existsSync(path.join(cacheDir, 'node_modules'))) {
113+
return {
114+
type: 'package',
115+
method: 'dlx-cache',
116+
inDlxCache: true,
117+
}
118+
}
119+
120+
return {
121+
type: 'binary',
122+
method: 'dlx-cache',
123+
inDlxCache: true,
124+
}
125+
}
126+
127+
/**
128+
* Detect executable type for local filesystem paths.
129+
* Uses package.json and file extension checks.
130+
*
131+
* @param filePath - Local filesystem path (not in DLX cache)
132+
* @returns Detection result
133+
*/
134+
export function detectLocalExecutableType(
135+
filePath: string,
136+
): ExecutableDetectionResult {
137+
const fs = getFs()
138+
const path = getPath()
139+
140+
// Check 1: Look for package.json with bin field.
141+
const packageJsonPath = findPackageJson(filePath)
142+
if (packageJsonPath !== undefined) {
143+
try {
144+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
145+
// If it has a bin field, it's a Node.js package.
146+
if (packageJson.bin) {
147+
return {
148+
type: 'package',
149+
method: 'package-json',
150+
packageJsonPath,
151+
inDlxCache: false,
152+
}
153+
}
154+
} catch {
155+
// Invalid package.json, fall through.
156+
}
157+
}
158+
159+
// Check 2: File extension fallback.
160+
const ext = path.extname(filePath).toLowerCase()
161+
if (NODE_JS_EXTENSIONS.has(ext as '.js' | '.mjs' | '.cjs')) {
162+
return {
163+
type: 'package',
164+
method: 'file-extension',
165+
inDlxCache: false,
166+
}
167+
}
168+
169+
return {
170+
type: 'binary',
171+
method: 'file-extension',
172+
inDlxCache: false,
173+
}
174+
}
175+
176+
/**
177+
* Find package.json in the directory containing the file or parent directories.
178+
*
179+
* @param filePath - Path to search from
180+
* @returns Path to package.json if found, undefined otherwise
181+
*/
182+
function findPackageJson(filePath: string): string | undefined {
183+
const fs = getFs()
184+
const path = getPath()
185+
186+
let currentDir = path.dirname(path.resolve(filePath))
187+
const root = path.parse(currentDir).root
188+
189+
while (currentDir !== root) {
190+
const packageJsonPath = path.join(currentDir, 'package.json')
191+
if (fs.existsSync(packageJsonPath)) {
192+
return packageJsonPath
193+
}
194+
195+
currentDir = path.dirname(currentDir)
196+
}
197+
198+
return undefined
199+
}
200+
201+
/**
202+
* Check if a file extension indicates a Node.js script.
203+
*
204+
* @param filePath - Path to check
205+
* @returns True if file has .js, .mjs, or .cjs extension
206+
*/
207+
export function isNodeJsExtension(filePath: string): boolean {
208+
const path = getPath()
209+
const ext = path.extname(filePath).toLowerCase()
210+
return NODE_JS_EXTENSIONS.has(ext as '.js' | '.mjs' | '.cjs')
211+
}
212+
213+
/**
214+
* Simplified helper: Is this a Node.js package?
215+
*
216+
* @param filePath - Path to check
217+
* @returns True if detected as Node.js package
218+
*/
219+
export function isNodePackage(filePath: string): boolean {
220+
return detectExecutableType(filePath).type === 'package'
221+
}
222+
223+
/**
224+
* Simplified helper: Is this a native binary executable?
225+
*
226+
* @param filePath - Path to check
227+
* @returns True if detected as native binary (not Node.js package)
228+
*/
229+
export function isNativeBinary(filePath: string): boolean {
230+
return detectExecutableType(filePath).type === 'binary'
231+
}

0 commit comments

Comments
 (0)