-
Notifications
You must be signed in to change notification settings - Fork 17k
Description
Preflight Checklist
- I have read the Contributing Guidelines for this project.
- I agree to follow the Code of Conduct that this project adheres to.
- I have searched the issue tracker for a feature request that matches the one I want to file, without success.
Problem Description
Currently any ESM module imports in the renderer process have to be either bundled or locally available to be loaded.
It will be very useful if the chrome loader can be patched to search them also in the node_modules, so there will be no need for bundling.
Proposed Solution
As a temporally solution, it is possible to hook a custom protocol in the main process to that will search and serve the es6 modules. But this should be actually integrated in Electron. Example:
Usage:
import some_module from 'es6://loader/some_module'
// Register custom protocol for ES6 modules
protocol.handle('es6', async (request) => {
try {
const urlObj = new URL(request.url);
let modulePath = urlObj.pathname;
// Decode URI components to get proper file path
modulePath = decodeURIComponent(modulePath);
// Check if the path starts with a slash and remove it
if (modulePath.startsWith('/')) {
modulePath = modulePath.substring(1);
}
// Parse the package name and subpath
let packageName, subPath;
// Handle scoped packages (@org/pkg)
if (modulePath.startsWith('@')) {
const parts = modulePath.split('/');
// Scoped package name is @org/pkg
packageName = parts.slice(0, 2).join('/');
// Subpath is everything after the package name
subPath = parts.slice(2).join('/');
} else {
// Regular package
const parts = modulePath.split('/');
// Package name is the first part
packageName = parts[0];
// Subpath is everything after the package name
subPath = parts.slice(1).join('/');
}
// Find the package in node_modules
const basePath = path.join(app.getAppPath(), 'node_modules', packageName);
// Read package.json to find exports mapping
const packageJsonPath = path.join(basePath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Package not found: ${packageName}`);
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Check if the package has exports field
if (packageJson.exports) {
let exportPath;
if (subPath.endsWith('.js')) {
// If the subPath ends with .js, we need to handle it differently
exportPath = subPath;
} else {
if (subPath === '') {
// Main export
exportPath = typeof packageJson.exports === 'object'
? (packageJson.exports['.']?.import || packageJson.exports['.']?.browser || packageJson.exports['.']?.default || packageJson.exports['.'] || packageJson.exports['import'])
: packageJson.exports; // Handle case where exports is a string
} else {
// Subpath export
const exportKey = './' + subPath;
exportPath = packageJson.exports[exportKey]?.import || packageJson.exports[exportKey]?.browser || packageJson.exports[exportKey]?.default || packageJson.exports[exportKey];
}
}
if (exportPath) {
const filePath = path.join(basePath, exportPath);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf8');
//fix all imports to start with es6://
const fileContentStr = fileContent.toString()
const modifiedContents = fileContentStr.replace(/(?:import|export)\s+.*?\s+from\s+['"]([^'"]+)['"]/g, (match, p1) => {
// if it starts with . then prepend the current module path
if (p1.startsWith('./')) {
match = match.replace(p1, 'es6://loader/' + packageName + subPath + p1.replace(/^\.\//, '/'))
return match;
} else if (p1.startsWith('/')) {
return match; // Don't modify the import statement
} else if (p1.startsWith('node:')) {
// generate own node import so they work
//todo
} else {
// Prepend 'es6://' to the import path
return match.replace(p1, `es6://loader/${p1}`);
}
})
// Return the file content with proper MIME type
return new Response(modifiedContents, {
headers: {
'Content-Type': 'application/javascript; charset=utf-8'
}
});
} else {
throw new Error(`File not found: ${filePath}`);
}
}
}
// Fallback to direct resolution for packages without exports field
const directPath = path.join(basePath, subPath || 'index.js');
if (fs.existsSync(directPath)) {
const fileContent = fs.readFileSync(directPath, 'utf8');
return new Response(fileContent, {
headers: {
'Content-Type': 'application/javascript; charset=utf-8'
}
});
}
throw new Error(`Could not resolve module: ${packageName}/${subPath}`);
} catch (error) {
console.error('ES6 protocol error:', error);
return new Response(`console.error('Failed to load ES6 module: ${error.message}')`, {
status: 404,
headers: {
'Content-Type': 'application/javascript; charset=utf-8'
}
});
}
});However the chrome loader, while it loads all successfully, throws an error about the invalid protocol es6://
So maybe the protocol isn't fully registered by Electron also for Chrome imports?
Alternatives Considered
This should be integrated in Electron and offered as native solution.
Same is also valid for loading node modules in the rendered process if allowed.
Maybe this can be also solved with a custom protocol:
protocol.handle('node', async (request) => {
try {
const requestPath = new URL(request.url).pathname;
const moduleName = requestPath.startsWith('/') ? requestPath.substring(1) : requestPath;
// Handle built-in Node.js modules
if (moduleName) {
// For requiring the actual module, we need to strip the "node:" prefix
const actualModuleName = moduleName.replace(/^node:/, '');
// Create module wrapper for ES module compatibility
const moduleContent = `
const nodeModule = require('${actualModuleName}');
export default nodeModule;
// Export all properties of the module
for (const key in nodeModule) {
if (Object.prototype.hasOwnProperty.call(nodeModule, key)) {
export const \${key} = nodeModule[key];
}
}
`;
console.log('Node[' + moduleName + '] module content:', moduleContent);
return new Response(moduleContent, {
headers: {
'Content-Type': 'application/javascript'
}
});
}
throw new Error(`Could not resolve Node.js module: ${moduleName}`);
} catch (error) {
console.error('Error handling node: protocol:', error);
return new Response(`console.error("Failed to load Node.js module: ${error.message}");`, {
status: 500,
headers: {
'Content-Type': 'application/javascript'
}
});
}
});Additional Information
No response