diff --git a/examples/cache-demo.mjs b/examples/cache-demo.mjs new file mode 100644 index 0000000..eb431b2 --- /dev/null +++ b/examples/cache-demo.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +// Demo script showing the in-memory cache functionality of use-m +// This script demonstrates that modules are cached and the same instance is returned + +const { use } = eval( + await fetch('https://unpkg.com/use-m/use.js').then(u => u.text()) +); + +console.log('šŸš€ use-m cache demonstration\n'); + +// First, let's test with a built-in module +console.log('1. Testing built-in module caching:'); +const crypto1 = await use('crypto'); +const crypto2 = await use('crypto'); + +console.log(`crypto1 === crypto2: ${crypto1 === crypto2} (should be true - cached!)`); +console.log(`crypto hash test: ${crypto1.createHash('sha256').update('test').digest('hex')}\n`); + +// Now test with an NPM module +console.log('2. Testing npm module caching:'); +console.log('Loading lodash first time...'); +const start1 = performance.now(); +const lodash1 = await use('lodash@4.17.21'); +const time1 = performance.now() - start1; + +console.log('Loading lodash second time...'); +const start2 = performance.now(); +const lodash2 = await use('lodash@4.17.21'); +const time2 = performance.now() - start2; + +console.log(`lodash1 === lodash2: ${lodash1 === lodash2} (should be true - cached!)`); +console.log(`First load time: ${time1.toFixed(2)}ms`); +console.log(`Second load time: ${time2.toFixed(2)}ms (should be much faster!)`); +console.log(`Speed improvement: ${((time1 - time2) / time1 * 100).toFixed(1)}%\n`); + +// Test functionality still works +console.log('3. Testing functionality:'); +console.log(`lodash1.add(1, 2) = ${lodash1.add(1, 2)}`); +console.log(`lodash2.add(3, 4) = ${lodash2.add(3, 4)}`); + +console.log('\nāœ… Cache demo complete! Modules are cached in memory for better performance.'); \ No newline at end of file diff --git a/tests/cache.test.mjs b/tests/cache.test.mjs new file mode 100644 index 0000000..cddd440 --- /dev/null +++ b/tests/cache.test.mjs @@ -0,0 +1,81 @@ +import { describe, test, expect } from '../test-adapter.mjs'; +import { use } from '../use.mjs'; + +const moduleName = `[${import.meta.url.split('.').pop()} module]`; + +describe(`${moduleName} Module caching functionality`, () => { + test(`${moduleName} Module cache - same module instance returned on repeated calls`, async () => { + // Import the same built-in module twice + const url1 = await use('url'); + const url2 = await use('url'); + + // Both should return the exact same object reference (cached) + expect(url1).toBe(url2); + + // Both should have the same functionality + expect(typeof url1.URL).toBe('function'); + expect(typeof url2.URL).toBe('function'); + + const testUrl1 = new url1.URL('https://example.com'); + const testUrl2 = new url2.URL('https://example.com'); + expect(testUrl1.hostname).toBe(testUrl2.hostname); + }); + + test(`${moduleName} Built-in module cache - same instance for repeated built-in imports`, async () => { + // Import the same built-in module twice + const fs1 = await use('fs'); + const fs2 = await use('fs'); + + // Both should return the exact same object reference (cached) + expect(fs1).toBe(fs2); + + // Both should have expected properties + expect(typeof fs1.readFile).toBe('function'); + expect(typeof fs2.readFile).toBe('function'); + }); + + test(`${moduleName} Different built-in modules are cached separately`, async () => { + // Import different built-in modules + const crypto1 = await use('crypto'); + const fs1 = await use('fs'); + + // They should be different objects + expect(crypto1).not.toBe(fs1); + expect(typeof crypto1.createHash).toBe('function'); + expect(typeof fs1.readFile).toBe('function'); + }); + + test(`${moduleName} Cache functionality - same object reference for built-ins`, async () => { + // Import the same built-in module multiple times + const crypto1 = await use('crypto'); + const crypto2 = await use('node:crypto'); + const crypto3 = await use('crypto'); + + // All should return the exact same object reference (cached) + expect(crypto1).toBe(crypto2); + expect(crypto2).toBe(crypto3); + expect(crypto1).toBe(crypto3); + + // All should have the same functionality + const hash1 = crypto1.createHash('sha256').update('test').digest('hex'); + const hash2 = crypto2.createHash('sha256').update('test').digest('hex'); + const hash3 = crypto3.createHash('sha256').update('test').digest('hex'); + expect(hash1).toBe(hash2); + expect(hash2).toBe(hash3); + }); + + test(`${moduleName} Built-in cache with node: prefix`, async () => { + // Test caching with node: prefix + const crypto1 = await use('node:crypto'); + const crypto2 = await use('crypto'); + + // These should potentially be the same cached object + expect(typeof crypto1.createHash).toBe('function'); + expect(typeof crypto2.createHash).toBe('function'); + + // Test that they work + const hash1 = crypto1.createHash('sha256').update('test').digest('hex'); + const hash2 = crypto2.createHash('sha256').update('test').digest('hex'); + expect(hash1).toBe(hash2); + }); +}); \ No newline at end of file diff --git a/use.cjs b/use.cjs index 3917be3..55f3a14 100644 --- a/use.cjs +++ b/use.cjs @@ -214,6 +214,9 @@ const supportedBuiltins = { } }; +// Cache for built-in modules to avoid redundant imports +const builtinCache = new Map(); + const resolvers = { builtin: async (moduleSpecifier, pathResolver) => { const { packageName } = parseModuleSpecifier(moduleSpecifier); @@ -221,6 +224,16 @@ const resolvers = { // Remove 'node:' prefix if present const moduleName = packageName.startsWith('node:') ? packageName.slice(5) : packageName; + // Create cache key including environment for built-in modules + const isBrowser = typeof window !== 'undefined'; + const environment = isBrowser ? 'browser' : 'node'; + const cacheKey = `${moduleName}:${environment}`; + + // Check if built-in module is already cached + if (builtinCache.has(cacheKey)) { + return builtinCache.get(cacheKey); + } + // Check if we support this built-in module if (supportedBuiltins[moduleName]) { const builtinConfig = supportedBuiltins[moduleName]; @@ -229,10 +242,6 @@ const resolvers = { throw new Error(`Built-in module '${moduleName}' is not supported.`); } - // Determine environment - const isBrowser = typeof window !== 'undefined'; - const environment = isBrowser ? 'browser' : 'node'; - const moduleFactory = builtinConfig[environment]; if (!moduleFactory) { throw new Error(`Built-in module '${moduleName}' is not available in ${environment} environment.`); @@ -241,6 +250,9 @@ const resolvers = { try { // Execute the factory function to get the module const result = await moduleFactory(); + + // Cache the result for future use + builtinCache.set(cacheKey, result); return result; } catch (error) { throw new Error(`Failed to load built-in module '${moduleName}' in ${environment} environment.`, { cause: error }); @@ -610,7 +622,15 @@ const resolvers = { }, } +// In-memory cache for loaded modules to avoid redundant file operations +const moduleCache = new Map(); + const baseUse = async (modulePath) => { + // Check if module is already cached + if (moduleCache.has(modulePath)) { + return moduleCache.get(modulePath); + } + // Dynamically import the module try { const module = await import(modulePath); @@ -618,30 +638,38 @@ const baseUse = async (modulePath) => { // More robust default export handling for cross-environment compatibility const keys = Object.keys(module); + let processedModule; // If it's a Module object with a default property, unwrap it if (module.default !== undefined) { // Check if this is likely a CommonJS module with only default export if (keys.length === 1 && keys[0] === 'default') { - return module.default; - } - - // Check if default is the main export and other keys are just function/module metadata - const metadataKeys = new Set([ - 'default', '__esModule', 'Symbol(Symbol.toStringTag)', - 'length', 'name', 'prototype', 'constructor', - 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' - ]); - - const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); - - // If there are no significant non-metadata keys, return the default - if (nonMetadataKeys.length === 0) { - return module.default; + processedModule = module.default; + } else { + // Check if default is the main export and other keys are just function/module metadata + const metadataKeys = new Set([ + 'default', '__esModule', 'Symbol(Symbol.toStringTag)', + 'length', 'name', 'prototype', 'constructor', + 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' + ]); + + const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); + + // If there are no significant non-metadata keys, return the default + if (nonMetadataKeys.length === 0) { + processedModule = module.default; + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; + } } + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; } - // Return the whole module if it has multiple meaningful exports or no default - return module; + // Cache the processed module for future use + moduleCache.set(modulePath, processedModule); + return processedModule; } catch (error) { throw new Error(`Failed to import module from '${modulePath}'.`, { cause: error }); } diff --git a/use.js b/use.js index c2dcb3b..f4adbd2 100644 --- a/use.js +++ b/use.js @@ -214,6 +214,9 @@ const supportedBuiltins = { } }; +// Cache for built-in modules to avoid redundant imports +const builtinCache = new Map(); + const resolvers = { builtin: async (moduleSpecifier, pathResolver) => { const { packageName } = parseModuleSpecifier(moduleSpecifier); @@ -221,6 +224,16 @@ const resolvers = { // Remove 'node:' prefix if present const moduleName = packageName.startsWith('node:') ? packageName.slice(5) : packageName; + // Create cache key including environment for built-in modules + const isBrowser = typeof window !== 'undefined'; + const environment = isBrowser ? 'browser' : 'node'; + const cacheKey = `${moduleName}:${environment}`; + + // Check if built-in module is already cached + if (builtinCache.has(cacheKey)) { + return builtinCache.get(cacheKey); + } + // Check if we support this built-in module if (supportedBuiltins[moduleName]) { const builtinConfig = supportedBuiltins[moduleName]; @@ -229,10 +242,6 @@ const resolvers = { throw new Error(`Built-in module '${moduleName}' is not supported.`); } - // Determine environment - const isBrowser = typeof window !== 'undefined'; - const environment = isBrowser ? 'browser' : 'node'; - const moduleFactory = builtinConfig[environment]; if (!moduleFactory) { throw new Error(`Built-in module '${moduleName}' is not available in ${environment} environment.`); @@ -241,6 +250,9 @@ const resolvers = { try { // Execute the factory function to get the module const result = await moduleFactory(); + + // Cache the result for future use + builtinCache.set(cacheKey, result); return result; } catch (error) { throw new Error(`Failed to load built-in module '${moduleName}' in ${environment} environment.`, { cause: error }); @@ -610,7 +622,15 @@ const resolvers = { }, } +// In-memory cache for loaded modules to avoid redundant file operations +const moduleCache = new Map(); + const baseUse = async (modulePath) => { + // Check if module is already cached + if (moduleCache.has(modulePath)) { + return moduleCache.get(modulePath); + } + // Dynamically import the module try { const module = await import(modulePath); @@ -618,30 +638,38 @@ const baseUse = async (modulePath) => { // More robust default export handling for cross-environment compatibility const keys = Object.keys(module); + let processedModule; // If it's a Module object with a default property, unwrap it if (module.default !== undefined) { // Check if this is likely a CommonJS module with only default export if (keys.length === 1 && keys[0] === 'default') { - return module.default; - } - - // Check if default is the main export and other keys are just function/module metadata - const metadataKeys = new Set([ - 'default', '__esModule', 'Symbol(Symbol.toStringTag)', - 'length', 'name', 'prototype', 'constructor', - 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' - ]); - - const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); - - // If there are no significant non-metadata keys, return the default - if (nonMetadataKeys.length === 0) { - return module.default; + processedModule = module.default; + } else { + // Check if default is the main export and other keys are just function/module metadata + const metadataKeys = new Set([ + 'default', '__esModule', 'Symbol(Symbol.toStringTag)', + 'length', 'name', 'prototype', 'constructor', + 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' + ]); + + const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); + + // If there are no significant non-metadata keys, return the default + if (nonMetadataKeys.length === 0) { + processedModule = module.default; + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; + } } + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; } - // Return the whole module if it has multiple meaningful exports or no default - return module; + // Cache the processed module for future use + moduleCache.set(modulePath, processedModule); + return processedModule; } catch (error) { throw new Error(`Failed to import module from '${modulePath}'.`, { cause: error }); } diff --git a/use.mjs b/use.mjs index 250fe81..b3019dd 100644 --- a/use.mjs +++ b/use.mjs @@ -214,6 +214,9 @@ const supportedBuiltins = { } }; +// Cache for built-in modules to avoid redundant imports +const builtinCache = new Map(); + export const resolvers = { builtin: async (moduleSpecifier, pathResolver) => { const { packageName } = parseModuleSpecifier(moduleSpecifier); @@ -221,6 +224,16 @@ export const resolvers = { // Remove 'node:' prefix if present const moduleName = packageName.startsWith('node:') ? packageName.slice(5) : packageName; + // Create cache key including environment for built-in modules + const isBrowser = typeof window !== 'undefined'; + const environment = isBrowser ? 'browser' : 'node'; + const cacheKey = `${moduleName}:${environment}`; + + // Check if built-in module is already cached + if (builtinCache.has(cacheKey)) { + return builtinCache.get(cacheKey); + } + // Check if we support this built-in module if (supportedBuiltins[moduleName]) { const builtinConfig = supportedBuiltins[moduleName]; @@ -229,10 +242,6 @@ export const resolvers = { throw new Error(`Built-in module '${moduleName}' is not supported.`); } - // Determine environment - const isBrowser = typeof window !== 'undefined'; - const environment = isBrowser ? 'browser' : 'node'; - const moduleFactory = builtinConfig[environment]; if (!moduleFactory) { throw new Error(`Built-in module '${moduleName}' is not available in ${environment} environment.`); @@ -241,6 +250,9 @@ export const resolvers = { try { // Execute the factory function to get the module const result = await moduleFactory(); + + // Cache the result for future use + builtinCache.set(cacheKey, result); return result; } catch (error) { throw new Error(`Failed to load built-in module '${moduleName}' in ${environment} environment.`, { cause: error }); @@ -610,7 +622,15 @@ export const resolvers = { }, } +// In-memory cache for loaded modules to avoid redundant file operations +const moduleCache = new Map(); + export const baseUse = async (modulePath) => { + // Check if module is already cached + if (moduleCache.has(modulePath)) { + return moduleCache.get(modulePath); + } + // Dynamically import the module try { const module = await import(modulePath); @@ -618,30 +638,38 @@ export const baseUse = async (modulePath) => { // More robust default export handling for cross-environment compatibility const keys = Object.keys(module); + let processedModule; // If it's a Module object with a default property, unwrap it if (module.default !== undefined) { // Check if this is likely a CommonJS module with only default export if (keys.length === 1 && keys[0] === 'default') { - return module.default; - } - - // Check if default is the main export and other keys are just function/module metadata - const metadataKeys = new Set([ - 'default', '__esModule', 'Symbol(Symbol.toStringTag)', - 'length', 'name', 'prototype', 'constructor', - 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' - ]); - - const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); - - // If there are no significant non-metadata keys, return the default - if (nonMetadataKeys.length === 0) { - return module.default; + processedModule = module.default; + } else { + // Check if default is the main export and other keys are just function/module metadata + const metadataKeys = new Set([ + 'default', '__esModule', 'Symbol(Symbol.toStringTag)', + 'length', 'name', 'prototype', 'constructor', + 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable' + ]); + + const nonMetadataKeys = keys.filter(key => !metadataKeys.has(key)); + + // If there are no significant non-metadata keys, return the default + if (nonMetadataKeys.length === 0) { + processedModule = module.default; + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; + } } + } else { + // Return the whole module if it has multiple meaningful exports or no default + processedModule = module; } - // Return the whole module if it has multiple meaningful exports or no default - return module; + // Cache the processed module for future use + moduleCache.set(modulePath, processedModule); + return processedModule; } catch (error) { throw new Error(`Failed to import module from '${modulePath}'.`, { cause: error }); }