From 49c40ac255ffdc6555a754274a7d0fd9ff43295a Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:45:57 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #23 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/23 --- 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..e25002b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/use-m/issues/23 +Your prepared branch: issue-23-4cfef6db +Your prepared working directory: /tmp/gh-issue-solver-1757479554602 + +Proceed. \ No newline at end of file From 1935fec8062e9648d8b8f119143120ded3a88443 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:46:14 +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 e25002b..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/use-m/issues/23 -Your prepared branch: issue-23-4cfef6db -Your prepared working directory: /tmp/gh-issue-solver-1757479554602 - -Proceed. \ No newline at end of file From 1b8e78ce63b267d43c0d7332a9bc213b6e5db095 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 07:59:41 +0300 Subject: [PATCH 3/3] Implement freeze() function for version freezing (issue #23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add experimental freeze function that allows freezing dependency versions after first execution by modifying source code using regex replacements. Features: - freeze('lodash') gets latest version from npm and replaces with use('lodash@4.17.21') - Works with both ESM (.mjs) and CommonJS (.cjs) modules - Supports await syntax preservation - Captures caller context from stack traces for accurate file modification - Includes comprehensive error handling for edge cases Examples: - const _ = await freeze('lodash') → const _ = await use('lodash@4.17.21') - Works with scoped packages like @scope/package 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/freeze/freeze-example.mjs | 8 ++ examples/freeze/freeze-test-final.mjs | 11 ++ examples/freeze/test-freeze-cjs.cjs | 11 ++ examples/freeze/test-simple-git.mjs | 9 ++ package-lock.json | 4 +- use.cjs | 155 ++++++++++++++++++++++++++ use.js | 144 ++++++++++++++++++++++++ use.mjs | 145 ++++++++++++++++++++++++ yarn.lock | 5 - 9 files changed, 485 insertions(+), 7 deletions(-) create mode 100644 examples/freeze/freeze-example.mjs create mode 100644 examples/freeze/freeze-test-final.mjs create mode 100644 examples/freeze/test-freeze-cjs.cjs create mode 100644 examples/freeze/test-simple-git.mjs diff --git a/examples/freeze/freeze-example.mjs b/examples/freeze/freeze-example.mjs new file mode 100644 index 0000000..a677365 --- /dev/null +++ b/examples/freeze/freeze-example.mjs @@ -0,0 +1,8 @@ +import { use, freeze } from '../../use.mjs'; + +// This freeze call should be replaced with a specific version +const _ = await freeze('lodash'); + +console.log(`Successfully loaded lodash: ${typeof _}`); +console.log(`_.add(1, 2) = ${_.add(1, 2)}`); +console.log(`_.capitalize('hello world') = ${_.capitalize('hello world')}`); \ No newline at end of file diff --git a/examples/freeze/freeze-test-final.mjs b/examples/freeze/freeze-test-final.mjs new file mode 100644 index 0000000..417540b --- /dev/null +++ b/examples/freeze/freeze-test-final.mjs @@ -0,0 +1,11 @@ +import { use, freeze } from '../../use.mjs'; + +console.log('About to call freeze...'); + +// This freeze call should be replaced with a specific version +const _ = await use('lodash@4.17.21'); + +console.log('Freeze completed successfully!'); +console.log(`Successfully loaded lodash: ${typeof _}`); +console.log(`_.add(1, 2) = ${_.add(1, 2)}`); +console.log(`_.capitalize('hello world') = ${_.capitalize('hello world')}`); \ No newline at end of file diff --git a/examples/freeze/test-freeze-cjs.cjs b/examples/freeze/test-freeze-cjs.cjs new file mode 100644 index 0000000..c74c948 --- /dev/null +++ b/examples/freeze/test-freeze-cjs.cjs @@ -0,0 +1,11 @@ +const { use, freeze } = require('../../use.cjs'); + +(async () => { + console.log('Testing freeze with CommonJS and chalk...'); + + // This should get frozen to the latest version + const chalk = await use('chalk@5.6.2'); + + console.log('Successfully loaded chalk:', typeof chalk); + console.log('chalk has red method:', typeof chalk.red === 'function'); +})().catch(console.error); \ No newline at end of file diff --git a/examples/freeze/test-simple-git.mjs b/examples/freeze/test-simple-git.mjs new file mode 100644 index 0000000..3c8c57d --- /dev/null +++ b/examples/freeze/test-simple-git.mjs @@ -0,0 +1,9 @@ +import { use, freeze } from '../../use.mjs'; + +console.log('Testing freeze with simple-git...'); + +// This should get frozen to the latest version +const simpleGit = await use('simple-git@3.28.0'); + +console.log('Successfully loaded simple-git:', typeof simpleGit); +console.log('simple-git is a function:', typeof simpleGit === 'function'); \ 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/use.cjs b/use.cjs index 3917be3..cbecd91 100644 --- a/use.cjs +++ b/use.cjs @@ -761,10 +761,165 @@ use.all = async (...moduleSpecifiers) => { return Promise.all(moduleSpecifiers.map(__use)); } +// Freeze function implementation +const freeze = (packageSpecifier) => { + // Capture the stack trace immediately when freeze is called, before any async operations + const initialError = new Error(); + const initialStack = initialError.stack || ''; + + // Return a Promise that will handle the version resolution and source code modification + return new Promise(async (resolve, reject) => { + try { + // Parse the package specifier to get just the package name + const { packageName } = parseModuleSpecifier(packageSpecifier); + + // Get the latest version from npm + const { spawn } = require('child_process'); + const { promisify } = require('util'); + const execAsync = promisify(spawn); + + let latestVersion; + try { + const npmViewProcess = spawn('npm', ['view', packageName, 'version'], { stdio: 'pipe' }); + let stdout = ''; + let stderr = ''; + + npmViewProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + npmViewProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + await new Promise((resolve, reject) => { + npmViewProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`npm view failed: ${stderr}`)); + } + }); + }); + + latestVersion = stdout.trim(); + } catch (error) { + throw new Error(`Failed to get latest version for ${packageName}: ${error.message}`); + } + + if (!latestVersion) { + throw new Error(`Could not determine latest version for ${packageName}`); + } + + // Get the calling file path from the initial stack trace (captured when freeze was called) + const stack = initialStack; + + // Custom caller context extraction for freeze function + const getFreezeCallerContext = (stack) => { + if (!stack) return null; + + const lines = stack.split('\n'); + + // Look for the first file that isn't use.mjs and isn't inside freeze function definition + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Skip internal framework calls + if (line.includes('ModuleJob') || + line.includes('ModuleLoader') || + line.includes('asyncRunEntryPointWithESMLoader') || + line.includes('processTicksAndRejections')) { + continue; + } + + // Skip any line that points to use.mjs (including freeze function) + if (line.includes('/use.mjs') || line.includes('/use.js') || line.includes('/use.cjs')) { + continue; + } + + // Try to match file:// URLs (ESM style) + let match = line.match(/file:\/\/([^\s)]+)/); + if (match) { + // Remove line:column numbers if present + const url = match[0].replace(/:\d+:\d+$/, ''); + return url; + } + + // Try to match CommonJS style paths in parentheses + match = line.match(/\(([^)]+\.(?:cjs|js|mjs)):\d+:\d+\)/); + if (match && match[1]) { + // Convert absolute path to file:// URL + const path = match[1]; + if (path.startsWith('/')) { + return 'file://' + path; + } + return path; + } + } + return null; + }; + + const callerContext = getFreezeCallerContext(stack); + + if (!callerContext) { + throw new Error('Could not determine the source file to modify'); + } + + let filePath; + if (callerContext.startsWith('file://')) { + filePath = callerContext.replace('file://', ''); + } else if (callerContext.startsWith('http://') || callerContext.startsWith('https://')) { + throw new Error('Cannot modify remote files. freeze() only works with local files.'); + } else { + filePath = callerContext; + } + + // Read the source file + const fs = require('fs').promises; + let sourceContent; + try { + sourceContent = await fs.readFile(filePath, 'utf8'); + } catch (error) { + throw new Error(`Could not read source file ${filePath}: ${error.message}`); + } + + // Create regex to find and replace freeze() calls (including await prefix) + const freezeRegex = new RegExp( + `(await\\s+)?freeze\\s*\\(\\s*['"\`]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]\\s*\\)`, + 'g' + ); + + const versionedSpecifier = `use('${packageName}@${latestVersion}')`; + const updatedContent = sourceContent.replace(freezeRegex, (match, awaitPrefix) => { + return (awaitPrefix || '') + versionedSpecifier; + }); + + if (updatedContent === sourceContent) { + throw new Error(`Could not find freeze('${packageName}') call in source file`); + } + + // Write the updated content back to the file + try { + await fs.writeFile(filePath, updatedContent, 'utf8'); + } catch (error) { + throw new Error(`Could not write updated source file ${filePath}: ${error.message}`); + } + + // Now import the module with the resolved version + const module = await use(`${packageName}@${latestVersion}`); + resolve(module); + + } catch (error) { + reject(error); + } + }); +}; + module.exports = { parseModuleSpecifier, resolvers, makeUse, baseUse, use, + freeze, }; \ No newline at end of file diff --git a/use.js b/use.js index c2dcb3b..162e932 100644 --- a/use.js +++ b/use.js @@ -776,10 +776,154 @@ use.all = async (...moduleSpecifiers) => { return Promise.all(moduleSpecifiers.map(__use)); } +// Freeze function implementation +const freeze = (packageSpecifier) => { + // Capture the stack trace immediately when freeze is called, before any async operations + const initialError = new Error(); + const initialStack = initialError.stack || ''; + + // Return a Promise that will handle the version resolution and source code modification + return new Promise(async (resolve, reject) => { + try { + // Parse the package specifier to get just the package name + const { packageName } = parseModuleSpecifier(packageSpecifier); + + // Get the latest version from npm + const { spawn } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(spawn); + + let latestVersion; + try { + const npmViewProcess = spawn('npm', ['view', packageName, 'version'], { stdio: 'pipe' }); + let stdout = ''; + let stderr = ''; + + npmViewProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + npmViewProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + await new Promise((resolve, reject) => { + npmViewProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`npm view failed: ${stderr}`)); + } + }); + }); + + latestVersion = stdout.trim(); + } catch (error) { + throw new Error(`Failed to get latest version for ${packageName}: ${error.message}`); + } + + if (!latestVersion) { + throw new Error(`Could not determine latest version for ${packageName}`); + } + + // Get the calling file path from the initial stack trace (captured when freeze was called) + const stack = initialStack; + + // Custom caller context extraction for freeze function + const getFreezeCallerContext = (stack) => { + if (!stack) return null; + + const lines = stack.split('\n'); + + // Look for the first file that isn't use.mjs and isn't inside freeze function definition + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Skip internal framework calls + if (line.includes('ModuleJob') || + line.includes('ModuleLoader') || + line.includes('asyncRunEntryPointWithESMLoader') || + line.includes('processTicksAndRejections')) { + continue; + } + + // Skip any line that points to use.mjs (including freeze function) + if (line.includes('/use.mjs') || line.includes('/use.js')) { + continue; + } + + // Try to match file:// URLs + let match = line.match(/file:\/\/([^\s)]+)/); + if (match) { + // Remove line:column numbers if present + const url = match[0].replace(/:\d+:\d+$/, ''); + return url; + } + } + return null; + }; + + const callerContext = getFreezeCallerContext(stack); + + if (!callerContext) { + throw new Error('Could not determine the source file to modify'); + } + + let filePath; + if (callerContext.startsWith('file://')) { + filePath = callerContext.replace('file://', ''); + } else if (callerContext.startsWith('http://') || callerContext.startsWith('https://')) { + throw new Error('Cannot modify remote files. freeze() only works with local files.'); + } else { + filePath = callerContext; + } + + // Read the source file + const fs = await import('fs').then(m => m.promises); + let sourceContent; + try { + sourceContent = await fs.readFile(filePath, 'utf8'); + } catch (error) { + throw new Error(`Could not read source file ${filePath}: ${error.message}`); + } + + // Create regex to find and replace freeze() calls (including await prefix) + const freezeRegex = new RegExp( + `(await\\s+)?freeze\\s*\\(\\s*['"\`]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]\\s*\\)`, + 'g' + ); + + const versionedSpecifier = `use('${packageName}@${latestVersion}')`; + const updatedContent = sourceContent.replace(freezeRegex, (match, awaitPrefix) => { + return (awaitPrefix || '') + versionedSpecifier; + }); + + if (updatedContent === sourceContent) { + throw new Error(`Could not find freeze('${packageName}') call in source file`); + } + + // Write the updated content back to the file + try { + await fs.writeFile(filePath, updatedContent, 'utf8'); + } catch (error) { + throw new Error(`Could not write updated source file ${filePath}: ${error.message}`); + } + + // Now import the module with the resolved version + const module = await use(`${packageName}@${latestVersion}`); + resolve(module); + + } catch (error) { + reject(error); + } + }); +}; + makeUse.parseModuleSpecifier = parseModuleSpecifier; makeUse.resolvers = resolvers; makeUse.makeUse = makeUse; makeUse.baseUse = baseUse; makeUse.use = use; +makeUse.freeze = freeze; makeUse \ No newline at end of file diff --git a/use.mjs b/use.mjs index 250fe81..3f726fc 100644 --- a/use.mjs +++ b/use.mjs @@ -763,4 +763,149 @@ _use.all = async (...moduleSpecifiers) => { } return Promise.all(moduleSpecifiers.map(__use)); } + +// Freeze function implementation +const freeze = (packageSpecifier) => { + // Capture the stack trace immediately when freeze is called, before any async operations + const initialError = new Error(); + const initialStack = initialError.stack || ''; + + // Return a Promise that will handle the version resolution and source code modification + return new Promise(async (resolve, reject) => { + try { + // Parse the package specifier to get just the package name + const { packageName } = parseModuleSpecifier(packageSpecifier); + + // Get the latest version from npm + const { spawn } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(spawn); + + let latestVersion; + try { + const npmViewProcess = spawn('npm', ['view', packageName, 'version'], { stdio: 'pipe' }); + let stdout = ''; + let stderr = ''; + + npmViewProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + npmViewProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + await new Promise((resolve, reject) => { + npmViewProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`npm view failed: ${stderr}`)); + } + }); + }); + + latestVersion = stdout.trim(); + } catch (error) { + throw new Error(`Failed to get latest version for ${packageName}: ${error.message}`); + } + + if (!latestVersion) { + throw new Error(`Could not determine latest version for ${packageName}`); + } + + // Get the calling file path from the initial stack trace (captured when freeze was called) + const stack = initialStack; + + // Custom caller context extraction for freeze function + const getFreezeCallerContext = (stack) => { + if (!stack) return null; + + const lines = stack.split('\n'); + + // Look for the first file that isn't use.mjs and isn't inside freeze function definition + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Skip internal framework calls + if (line.includes('ModuleJob') || + line.includes('ModuleLoader') || + line.includes('asyncRunEntryPointWithESMLoader') || + line.includes('processTicksAndRejections')) { + continue; + } + + // Skip any line that points to use.mjs (including freeze function) + if (line.includes('/use.mjs')) { + continue; + } + + // Try to match file:// URLs + let match = line.match(/file:\/\/([^\s)]+)/); + if (match) { + // Remove line:column numbers if present + const url = match[0].replace(/:\d+:\d+$/, ''); + return url; + } + } + return null; + }; + + const callerContext = getFreezeCallerContext(stack); + + if (!callerContext) { + throw new Error('Could not determine the source file to modify'); + } + + let filePath; + if (callerContext.startsWith('file://')) { + filePath = callerContext.replace('file://', ''); + } else if (callerContext.startsWith('http://') || callerContext.startsWith('https://')) { + throw new Error('Cannot modify remote files. freeze() only works with local files.'); + } else { + filePath = callerContext; + } + + // Read the source file + const fs = await import('fs').then(m => m.promises); + let sourceContent; + try { + sourceContent = await fs.readFile(filePath, 'utf8'); + } catch (error) { + throw new Error(`Could not read source file ${filePath}: ${error.message}`); + } + + // Create regex to find and replace freeze() calls (including await prefix) + const freezeRegex = new RegExp( + `(await\\s+)?freeze\\s*\\(\\s*['"\`]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]\\s*\\)`, + 'g' + ); + + const versionedSpecifier = `use('${packageName}@${latestVersion}')`; + const updatedContent = sourceContent.replace(freezeRegex, (match, awaitPrefix) => { + return (awaitPrefix || '') + versionedSpecifier; + }); + + if (updatedContent === sourceContent) { + throw new Error(`Could not find freeze('${packageName}') call in source file`); + } + + // Write the updated content back to the file + try { + await fs.writeFile(filePath, updatedContent, 'utf8'); + } catch (error) { + throw new Error(`Could not write updated source file ${filePath}: ${error.message}`); + } + + // Now import the module with the resolved version + const module = await _use(`${packageName}@${latestVersion}`); + resolve(module); + + } catch (error) { + reject(error); + } + }); +}; + export const use = _use; +export { freeze }; 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"