Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/cache-demo.mjs
Original file line number Diff line number Diff line change
@@ -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.');
81 changes: 81 additions & 0 deletions tests/cache.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
});
70 changes: 49 additions & 21 deletions use.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,26 @@ 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);

// 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];
Expand All @@ -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.`);
Expand All @@ -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 });
Expand Down Expand Up @@ -610,38 +622,54 @@ 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);

// 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 });
}
Expand Down
70 changes: 49 additions & 21 deletions use.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,26 @@ 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);

// 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];
Expand All @@ -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.`);
Expand All @@ -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 });
Expand Down Expand Up @@ -610,38 +622,54 @@ 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);

// 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 });
}
Expand Down
Loading
Loading