From ea42c3cc22473b62eb8afdcc5abf7fa1d9493eb9 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:47:32 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #19 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/use-m/issues/19 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5f96a4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/use-m/issues/19 +Your prepared branch: issue-19-f9451b6d +Your prepared working directory: /tmp/gh-issue-solver-1757479650350 + +Proceed. \ No newline at end of file From 32ae6300ddf9bfd0918a2dd6610ddf95d92a3970 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:47:48 +0300 Subject: [PATCH 2/3] Remove CLAUDE.md - PR created successfully --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 5f96a4f..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/use-m/issues/19 -Your prepared branch: issue-19-f9451b6d -Your prepared working directory: /tmp/gh-issue-solver-1757479650350 - -Proceed. \ No newline at end of file From a030f267e6f4b40f163c46a64660588ec736f951 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:56:05 +0300 Subject: [PATCH 3/3] Add yarn resolver support (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements support for yarn as a resolver, allowing use-m to install and resolve packages using yarn instead of npm. Features: - New yarn resolver that uses 'yarn global add' for package installation - Supports all module specifier formats (package@version, scoped packages, subpaths) - Follows yarn's global directory structure using 'yarn global dir' - Version checking to avoid unnecessary reinstalls - Comprehensive test coverage for yarn resolver functionality Changes: - Added yarn resolver to both use.mjs and use.cjs - Added yarn resolver tests to resolvers.test.mjs - Created yarn-resolver-demo.mjs example script - Bumped version to 8.14.0 Fixes #19 ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/yarn-resolver-demo.mjs | 33 ++++++++ package-lock.json | 4 +- package.json | 2 +- tests/resolvers.test.mjs | 24 ++++++ use.cjs | 132 ++++++++++++++++++++++++++++++++ use.mjs | 132 ++++++++++++++++++++++++++++++++ yarn.lock | 5 -- 7 files changed, 324 insertions(+), 8 deletions(-) create mode 100755 examples/yarn-resolver-demo.mjs 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"