diff --git a/examples/yarn-resolver-demo.mjs b/examples/yarn-resolver-demo.mjs new file mode 100755 index 0000000..5827b74 --- /dev/null +++ b/examples/yarn-resolver-demo.mjs @@ -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 lodash@4.17.21'); + const lodash = await use('lodash@4.17.21'); + 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 yargs@17.7.2/helpers'); + const yargHelpers = await use('yargs@17.7.2/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); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f5b1536..3728513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "use-m", - "version": "8.13.2", + "version": "8.13.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "use-m", - "version": "8.13.2", + "version": "8.13.6", "license": "UNLICENSED", "bin": { "use": "cli.mjs" diff --git a/package.json b/package.json index 806d1a6..5325f0a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/resolvers.test.mjs b/tests/resolvers.test.mjs index d681915..2ef3283 100644 --- a/tests/resolvers.test.mjs +++ b/tests/resolvers.test.mjs @@ -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('lodash@4.17.21', resolve); + expect(packagePath).toMatch(/node_modules\/lodash/); + }); + + test(`${moduleName} yarn resolver resolves scoped package path for @octokit/core@6.1.5`, async () => { + const { yarn } = resolvers; + const packagePath = await yarn('@octokit/core@6.1.5', resolve); + expect(packagePath).toMatch(/node_modules\/@octokit\/core/); + }); + + test(`${moduleName} yarn resolver resolves yargs/helpers`, async () => { + const { yarn } = resolvers; + const packagePath = await yarn('yargs@17.7.2/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/); + }); }); \ No newline at end of file diff --git a/use.cjs b/use.cjs index 3917be3..165df17 100644 --- a/use.cjs +++ b/use.cjs @@ -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) => { diff --git a/use.mjs b/use.mjs index 250fe81..ad25a52 100644 --- a/use.mjs +++ b/use.mjs @@ -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) => { diff --git a/yarn.lock b/yarn.lock index 7c44d21..f286c44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"