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
33 changes: 33 additions & 0 deletions examples/yarn-resolver-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env node

import { makeUse } from '../use.mjs';

// Create a use function with yarn resolver
const use = await makeUse({
specifierResolver: 'yarn'
});

console.log('🧶 Testing yarn resolver functionality...\n');

try {
console.log('1. Testing basic package resolution with [email protected]');
const lodash = await use('[email protected]');
console.log(` ✅ Success! Lodash version: ${lodash.VERSION}`);
console.log(` 📦 Package has ${Object.keys(lodash).length} exports\n`);

console.log('2. Testing scoped package resolution with @octokit/core@latest');
const octokitCore = await use('@octokit/core@latest');
console.log(` ✅ Success! Octokit Core loaded`);
console.log(` 📦 Octokit class available: ${typeof octokitCore.Octokit === 'function'}\n`);

console.log('3. Testing subpath resolution with [email protected]/helpers');
const yargHelpers = await use('[email protected]/helpers');
console.log(` ✅ Success! Yargs helpers loaded`);
console.log(` 📦 hideBin function available: ${typeof yargHelpers.hideBin === 'function'}\n`);

console.log('🎉 All yarn resolver tests passed! The yarn resolver is working correctly.');

} catch (error) {
console.error('❌ Error testing yarn resolver:', error.message);
console.error('Full error:', error);
}
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-m",
"version": "8.13.6",
"version": "8.14.0",
"description": "use-m: dynamically import any JavaScript module",
"type": "module",
"main": "use.cjs",
Expand Down
24 changes: 24 additions & 0 deletions tests/resolvers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,28 @@ describe(`${moduleName} resolvers tests`, () => {
const packagePath = await bun('yargs@latest/helpers', resolve);
expect(packagePath).toMatch(/node_modules\/yargs-v-latest\/helpers/);
});

test(`${moduleName} yarn resolver resolves package path`, async () => {
const { yarn } = resolvers;
const packagePath = await yarn('[email protected]', resolve);
expect(packagePath).toMatch(/node_modules\/lodash/);
});

test(`${moduleName} yarn resolver resolves scoped package path for @octokit/[email protected]`, async () => {
const { yarn } = resolvers;
const packagePath = await yarn('@octokit/[email protected]', resolve);
expect(packagePath).toMatch(/node_modules\/@octokit\/core/);
});

test(`${moduleName} yarn resolver resolves yargs/helpers`, async () => {
const { yarn } = resolvers;
const packagePath = await yarn('[email protected]/helpers', resolve);
expect(packagePath).toMatch(/node_modules\/yargs\/helpers/);
});

test(`${moduleName} yarn resolver resolves yargs@latest/helpers`, async () => {
const { yarn } = resolvers;
const packagePath = await yarn('yargs@latest/helpers', resolve);
expect(packagePath).toMatch(/node_modules\/yargs\/helpers/);
});
});
132 changes: 132 additions & 0 deletions use.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,138 @@ const resolvers = {
const resolvedPath = `https://jspm.dev/${packageName}${version ? `@${version}` : ''}${modulePath}`;
return resolvedPath;
},
yarn: async (moduleSpecifier, pathResolver) => {
const path = await import('node:path');
const { exec } = await import('node:child_process');
const { promisify } = await import('node:util');
const { stat, readFile } = await import('node:fs/promises');
const execAsync = promisify(exec);

if (!pathResolver) {
throw new Error('Failed to get the current resolver.');
}

const fileExists = async (filePath) => {
try {
const stats = await stat(filePath);
return stats.isFile();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return false;
}
};

const directoryExists = async (directoryPath) => {
try {
const stats = await stat(directoryPath);
return stats.isDirectory();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return false;
}
};

const tryResolveModule = async (packagePath) => {
try {
return await pathResolver(packagePath);
} catch (error) {
if (error.code !== 'MODULE_NOT_FOUND') {
throw error;
}

if (await directoryExists(packagePath)) {
const directoryName = path.basename(packagePath);
const resolvedPath = await tryResolveModule(path.join(packagePath, directoryName));
if (resolvedPath) {
return resolvedPath;
}

const packageJsonPath = path.join(packagePath, 'package.json');
if (await fileExists(packageJsonPath)) {
const packageJson = await readFile(packageJsonPath, 'utf8');
const parsed = JSON.parse(packageJson);
const exp = parsed.exports;
if (exp) {
let target = null;
if (typeof exp === 'string') {
target = exp;
} else {
const root = exp['.'] ?? exp;
if (typeof root === 'string') {
target = root;
} else if (root && typeof root === 'object') {
target = root.import || root.default || root.require || root.module || root.browser || null;
}
}
if (typeof target === 'string') {
const updatedPath = path.join(packagePath, target);
return await tryResolveModule(updatedPath);
}
}
}

return null;
}

return null;
}
};

const ensurePackageInstalled = async ({ packageName, version }) => {
let globalDir = '';
try {
const { stdout } = await execAsync('yarn global dir');
globalDir = stdout.trim();
} catch (error) {
throw new Error('Failed to get yarn global directory. Make sure yarn is installed.', { cause: error });
}

const globalModulesPath = path.join(globalDir, 'node_modules');
const packagePath = path.join(globalModulesPath, packageName);

if (await directoryExists(packagePath)) {
if (version === 'latest') {
return packagePath;
}

// Check if installed version matches requested version
try {
const packageJsonPath = path.join(packagePath, 'package.json');
if (await fileExists(packageJsonPath)) {
const packageJson = await readFile(packageJsonPath, 'utf8');
const parsed = JSON.parse(packageJson);
if (parsed.version === version) {
return packagePath;
}
}
} catch {
// If we can't read version, reinstall
}
}

try {
const packageSpec = version === 'latest' ? packageName : `${packageName}@${version}`;
await execAsync(`yarn global add ${packageSpec}`, { stdio: 'ignore' });
} catch (error) {
throw new Error(`Failed to install ${packageName}@${version} globally with yarn.`, { cause: error });
}

return packagePath;
};

const { packageName, version, modulePath } = parseModuleSpecifier(moduleSpecifier);
const packagePath = await ensurePackageInstalled({ packageName, version });
const packageModulePath = modulePath ? path.join(packagePath, modulePath) : packagePath;
const resolvedPath = await tryResolveModule(packageModulePath);
if (!resolvedPath) {
throw new Error(`Failed to resolve the path to '${moduleSpecifier}' from '${packageModulePath}'.`);
}
return resolvedPath;
},
}

const baseUse = async (modulePath) => {
Expand Down
132 changes: 132 additions & 0 deletions use.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,138 @@ export const resolvers = {
const resolvedPath = `https://jspm.dev/${packageName}${version ? `@${version}` : ''}${modulePath}`;
return resolvedPath;
},
yarn: async (moduleSpecifier, pathResolver) => {
const path = await import('node:path');
const { exec } = await import('node:child_process');
const { promisify } = await import('node:util');
const { stat, readFile } = await import('node:fs/promises');
const execAsync = promisify(exec);

if (!pathResolver) {
throw new Error('Failed to get the current resolver.');
}

const fileExists = async (filePath) => {
try {
const stats = await stat(filePath);
return stats.isFile();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return false;
}
};

const directoryExists = async (directoryPath) => {
try {
const stats = await stat(directoryPath);
return stats.isDirectory();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return false;
}
};

const tryResolveModule = async (packagePath) => {
try {
return await pathResolver(packagePath);
} catch (error) {
if (error.code !== 'MODULE_NOT_FOUND') {
throw error;
}

if (await directoryExists(packagePath)) {
const directoryName = path.basename(packagePath);
const resolvedPath = await tryResolveModule(path.join(packagePath, directoryName));
if (resolvedPath) {
return resolvedPath;
}

const packageJsonPath = path.join(packagePath, 'package.json');
if (await fileExists(packageJsonPath)) {
const packageJson = await readFile(packageJsonPath, 'utf8');
const parsed = JSON.parse(packageJson);
const exp = parsed.exports;
if (exp) {
let target = null;
if (typeof exp === 'string') {
target = exp;
} else {
const root = exp['.'] ?? exp;
if (typeof root === 'string') {
target = root;
} else if (root && typeof root === 'object') {
target = root.import || root.default || root.require || root.module || root.browser || null;
}
}
if (typeof target === 'string') {
const updatedPath = path.join(packagePath, target);
return await tryResolveModule(updatedPath);
}
}
}

return null;
}

return null;
}
};

const ensurePackageInstalled = async ({ packageName, version }) => {
let globalDir = '';
try {
const { stdout } = await execAsync('yarn global dir');
globalDir = stdout.trim();
} catch (error) {
throw new Error('Failed to get yarn global directory. Make sure yarn is installed.', { cause: error });
}

const globalModulesPath = path.join(globalDir, 'node_modules');
const packagePath = path.join(globalModulesPath, packageName);

if (await directoryExists(packagePath)) {
if (version === 'latest') {
return packagePath;
}

// Check if installed version matches requested version
try {
const packageJsonPath = path.join(packagePath, 'package.json');
if (await fileExists(packageJsonPath)) {
const packageJson = await readFile(packageJsonPath, 'utf8');
const parsed = JSON.parse(packageJson);
if (parsed.version === version) {
return packagePath;
}
}
} catch {
// If we can't read version, reinstall
}
}

try {
const packageSpec = version === 'latest' ? packageName : `${packageName}@${version}`;
await execAsync(`yarn global add ${packageSpec}`, { stdio: 'ignore' });
} catch (error) {
throw new Error(`Failed to install ${packageName}@${version} globally with yarn.`, { cause: error });
}

return packagePath;
};

const { packageName, version, modulePath } = parseModuleSpecifier(moduleSpecifier);
const packagePath = await ensurePackageInstalled({ packageName, version });
const packageModulePath = modulePath ? path.join(packagePath, modulePath) : packagePath;
const resolvedPath = await tryResolveModule(packageModulePath);
if (!resolvedPath) {
throw new Error(`Failed to resolve the path to '${moduleSpecifier}' from '${packageModulePath}'.`);
}
return resolvedPath;
},
}

export const baseUse = async (modulePath) => {
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1915,11 +1915,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

fsevents@^2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==

function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
Expand Down
Loading