diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a7ef7c2..3ff67ab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -129,10 +129,10 @@ jobs: - name: Create GitHub Release if: steps.check_tag.outputs.exists == 'true' - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: v${{ needs.check-version.outputs.version }} - release_name: v${{ needs.check-version.outputs.version }} + name: v${{ needs.check-version.outputs.version }} body_path: release_notes.md draft: false prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..60cb236 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,177 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- CONTRIBUTING.md with comprehensive contribution guidelines +- CHANGELOG.md to track version history +- Explicit "files" field in package.json for better npm publish control +- Better error messages with contextual information and error chaining + +### Fixed +- **CRITICAL**: Removed debug console.log statements in production code (use.mjs, use.cjs) +- **CRITICAL**: Fixed license field from "UNLICENSED" to "Unlicense" in package.json +- Fixed race condition in global use singleton by using promise-based initialization +- Fixed inconsistent file extension filtering in stack trace parsing (now checks use.mjs, use.cjs, and use.js) +- Fixed error suppression in loader.js with proper error chaining +- Updated deprecated GitHub Action from actions/create-release@v1 to softprops/action-gh-release@v2 +- Replaced process.env.HOME with os.homedir() for better cross-platform compatibility + +### Changed +- Improved error handling in npm and bun resolvers with better context +- Clarified TODOs in network-imports examples with explanatory comments + +## [8.13.7] - 2025-09-14 + +### Fixed +- Fix Bun and Deno test failures for fs/promises compatibility +- Add --allow-write permission for Deno tests +- Implement signature-matching async wrapper for Bun/Deno fs/promises +- Replace fs/promises runtime error with util.promisify fallback +- Add runtime validation for fs/promises to detect callback vs promise-based functions + +### Changed +- Improve fs/promises compatibility layer for Bun and Deno runtimes +- Restore accidentally deleted test-adapter files + +## [8.13.6] - 2025-08-18 + +### Changed +- Refactor CI/CD workflow to improve version checking and job dependencies +- Remove CI/CD badge from README +- Increase timeout for test completion in browser environment + +## [8.13.5] - 2025-08-18 + +### Added +- Main example script demonstrating universal module usage + +## [8.13.4] - 2025-08-18 + +### Changed +- Various improvements and bug fixes + +## [8.13.3] - 2025-08-17 + +### Changed +- Code quality and stability improvements + +## [8.13.2] - 2025-08-17 + +### Changed +- Performance and reliability enhancements + +## [8.13.1] - 2025-08-17 + +### Added +- Support for GitPod and GitHub Codespaces + +### Changed +- Documentation improvements + +## [8.13.0] - 2025-08-16 + +### Added +- Enhanced cross-runtime support +- Improved built-in module emulation + +### Changed +- Better error messages across all resolvers +- Improved stack trace parsing logic + +## [8.12.0] - 2025-08-15 + +### Added +- Relative path resolution support for ./ and ../ +- JSON file import support with import assertions + +### Changed +- Improved caller context detection +- Enhanced browser environment support + +## [8.11.0] - 2025-08-14 + +### Added +- Multiple CDN resolver support (esm.sh, unpkg, jsdelivr, skypack, jspm) +- CDN-specific package name transformations + +### Fixed +- CDN URL construction for scoped packages + +## [8.10.0] - 2025-08-13 + +### Added +- Bun runtime support with global package installation +- Deno runtime support with esm.sh CDN integration + +### Changed +- Improved runtime detection logic +- Enhanced module resolution for different environments + +## [8.9.0] - 2025-08-12 + +### Added +- Built-in module emulation for 25+ Node.js modules +- Cross-environment module support (browser, Node.js, Bun, Deno) +- Promise-based module variants (fs/promises, dns/promises, etc.) + +### Changed +- Improved module export handling +- Better default export detection + +## [8.8.0] - 2025-08-11 + +### Added +- CLI tool with version and loader-path commands +- Module loader hooks for custom resolution + +### Changed +- Enhanced npm resolver with better package.json handling +- Improved version resolution for "latest" packages + +## [8.7.0] - 2025-08-10 + +### Added +- Test adapter for cross-runtime testing (Jest, Bun, Deno) +- Comprehensive test suite with 34+ test files +- Examples for multiple use cases + +### Changed +- Improved documentation with more examples +- Better error messages for common issues + +## [8.6.0] - 2025-08-09 + +### Added +- Support for loading multiple versions of the same library +- Global installation with version-specific aliases + +### Changed +- Enhanced module specifier parsing +- Improved version string handling + +## Earlier Versions + +For earlier version history, please see the [GitHub releases page](https://github.com/link-foundation/use-m/releases). + +--- + +## Version Guidelines + +- **Major version (X.0.0)**: Breaking changes that require user code updates +- **Minor version (0.X.0)**: New features, backward-compatible functionality +- **Patch version (0.0.X)**: Bug fixes, documentation updates, internal improvements + +## Types of Changes + +- **Added**: New features +- **Changed**: Changes in existing functionality +- **Deprecated**: Soon-to-be removed features +- **Removed**: Removed features +- **Fixed**: Bug fixes +- **Security**: Security vulnerability fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..be63e6c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,358 @@ +# Contributing to use-m + +Thank you for your interest in contributing to use-m! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Project Structure](#project-structure) +- [Making Changes](#making-changes) +- [Testing](#testing) +- [Coding Standards](#coding-standards) +- [Submitting Changes](#submitting-changes) +- [Reporting Issues](#reporting-issues) + +## Code of Conduct + +This project follows a simple code of conduct: be respectful, collaborative, and constructive in all interactions. + +## Getting Started + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR_USERNAME/use-m.git + cd use-m + ``` +3. **Add the upstream repository**: + ```bash + git remote add upstream https://github.com/link-foundation/use-m.git + ``` + +## Development Setup + +### Prerequisites + +- Node.js 20.x or later +- npm (comes with Node.js) +- Optionally: Bun and/or Deno for cross-runtime testing + +### Install Dependencies + +```bash +npm install +``` + +### Verify Installation + +Run the test suite to ensure everything is working: + +```bash +npm test +``` + +For cross-runtime testing: + +```bash +# Node.js (default) +npm test + +# Bun +bun test + +# Deno +deno test --allow-net --allow-env --allow-run --allow-read --allow-write --allow-sys +``` + +## Project Structure + +``` +/home/user/use-m/ +├── use.mjs # ES Module version (main implementation) +├── use.cjs # CommonJS version +├── use.js # Universal version (browser/eval) +├── cli.mjs # CLI tool +├── loader.js # Node.js module loader hooks +├── test-adapter.mjs # Cross-runtime test framework adapter +├── test-adapter.cjs # CommonJS version of test adapter +├── tests/ # Test suite (34 test files) +├── examples/ # Usage examples (15 examples) +├── .github/workflows/ # CI/CD configuration +└── docs/ # Documentation files +``` + +### Key Files + +- **use.mjs**: Primary implementation file. Changes here should be synchronized to use.cjs +- **use.cjs**: CommonJS variant of use.mjs +- **tests/**: Each test file has both .mjs and .cjs versions +- **examples/**: Real-world usage examples + +## Making Changes + +### Branching Strategy + +1. **Create a feature branch** from `main`: + ```bash + git checkout main + git pull upstream main + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** in logical, atomic commits + +3. **Keep your branch updated**: + ```bash + git fetch upstream + git rebase upstream/main + ``` + +### Types of Changes + +- **Bug fixes**: Target specific issues with clear reproduction steps +- **Features**: New functionality or enhancements +- **Documentation**: Improvements to README, examples, or inline docs +- **Tests**: Additional test coverage or test improvements +- **Refactoring**: Code improvements without changing functionality + +### Important Notes + +- **Dual-file updates**: Changes to `use.mjs` typically require corresponding updates to `use.cjs` +- **Cross-runtime compatibility**: Ensure changes work on Node.js, Bun, and Deno +- **Examples**: Update examples if you change public APIs + +## Testing + +### Running Tests + +```bash +# Run all tests (Node.js with Jest) +npm test + +# Run specific test file +npm test -- use.test.mjs + +# Run tests in watch mode +npm test -- --watch + +# Run tests with coverage +npm test -- --coverage +``` + +### Writing Tests + +- Use the cross-runtime test adapter: `import { describe, test, expect } from '../test-adapter.mjs'` +- Create both `.mjs` and `.cjs` versions of new tests +- Test both success and error paths +- Include edge cases + +Example test structure: + +```javascript +import { describe, test, expect } from '../test-adapter.mjs'; +import { use } from '../use.mjs'; + +describe('Feature name', () => { + test('should do something specific', async () => { + const result = await use('lodash@4.17.21'); + expect(result).toBeDefined(); + }); + + test('should handle errors correctly', async () => { + await expect(async () => { + await use('invalid-package-name-12345'); + }).rejects.toThrow(); + }); +}); +``` + +### Testing Guidelines + +- Test all supported runtimes (Node.js, Bun, Deno) when possible +- Test both ESM and CommonJS module formats +- Verify built-in module emulation +- Test relative path resolution +- Test error conditions and edge cases + +## Coding Standards + +### JavaScript Style + +- **Modern JavaScript**: Use ES6+ features (const/let, arrow functions, async/await) +- **No semicolons**: Follow the existing semicolon-less style +- **2 spaces**: For indentation +- **Clear variable names**: Prefer descriptive names over abbreviations +- **Comments**: Add comments for complex logic, but prefer self-documenting code + +### Best Practices + +1. **Error handling**: Always provide helpful error messages with context + ```javascript + throw new Error(`Failed to resolve '${moduleName}'`, { cause: error }); + ``` + +2. **Async/await**: Prefer async/await over promise chains for consistency + +3. **Validation**: Validate inputs early and provide clear error messages + +4. **Cross-runtime**: Use feature detection instead of runtime-specific checks when possible + +### Documentation + +- Add JSDoc comments for exported functions +- Update README.md if adding new features +- Create or update examples for significant changes +- Document breaking changes clearly + +## Submitting Changes + +### Before Submitting + +1. **Run tests**: Ensure all tests pass + ```bash + npm test + ``` + +2. **Check for linting errors** (if applicable) + +3. **Update documentation**: README, examples, inline comments + +4. **Verify cross-runtime compatibility**: Test on Node.js, and ideally Bun/Deno + +### Pull Request Process + +1. **Push your changes** to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +2. **Create a pull request** on GitHub: + - Use a clear, descriptive title + - Reference any related issues (e.g., "Fixes #123") + - Describe what changed and why + - Include examples of new functionality + - Note any breaking changes + +3. **PR description template**: + ```markdown + ## Summary + Brief description of changes + + ## Motivation + Why is this change needed? + + ## Changes + - List of specific changes + - Another change + + ## Testing + - How was this tested? + - What edge cases were considered? + + ## Breaking Changes + - List any breaking changes (if applicable) + + ## Related Issues + Fixes #issue_number + ``` + +4. **Respond to feedback**: Be open to suggestions and iterate on your PR + +### Commit Messages + +Write clear, descriptive commit messages: + +``` +feat: add support for CDN fallback mechanism + +- Implement fallback to multiple CDNs if primary fails +- Add tests for CDN fallback scenarios +- Update documentation with new CDN options + +Fixes #45 +``` + +**Commit message format**: +- `feat:` for new features +- `fix:` for bug fixes +- `docs:` for documentation changes +- `test:` for test additions/changes +- `refactor:` for code refactoring +- `chore:` for maintenance tasks + +## Reporting Issues + +### Bug Reports + +Include: +- **Description**: Clear description of the bug +- **Steps to reproduce**: Minimal reproduction steps +- **Expected behavior**: What you expected to happen +- **Actual behavior**: What actually happened +- **Environment**: OS, Node.js version, runtime (Node/Bun/Deno) +- **Code sample**: Minimal code that reproduces the issue + +### Feature Requests + +Include: +- **Use case**: Why is this feature needed? +- **Proposed solution**: How should it work? +- **Alternatives considered**: Other approaches you've thought about +- **Examples**: Code examples of how the feature would be used + +## Development Tips + +### Debugging + +1. **Add temporary logging**: + ```javascript + console.log('Debug:', variable); + ``` + +2. **Use debugger**: + ```javascript + debugger; // Use with Node.js --inspect flag + ``` + +3. **Run specific tests**: + ```bash + npm test -- use.test.mjs + ``` + +### Testing Locally + +Test your changes in a real project: + +```bash +cd /path/to/your/test/project +npm link /path/to/use-m +``` + +### Cross-Runtime Testing + +```bash +# Node.js +npm test + +# Bun +bun test + +# Deno +deno test --allow-net --allow-env --allow-run --allow-read --allow-write --allow-sys +``` + +## Questions? + +- **Issues**: Open an issue on GitHub +- **Discussions**: Use GitHub Discussions for questions and ideas +- **Email**: Contact the maintainers + +## License + +By contributing to use-m, you agree that your contributions will be licensed under the Unlicense (public domain). + +--- + +Thank you for contributing to use-m! Your efforts help make this project better for everyone. diff --git a/README.md b/README.md index c32a4aa..15044bd 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ It may be useful for standalone scripts that do not require a `package.json`. Al - [Installation](#installation) - [CommonJS](#commonjs) - [ES Modules](#es-modules) + - [Security Considerations](#security-considerations) - [Examples](#examples) - [Questions and issues](#questions-and-issues) - [Contributing](#contributing) @@ -367,6 +368,67 @@ const _ = await use('lodash@4.17.21'); console.log(`_.add(1, 2) = ${_.add(1, 2)}`); ``` +## Security Considerations + +### Arbitrary Code Execution + +**Important**: When using `use-m` with npm/bun resolvers in Node.js or Bun environments, packages are installed globally using `npm install -g` or `bun add -g`. This means: + +- **Install scripts are executed**: npm packages can run arbitrary code during installation via install scripts +- **Trust required**: Only use packages from trusted sources +- **Malicious packages**: A compromised or malicious package could execute harmful code on your system + +### Best Practices + +1. **Pin versions**: Always specify exact versions instead of using `latest`: + ```javascript + // Good - specific version + const _ = await use('lodash@4.17.21'); + + // Risky - could download new, potentially compromised version + const _ = await use('lodash@latest'); + ``` + +2. **Trust your dependencies**: Only import packages from trusted maintainers and npm organizations + +3. **Use CDN resolver in untrusted environments**: For browser or Deno environments, packages are loaded from CDNs without running install scripts: + ```javascript + // Browser - loads from CDN, no install scripts + const { use } = await import("https://unpkg.com/use-m/use.mjs"); + const _ = await use('lodash@4.17.21'); + ``` + +4. **Review package contents**: Check package source code before using, especially for critical applications + +5. **Use in development/scripts**: `use-m` is ideal for development scripts, exploratory coding, and rapid prototyping where convenience outweighs strict security requirements + +### CDN Security + +When using CDN resolvers (browser, Deno), be aware that: + +- **CDN compromise**: If a CDN is compromised, malicious code could be served +- **No integrity checking**: By default, there's no Subresource Integrity (SRI) verification +- **Network dependency**: Your application depends on CDN availability + +### Eval Security + +Some examples use `eval()` for convenience in interactive shells and browsers. Be aware: + +- `eval()` executes arbitrary code +- Only use with trusted sources +- The `use-m` library code is short, unminified, and has no dependencies - you can [review it yourself](https://unpkg.com/use-m/use.js) +- For production code, prefer standard imports without `eval()` + +### Recommendations by Use Case + +| Use Case | Recommendation | Security Level | +|----------|---------------|----------------| +| Development scripts | ✅ Safe to use | Medium | +| Rapid prototyping | ✅ Safe to use | Medium | +| Interactive shell/REPL | ✅ Safe to use | Medium | +| Production applications | ⚠️ Use with caution | Low-Medium | +| Security-critical apps | ❌ Not recommended | Low | + ## Examples You can check out [usage examples source code](https://github.com/link-foundation/use-m/tree/main/examples). You can also explore our [tests](https://github.com/link-foundation/use-m/tree/main/tests) to get even more examples. @@ -377,7 +439,13 @@ If you have any questions or issues, [please write us on GitHub](https://github. ## Contributing -We welcome contributions! To contribute please [open Pull Request](https://github.com/link-foundation/use-m/pulls) with any suggested changes. +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on: +- Setting up the development environment +- Running tests across different runtimes +- Code style and standards +- Submitting pull requests + +For quick contributions, feel free to [open a Pull Request](https://github.com/link-foundation/use-m/pulls) with your suggested changes. ## License diff --git a/examples/network-imports/index.cjs b/examples/network-imports/index.cjs index e9cfacf..e65598f 100644 --- a/examples/network-imports/index.cjs +++ b/examples/network-imports/index.cjs @@ -1,4 +1,6 @@ -import("../../use.mjs") // TODO: replace with import("https://unpkg.com/use-m/use.mjs") +// Using local import for development/testing +// In production, use: import("https://unpkg.com/use-m/use.mjs") +import("../../use.mjs") .then(async ({ use }) => { const _ = await use("lodash@4.17.21"); console.log(`_.add(1, 2) = ${_.add(1, 2)}`); diff --git a/examples/network-imports/index.mjs b/examples/network-imports/index.mjs index becd4c5..1987385 100644 --- a/examples/network-imports/index.mjs +++ b/examples/network-imports/index.mjs @@ -1,4 +1,5 @@ -const { use } = await import("https://unpkg.com/use-m/use.mjs"); // TODO: replace with import("https://unpkg.com/use-m/use.mjs") -// const { use } = await import('https://esm.sh/use-m@8.8.0'); +// Import use-m directly from CDN without local installation +const { use } = await import("https://unpkg.com/use-m/use.mjs"); +// Alternative CDN: const { use } = await import('https://esm.sh/use-m'); const _ = await use('lodash@4.17.21'); console.log(`_.add(1, 2) = ${_.add(1, 2)}`); \ No newline at end of file diff --git a/loader.js b/loader.js index 5a3c5cc..0ece895 100644 --- a/loader.js +++ b/loader.js @@ -32,10 +32,11 @@ export async function resolve(specifier, context, defaultResolve) { return { url: pathToFileURL(resolvedUrl).href }; } catch (error) { if (defaultResolveError) { - console.error(error); - throw defaultResolveError; + throw new Error(`Failed to resolve module: ${specifier}`, { + cause: { resolverError: error, defaultError: defaultResolveError } + }); } else { - throw error; + throw new Error(`Failed to resolve module: ${specifier}`, { cause: error }); } } } diff --git a/package.json b/package.json index 545fac8..f192afd 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,18 @@ "unpkg" ], "author": "link-foundation", - "license": "UNLICENSED", + "license": "Unlicense", + "files": [ + "use.mjs", + "use.cjs", + "use.js", + "cli.mjs", + "loader.js", + "test-adapter.mjs", + "test-adapter.cjs", + "README.md", + "LICENSE" + ], "devDependencies": { "@babel/preset-env": "^7.28.3", "babel-jest": "^29.7.0", diff --git a/use.cjs b/use.cjs index afb7abd..99e843b 100644 --- a/use.cjs +++ b/use.cjs @@ -1,10 +1,17 @@ const extractCallerContext = (stack) => { + // Helper to check if a path is a use-m file + const isUseMFile = (path) => { + return path.endsWith('/use.mjs') || + path.endsWith('/use.cjs') || + path.endsWith('/use.js'); + }; + // In browser environment, use the current document URL as fallback if (typeof window !== 'undefined' && window.location) { // For inline scripts in HTML, use the document's URL // This will be the fallback if we can't extract from stack const documentUrl = window.location.href; - + // Try to extract from stack first, but we'll fallback to document URL if (!stack) return documentUrl; } else if (!stack) { @@ -12,20 +19,20 @@ const extractCallerContext = (stack) => { } const lines = stack.split('\n'); - // Look for the first file that isn't use.mjs - skip the first few frames + // Look for the first file that isn't use.mjs/use.cjs/use.js - skip the first few frames // to get past our internal function calls for (const line of lines) { - // Skip the first few frames which are internal to use.mjs + // Skip the first few frames which are internal to use-m if (line.includes('extractCallerContext') || line.includes('_use') || line.includes('makeUse') || - line.includes('') && line.includes('/use.mjs')) { + (line.includes('') && (line.includes('/use.mjs') || line.includes('/use.cjs') || line.includes('/use.js')))) { continue; } // Try to match http(s):// URLs for browser environments let match = line.match(/https?:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs') && !match[0].endsWith('/use.js')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -33,7 +40,7 @@ const extractCallerContext = (stack) => { // Try to match file:// URLs match = line.match(/file:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -59,13 +66,13 @@ const extractCallerContext = (stack) => { // For Node/Deno, try to match absolute paths (improved to handle more cases) match = line.match(/at\s+(?:Object\.\s+)?(?:async\s+)?[(]?(\/[^\s:)]+\.(?:m?js|json))(?::\d+:\d+)?\)?/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + match[1]; } // Alternative pattern for Jest and other environments match = line.match(/at\s+[^(]*\(([^)]+\.(?:m?js|json)):\d+:\d+\)/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + (match[1].startsWith('/') ? match[1] : '/' + match[1]); } } @@ -130,7 +137,6 @@ const supportedBuiltins = { // For Bun and Deno, use a different approach since their node:fs/promises may not be fully compatible if (runtime === 'Bun' || runtime === 'Deno') { - console.log(`[${runtime}] Using promisify fallback for fs/promises compatibility`); try { const fs = await import('node:fs'); const { promisify } = await import('node:util'); @@ -199,9 +205,7 @@ const supportedBuiltins = { if (fs.opendir) promisifiedFs.opendir = safePromisify(fs.opendir, 2); if (fs.statfs) promisifiedFs.statfs = safePromisify(fs.statfs, 2); if (fs.watch) promisifiedFs.watch = fs.watch.bind(fs); // watch is not callback-based - - console.log(`[${runtime}] Fallback mkdir.length:`, promisifiedFs.mkdir?.length); - console.log(`[${runtime}] Fallback mkdir.constructor.name:`, promisifiedFs.mkdir?.constructor.name); + return { default: promisifiedFs, ...promisifiedFs }; } catch (error) { throw new Error(`Failed to create fs/promises fallback for ${runtime}: ${error.message}`, { cause: error }); @@ -478,7 +482,7 @@ const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } // Attempt to resolve paths like 'yargs@18.0.0/helpers' to 'yargs-v-18.0.0/helpers/helpers.mjs' @@ -608,7 +612,7 @@ const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } if (await directoryExists(packagePath)) { @@ -659,10 +663,11 @@ const resolvers = { } catch (error) { // In CI or fresh environments, the global directory might not exist // Try to get the default Bun install path - const home = process.env.HOME || process.env.USERPROFILE; - if (home) { + try { + const os = await import('node:os'); + const home = os.homedir(); binDir = path.join(home, '.bun', 'bin'); - } else { + } catch (osError) { throw new Error('Unable to determine Bun global directory.', { cause: error }); } } @@ -854,7 +859,7 @@ const makeUse = async (options) => { }; } -let __use = null; +let __usePromise = null; const use = async (moduleSpecifier) => { const stack = new Error().stack; @@ -877,16 +882,18 @@ const use = async (moduleSpecifier) => { // Capture the caller context here, before entering makeUse const callerContext = bunCallerContext || extractCallerContext(stack); - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return __use(moduleSpecifier, callerContext); + const useInstance = await __usePromise; + return useInstance(moduleSpecifier, callerContext); } use.all = async (...moduleSpecifiers) => { - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return Promise.all(moduleSpecifiers.map(__use)); + const useInstance = await __usePromise; + return Promise.all(moduleSpecifiers.map(useInstance)); } module.exports = { diff --git a/use.js b/use.js index 00d03a0..34ff44c 100644 --- a/use.js +++ b/use.js @@ -1,10 +1,17 @@ const extractCallerContext = (stack) => { + // Helper to check if a path is a use-m file + const isUseMFile = (path) => { + return path.endsWith('/use.mjs') || + path.endsWith('/use.cjs') || + path.endsWith('/use.js'); + }; + // In browser environment, use the current document URL as fallback if (typeof window !== 'undefined' && window.location) { // For inline scripts in HTML, use the document's URL // This will be the fallback if we can't extract from stack const documentUrl = window.location.href; - + // Try to extract from stack first, but we'll fallback to document URL if (!stack) return documentUrl; } else if (!stack) { @@ -12,20 +19,20 @@ const extractCallerContext = (stack) => { } const lines = stack.split('\n'); - // Look for the first file that isn't use.mjs - skip the first few frames + // Look for the first file that isn't use.mjs/use.cjs/use.js - skip the first few frames // to get past our internal function calls for (const line of lines) { - // Skip the first few frames which are internal to use.mjs + // Skip the first few frames which are internal to use-m if (line.includes('extractCallerContext') || line.includes('_use') || line.includes('makeUse') || - line.includes('') && line.includes('/use.mjs')) { + (line.includes('') && (line.includes('/use.mjs') || line.includes('/use.cjs') || line.includes('/use.js')))) { continue; } // Try to match http(s):// URLs for browser environments let match = line.match(/https?:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs') && !match[0].endsWith('/use.js')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -33,7 +40,7 @@ const extractCallerContext = (stack) => { // Try to match file:// URLs match = line.match(/file:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -59,13 +66,13 @@ const extractCallerContext = (stack) => { // For Node/Deno, try to match absolute paths (improved to handle more cases) match = line.match(/at\s+(?:Object\.\s+)?(?:async\s+)?[(]?(\/[^\s:)]+\.(?:m?js|json))(?::\d+:\d+)?\)?/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + match[1]; } // Alternative pattern for Jest and other environments match = line.match(/at\s+[^(]*\(([^)]+\.(?:m?js|json)):\d+:\d+\)/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + (match[1].startsWith('/') ? match[1] : '/' + match[1]); } } @@ -130,7 +137,6 @@ const supportedBuiltins = { // For Bun and Deno, use a different approach since their node:fs/promises may not be fully compatible if (runtime === 'Bun' || runtime === 'Deno') { - console.log(`[${runtime}] Using promisify fallback for fs/promises compatibility`); try { const fs = await import('node:fs'); const { promisify } = await import('node:util'); @@ -199,9 +205,7 @@ const supportedBuiltins = { if (fs.opendir) promisifiedFs.opendir = safePromisify(fs.opendir, 2); if (fs.statfs) promisifiedFs.statfs = safePromisify(fs.statfs, 2); if (fs.watch) promisifiedFs.watch = fs.watch.bind(fs); // watch is not callback-based - - console.log(`[${runtime}] Fallback mkdir.length:`, promisifiedFs.mkdir?.length); - console.log(`[${runtime}] Fallback mkdir.constructor.name:`, promisifiedFs.mkdir?.constructor.name); + return { default: promisifiedFs, ...promisifiedFs }; } catch (error) { throw new Error(`Failed to create fs/promises fallback for ${runtime}: ${error.message}`, { cause: error }); @@ -478,7 +482,7 @@ const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } // Attempt to resolve paths like 'yargs@18.0.0/helpers' to 'yargs-v-18.0.0/helpers/helpers.mjs' @@ -608,7 +612,7 @@ const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } if (await directoryExists(packagePath)) { @@ -659,10 +663,11 @@ const resolvers = { } catch (error) { // In CI or fresh environments, the global directory might not exist // Try to get the default Bun install path - const home = process.env.HOME || process.env.USERPROFILE; - if (home) { + try { + const os = await import('node:os'); + const home = os.homedir(); binDir = path.join(home, '.bun', 'bin'); - } else { + } catch (osError) { throw new Error('Unable to determine Bun global directory.', { cause: error }); } } @@ -869,7 +874,7 @@ const makeUse = async (options) => { }; } -let __use = null; +let __usePromise = null; const use = async (moduleSpecifier) => { const stack = new Error().stack; @@ -892,16 +897,18 @@ const use = async (moduleSpecifier) => { // Capture the caller context here, before entering makeUse const callerContext = bunCallerContext || extractCallerContext(stack); - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return __use(moduleSpecifier, callerContext); + const useInstance = await __usePromise; + return useInstance(moduleSpecifier, callerContext); } use.all = async (...moduleSpecifiers) => { - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return Promise.all(moduleSpecifiers.map(__use)); + const useInstance = await __usePromise; + return Promise.all(moduleSpecifiers.map(useInstance)); } makeUse.parseModuleSpecifier = parseModuleSpecifier; diff --git a/use.mjs b/use.mjs index b7cba8b..628f462 100644 --- a/use.mjs +++ b/use.mjs @@ -1,10 +1,17 @@ const extractCallerContext = (stack) => { + // Helper to check if a path is a use-m file + const isUseMFile = (path) => { + return path.endsWith('/use.mjs') || + path.endsWith('/use.cjs') || + path.endsWith('/use.js'); + }; + // In browser environment, use the current document URL as fallback if (typeof window !== 'undefined' && window.location) { // For inline scripts in HTML, use the document's URL // This will be the fallback if we can't extract from stack const documentUrl = window.location.href; - + // Try to extract from stack first, but we'll fallback to document URL if (!stack) return documentUrl; } else if (!stack) { @@ -12,20 +19,20 @@ const extractCallerContext = (stack) => { } const lines = stack.split('\n'); - // Look for the first file that isn't use.mjs - skip the first few frames + // Look for the first file that isn't use.mjs/use.cjs/use.js - skip the first few frames // to get past our internal function calls for (const line of lines) { - // Skip the first few frames which are internal to use.mjs + // Skip the first few frames which are internal to use-m if (line.includes('extractCallerContext') || line.includes('_use') || line.includes('makeUse') || - line.includes('') && line.includes('/use.mjs')) { + (line.includes('') && (line.includes('/use.mjs') || line.includes('/use.cjs') || line.includes('/use.js')))) { continue; } // Try to match http(s):// URLs for browser environments let match = line.match(/https?:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs') && !match[0].endsWith('/use.js')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -33,7 +40,7 @@ const extractCallerContext = (stack) => { // Try to match file:// URLs match = line.match(/file:\/\/[^\s)]+/); - if (match && !match[0].endsWith('/use.mjs')) { + if (match && !isUseMFile(match[0])) { // Remove line:column numbers if present const url = match[0].replace(/:\d+:\d+$/, ''); return url; @@ -59,13 +66,13 @@ const extractCallerContext = (stack) => { // For Node/Deno, try to match absolute paths (improved to handle more cases) match = line.match(/at\s+(?:Object\.\s+)?(?:async\s+)?[(]?(\/[^\s:)]+\.(?:m?js|json))(?::\d+:\d+)?\)?/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + match[1]; } // Alternative pattern for Jest and other environments match = line.match(/at\s+[^(]*\(([^)]+\.(?:m?js|json)):\d+:\d+\)/); - if (match && !match[1].endsWith('/use.mjs') && !match[1].includes('node_modules')) { + if (match && !isUseMFile(match[1]) && !match[1].includes('node_modules')) { return 'file://' + (match[1].startsWith('/') ? match[1] : '/' + match[1]); } } @@ -130,7 +137,6 @@ const supportedBuiltins = { // For Bun and Deno, use a different approach since their node:fs/promises may not be fully compatible if (runtime === 'Bun' || runtime === 'Deno') { - console.log(`[${runtime}] Using promisify fallback for fs/promises compatibility`); try { const fs = await import('node:fs'); const { promisify } = await import('node:util'); @@ -199,9 +205,7 @@ const supportedBuiltins = { if (fs.opendir) promisifiedFs.opendir = safePromisify(fs.opendir, 2); if (fs.statfs) promisifiedFs.statfs = safePromisify(fs.statfs, 2); if (fs.watch) promisifiedFs.watch = fs.watch.bind(fs); // watch is not callback-based - - console.log(`[${runtime}] Fallback mkdir.length:`, promisifiedFs.mkdir?.length); - console.log(`[${runtime}] Fallback mkdir.constructor.name:`, promisifiedFs.mkdir?.constructor.name); + return { default: promisifiedFs, ...promisifiedFs }; } catch (error) { throw new Error(`Failed to create fs/promises fallback for ${runtime}: ${error.message}`, { cause: error }); @@ -478,7 +482,7 @@ export const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } // Attempt to resolve paths like 'yargs@18.0.0/helpers' to 'yargs-v-18.0.0/helpers/helpers.mjs' @@ -608,7 +612,7 @@ export const resolvers = { return await pathResolver(packagePath); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { - throw error; + throw new Error(`Failed to resolve module '${packagePath}'`, { cause: error }); } if (await directoryExists(packagePath)) { @@ -659,10 +663,11 @@ export const resolvers = { } catch (error) { // In CI or fresh environments, the global directory might not exist // Try to get the default Bun install path - const home = process.env.HOME || process.env.USERPROFILE; - if (home) { + try { + const os = await import('node:os'); + const home = os.homedir(); binDir = path.join(home, '.bun', 'bin'); - } else { + } catch (osError) { throw new Error('Unable to determine Bun global directory.', { cause: error }); } } @@ -857,7 +862,7 @@ export const makeUse = async (options) => { }; } -let __use = null; +let __usePromise = null; const _use = async (moduleSpecifier) => { const stack = new Error().stack; @@ -880,15 +885,17 @@ const _use = async (moduleSpecifier) => { // Capture the caller context here, before entering makeUse const callerContext = bunCallerContext || extractCallerContext(stack); - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return __use(moduleSpecifier, callerContext); + const useInstance = await __usePromise; + return useInstance(moduleSpecifier, callerContext); } _use.all = async (...moduleSpecifiers) => { - if (!__use) { - __use = await makeUse(); + if (!__usePromise) { + __usePromise = makeUse(); } - return Promise.all(moduleSpecifiers.map(__use)); + const useInstance = await __usePromise; + return Promise.all(moduleSpecifiers.map(useInstance)); } export const use = _use;