diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ca6b45..09cf548 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runtime: ['node', 'bun', 'deno'] node-version: [20.x] diff --git a/examples/windows-fetch-workaround/test-with-polyfill.mjs b/examples/windows-fetch-workaround/test-with-polyfill.mjs new file mode 100755 index 0000000..461f197 --- /dev/null +++ b/examples/windows-fetch-workaround/test-with-polyfill.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +/** + * Example: Using use-m on Windows with fetch polyfill + * + * This demonstrates the workaround for issue #45 where fetch is not defined + * in certain Windows contexts (Git Bash, shebang execution). + * + * Solution: Import the fetch-polyfill before loading use-m + */ + +console.log('Platform:', process.platform); +console.log('Node version:', process.version); +console.log('Fetch available initially:', typeof fetch !== 'undefined'); + +// Import fetch polyfill to ensure fetch is available +// This handles the Windows Git Bash issue where fetch isn't in global scope +await import('../../fetch-polyfill.js'); + +console.log('Fetch available after polyfill:', typeof fetch !== 'undefined'); + +// Now load use-m as usual +const { use } = eval( + await ( + await fetch('https://unpkg.com/use-m/use.js') + ).text() +); + +console.log('\nāœ… Successfully loaded use-m'); + +// Test with lodash +const _ = await use('lodash@4.17.21'); +console.log('āœ… Successfully loaded lodash'); +console.log('Test: _.add(1, 2) =', _.add(1, 2)); + +console.log('\nšŸŽ‰ All tests passed!'); diff --git a/examples/windows-fetch-workaround/test-without-polyfill.mjs b/examples/windows-fetch-workaround/test-without-polyfill.mjs new file mode 100755 index 0000000..dfd334a --- /dev/null +++ b/examples/windows-fetch-workaround/test-without-polyfill.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +/** + * Example: Testing fetch availability without polyfill + * + * This script reproduces issue #45 by attempting to use fetch without + * ensuring it's available first. + */ + +console.log('Platform:', process.platform); +console.log('Node version:', process.version); +console.log('Fetch available:', typeof fetch !== 'undefined'); + +if (typeof fetch === 'undefined') { + console.error('\nāŒ ERROR: fetch is not defined!'); + console.error('\nThis is the issue described in #45.'); + console.error('To fix this, use the fetch polyfill:'); + console.error(' await import("use-m/fetch-polyfill.js");'); + console.error('\nOr see: examples/windows-fetch-workaround/test-with-polyfill.mjs'); + process.exit(1); +} + +// Attempt to load use-m +try { + const { use } = eval( + await ( + await fetch('https://unpkg.com/use-m/use.js') + ).text() + ); + + console.log('āœ… Successfully loaded use-m'); + + const _ = await use('lodash@4.17.21'); + console.log('āœ… Test passed: _.add(1, 2) =', _.add(1, 2)); +} catch (error) { + console.error('āŒ Failed:', error.message); + process.exit(1); +} diff --git a/experiments/test-fetch-availability.mjs b/experiments/test-fetch-availability.mjs new file mode 100755 index 0000000..becb282 --- /dev/null +++ b/experiments/test-fetch-availability.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +/** + * Test script to reproduce issue #45: fetch is not defined on Windows + * + * This script checks: + * 1. Node.js version + * 2. Whether fetch is available + * 3. Whether we can load use-m with fetch + */ + +console.log('=== Fetch Availability Test ==='); +console.log('Node version:', process.version); +console.log('Platform:', process.platform); +console.log('Fetch available:', typeof fetch !== 'undefined'); +console.log('Global fetch:', typeof globalThis.fetch !== 'undefined'); + +// Test if fetch works +if (typeof fetch === 'undefined') { + console.error('\nāŒ ERROR: fetch is not defined!'); + console.log('\nThis is the bug described in issue #45'); + console.log('Expected: fetch should be available in Node.js 18+'); + console.log('Actual: fetch is undefined'); + process.exit(1); +} else { + console.log('\nāœ… fetch is available'); + + // Try to load use-m + try { + console.log('\nTrying to load use-m...'); + const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()); + console.log('āœ… Successfully loaded use-m'); + + // Test using a package + const _ = await use('lodash@4.17.21'); + console.log('āœ… Successfully loaded lodash via use-m'); + console.log('Test: _.add(1, 2) =', _.add(1, 2)); + } catch (error) { + console.error('āŒ Failed to load use-m:', error.message); + process.exit(1); + } +} diff --git a/experiments/test-fetch-polyfill.mjs b/experiments/test-fetch-polyfill.mjs new file mode 100644 index 0000000..631c8f6 --- /dev/null +++ b/experiments/test-fetch-polyfill.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +/** + * Test script demonstrating fetch polyfill for issue #45 + * + * This shows how to ensure fetch is available before loading use-m + */ + +console.log('=== Testing fetch polyfill approach ==='); +console.log('Node version:', process.version); +console.log('Platform:', process.platform); +console.log('Initial fetch available:', typeof fetch !== 'undefined'); + +// Polyfill fetch if not available (for Node.js 18+ with Windows Git Bash issues) +if (typeof fetch === 'undefined') { + console.log('\nāš ļø fetch is not defined, attempting to use node:http/https as fallback...'); + + // For Node.js 18+, fetch should be available but might not be in certain contexts + // We can try to explicitly import it from node: + try { + // Try to import fetch from Node.js built-ins (Node 18+) + // This is a workaround for contexts where global fetch isn't initialized + const { default: nodeFetch } = await import('node:http').then(() => { + // Actually try to get fetch from the global after a tick + return new Promise(resolve => { + setImmediate(() => { + if (typeof fetch !== 'undefined') { + resolve({ default: fetch }); + } else { + throw new Error('fetch still not available'); + } + }); + }); + }).catch(async () => { + // If Node.js built-in fetch is not available, fall back to https module + const https = await import('node:https'); + const http = await import('node:http'); + + return { + default: (url, options = {}) => { + return new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + const req = protocol.request(parsedUrl, { + method: options.method || 'GET', + headers: options.headers || {}, + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + text: () => Promise.resolve(data), + json: () => Promise.resolve(JSON.parse(data)), + }); + }); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); + } + }; + }); + + globalThis.fetch = nodeFetch; + console.log('āœ… fetch polyfill installed'); + } catch (error) { + console.error('āŒ Failed to install fetch polyfill:', error.message); + process.exit(1); + } +} + +console.log('Final fetch available:', typeof fetch !== 'undefined'); + +// Now try to load use-m +try { + console.log('\nLoading use-m...'); + const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()); + console.log('āœ… Successfully loaded use-m'); + + // Test using a package + const _ = await use('lodash@4.17.21'); + console.log('āœ… Successfully loaded lodash via use-m'); + console.log('Test: _.add(1, 2) =', _.add(1, 2)); + + console.log('\nāœ… All tests passed!'); +} catch (error) { + console.error('āŒ Failed:', error.message); + console.error(error.stack); + process.exit(1); +} diff --git a/experiments/test-polyfill-no-fetch.mjs b/experiments/test-polyfill-no-fetch.mjs new file mode 100755 index 0000000..a00ceca --- /dev/null +++ b/experiments/test-polyfill-no-fetch.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +/** + * Test the fetch polyfill by simulating a scenario where fetch is not available + */ + +console.log('=== Testing fetch polyfill with simulated missing fetch ==='); +console.log('Node version:', process.version); +console.log('Platform:', process.platform); +console.log('Fetch initially available:', typeof fetch !== 'undefined'); + +// Simulate the Windows Git Bash issue by removing fetch from global scope +const originalFetch = globalThis.fetch; +delete globalThis.fetch; + +console.log('Fetch after deletion:', typeof fetch !== 'undefined'); + +// Now import the polyfill +try { + await import('../fetch-polyfill.js'); + console.log('āœ… Polyfill imported successfully'); + console.log('Fetch after polyfill:', typeof fetch !== 'undefined'); + + if (typeof fetch === 'undefined') { + console.error('āŒ Polyfill failed to install fetch'); + process.exit(1); + } + + // Test that fetch works + console.log('\nTesting fetch...'); + const response = await fetch('https://unpkg.com/use-m/package.json'); + const data = await response.json(); + console.log('āœ… Fetch works! Package name:', data.name); + + // Now test loading use-m + console.log('\nTesting use-m...'); + const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()); + console.log('āœ… use-m loaded successfully'); + + const _ = await use('lodash@4.17.21'); + console.log('āœ… lodash loaded successfully'); + console.log('Test: _.add(1, 2) =', _.add(1, 2)); + + console.log('\nšŸŽ‰ All tests passed!'); +} catch (error) { + console.error('āŒ Test failed:', error.message); + console.error(error.stack); + + // Restore original fetch + if (originalFetch) { + globalThis.fetch = originalFetch; + } + + process.exit(1); +} diff --git a/fetch-polyfill.js b/fetch-polyfill.js new file mode 100644 index 0000000..db418ba --- /dev/null +++ b/fetch-polyfill.js @@ -0,0 +1,108 @@ +/** + * Fetch polyfill for Node.js environments where fetch is not available + * + * This addresses issue #45 where fetch is not defined in certain Windows contexts + * (e.g., Git Bash, shebang execution) even in Node.js 18+. + * + * Usage: + * // Ensure fetch is available before loading use-m + * await import('use-m/fetch-polyfill.js'); + * const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()); + * + * Or with a local import: + * await import('./fetch-polyfill.js'); + */ + +// Only install polyfill if fetch is not already available +const needsPolyfill = typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'undefined'; + +if (needsPolyfill) { + // Create a minimal fetch implementation using node:http/https + const installPolyfill = async () => { + try { + // First, try to import from undici (Node.js 18+) + let fetchImpl; + + try { + const undici = await import('undici'); + fetchImpl = undici.fetch; + } catch { + // If undici is not available, create a minimal fetch using node:http/https + const https = await import('node:https'); + const http = await import('node:http'); + const { URL } = await import('node:url'); + + fetchImpl = (url, options = {}, redirectCount = 0) => { + const MAX_REDIRECTS = 20; + + return new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + const requestOptions = { + method: options.method || 'GET', + headers: options.headers || {}, + }; + + const req = protocol.request(parsedUrl, requestOptions, (res) => { + // Handle redirects (301, 302, 303, 307, 308) + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + if (redirectCount >= MAX_REDIRECTS) { + return reject(new Error('Too many redirects')); + } + + const redirectUrl = new URL(res.headers.location, url).href; + return resolve(fetchImpl(redirectUrl, options, redirectCount + 1)); + } + + const chunks = []; + + res.on('data', chunk => chunks.push(chunk)); + + res.on('end', () => { + const buffer = Buffer.concat(chunks); + const text = buffer.toString('utf-8'); + + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + url: url, + redirected: redirectCount > 0, + type: 'basic', + text: () => Promise.resolve(text), + json: () => Promise.resolve(JSON.parse(text)), + blob: () => Promise.resolve(buffer), + arrayBuffer: () => Promise.resolve(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)), + }); + }); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); + }; + } + + // Install fetch polyfill globally + globalThis.fetch = fetchImpl; + } catch (error) { + // If polyfill installation fails, provide a helpful error message + console.error('Failed to install fetch polyfill:', error.message); + console.error('Please ensure you are running Node.js 18+ or install node-fetch manually.'); + } + }; + + // Execute the polyfill installation + await installPolyfill(); +} + +// Export the fetch function (either native or polyfilled) +export const fetch = globalThis.fetch; +export default globalThis.fetch; diff --git a/package.json b/package.json index 545fac8..a69e854 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "require": "./use.cjs" }, "./use.cjs": "./use.cjs", - "./use.mjs": "./use.mjs" + "./use.mjs": "./use.mjs", + "./fetch-polyfill.js": "./fetch-polyfill.js" }, "bin": { "use": "./cli.mjs"