Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions experiments/debug-resolution.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node

/**
* Debug script to trace the resolution logic
*/

import { parseModuleSpecifier } from '../use.mjs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';

const moduleSpecifier = 'yargs/helpers';
const { packageName, version, modulePath } = parseModuleSpecifier(moduleSpecifier);

console.log('Parsed module specifier:', { packageName, version, modulePath });

// This is what happens in the code:
const packagePath = '/home/hive/.nvm/versions/node/v20.19.5/lib/node_modules/yargs-v-latest';
const packageModulePath = modulePath ? path.join(packagePath, modulePath) : packagePath;

console.log('Package path:', packagePath);
console.log('Package module path:', packageModulePath);

// The problem: we're reading package.json from packageModulePath, but we should read from packagePath
const wrongPackageJsonPath = path.join(packageModulePath, 'package.json');
const correctPackageJsonPath = path.join(packagePath, 'package.json');

console.log('\nWrong package.json path:', wrongPackageJsonPath);
console.log('Correct package.json path:', correctPackageJsonPath);

try {
const packageJson = await readFile(correctPackageJsonPath, 'utf8');
const parsed = JSON.parse(packageJson);
console.log('\nExports field:', JSON.stringify(parsed.exports, null, 2));

// Check for subPath
const dottedSubPath = `.${modulePath}`;
console.log('\nLooking for:', dottedSubPath);
console.log('Found:', parsed.exports[dottedSubPath]);
} catch (error) {
console.error('Error:', error.message);
}
62 changes: 62 additions & 0 deletions experiments/root-cause-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Root Cause Analysis for Issue #47

## Problem
Cannot import sub-paths like 'yargs/helpers' - use-m fails to resolve the path.

## Error Message
```
Error: Failed to resolve the path to 'yargs/helpers' from '/home/hive/.nvm/versions/node/v20.19.5/lib/node_modules/yargs-v-latest/helpers'.
```

## Root Cause

### What Should Happen
When importing `yargs/helpers`:
1. Parse the module specifier into: packageName='yargs', version='latest', modulePath='/helpers'
2. Install package as `yargs-v-latest`
3. Read `yargs-v-latest/package.json`
4. Look up `./helpers` in the `exports` field
5. Resolve to the mapped file: `./helpers/helpers.mjs`
6. Import from `/node_modules/yargs-v-latest/helpers/helpers.mjs`

### What Actually Happens
1. ✓ Parse correctly: packageName='yargs', version='latest', modulePath='/helpers'
2. ✓ Install package as `yargs-v-latest`
3. ✓ Construct path: `packageModulePath = /node_modules/yargs-v-latest/helpers`
4. ✗ **BUG**: `tryResolveModule` tries to resolve the directory `/helpers` directly
5. ✗ When that fails, it reads package.json but only checks for the root "." export
6. ✗ It never checks if there's a sub-path export for "./helpers"
7. ✗ Returns null, causing the error

### Code Location
File: `use.mjs`, lines 476-522 (npm resolver) and lines 606-650 (bun resolver)

The `tryResolveModule` function:
- Only handles the root export (`exp['.']`)
- Does NOT handle sub-path exports like `exp['./helpers']`

### The Fix Needed
In the `tryResolveModule` function, when we have a modulePath (like '/helpers'):
1. Check if the exports field has a matching sub-path entry
2. If found, resolve to the mapped file
3. Otherwise, fall back to current behavior

### Example from yargs/package.json
```json
{
"exports": {
"./package.json": "./package.json",
"./helpers": "./helpers/helpers.mjs", // ← This needs to be checked!
"./browser": {
"types": "./browser.d.ts",
"import": "./browser.mjs"
},
".": "./index.mjs",
"./yargs": "./index.mjs"
}
}
```

When importing `yargs/helpers`:
- Current code: Only checks `exports["."]` → doesn't find "./helpers"
- Fixed code: Should check `exports["./helpers"]` → finds "./helpers/helpers.mjs"
44 changes: 44 additions & 0 deletions experiments/test-yargs-helpers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env node

/**
* Test script to reproduce issue #47: Cannot import sub-paths like 'yargs/helpers'
*
* This script tests importing yargs/helpers to verify the exports field handling.
*/

console.log('Testing yargs with use-m...\n');

// Import use-m from the local source
import { use } from '../use.mjs';

try {
console.log('1. Testing yargs main import...');
const yargs = await use('yargs');
console.log('✓ yargs imported successfully');
console.log(' Type:', typeof yargs);
} catch (error) {
console.error('✗ Failed to import yargs:', error.message);
console.error(' Stack:', error.stack);
process.exit(1);
}

try {
console.log('\n2. Testing yargs/helpers import...');
const helpers = await use('yargs/helpers');
console.log('✓ yargs/helpers imported successfully');
console.log(' Type:', typeof helpers);
console.log(' Has hideBin:', 'hideBin' in helpers);

// Test hideBin function
if (helpers.hideBin) {
const testArgs = ['node', 'script.js', 'arg1', 'arg2'];
const result = helpers.hideBin(testArgs);
console.log(' hideBin test:', JSON.stringify(result));
}
} catch (error) {
console.error('✗ Failed to import yargs/helpers:', error.message);
console.error(' Stack:', error.stack);
process.exit(1);
}

console.log('\n✅ All imports successful!');
9 changes: 6 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions tests/exports-field.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { use } from '../use.mjs';
import { describe, test, expect } from '../test-adapter.mjs';

// Mock jest object for Deno compatibility
const jest = typeof Deno !== 'undefined' ? { setTimeout: () => {} } : (await import('@jest/globals')).jest;

const moduleName = `[${import.meta.url.split('.').pop()} module]`;

jest.setTimeout(60000);

describe(`${moduleName} exports field handling tests`, () => {
// Test for issue #47: Cannot import sub-paths like 'yargs/helpers'
test(`${moduleName} should import yargs/helpers using exports field`, async () => {
const helpers = await use('yargs/helpers');
expect(helpers).toBeDefined();
expect(typeof helpers).toBe('object');
expect(typeof helpers.hideBin).toBe('function');
});

test(`${moduleName} should import yargs main module`, async () => {
const yargs = await use('yargs');
expect(yargs).toBeDefined();
expect(typeof yargs).toBe('object');
});

test(`${moduleName} should import yargs@17.7.2/helpers`, async () => {
const helpers = await use('yargs@17.7.2/helpers');
expect(helpers).toBeDefined();
expect(typeof helpers).toBe('object');
expect(typeof helpers.hideBin).toBe('function');
});

test(`${moduleName} should import yargs@18.0.0/helpers`, async () => {
const helpers = await use('yargs@18.0.0/helpers');
expect(helpers).toBeDefined();
expect(typeof helpers).toBe('object');
expect(typeof helpers.hideBin).toBe('function');
});

// Test the hideBin function actually works
test(`${moduleName} yargs/helpers hideBin should work correctly`, async () => {
const { hideBin } = await use('yargs/helpers');
const testArgs = ['node', 'script.js', 'arg1', 'arg2'];
const result = hideBin(testArgs);
expect(result).toEqual(['arg1', 'arg2']);
});

// Test that main package import still works
test(`${moduleName} should import @octokit/core using exports field`, async () => {
const { Octokit } = await use('@octokit/core@6.1.5');
expect(Octokit).toBeDefined();
expect(typeof Octokit).toBe('function');
});
});
Loading
Loading