[Bug] esm build does not have .js suffix
Reproducible in vscode.dev or in VS Code Desktop?
- [x] Not reproducible in vscode.dev or VS Code Desktop
Reproducible in the monaco editor playground?
- [x] Not reproducible in the monaco editor playground
Monaco Editor Playground Link
n.a.
Monaco Editor Playground Code
n.a.
Reproduction Steps
I want to use esm build of monaco editor in pure frontend html, means without bundler like webpack, vite and without server.
The following is the demo code I wrote, The following code in browser doesn't work due the import file is not end with .js suffix.
// monaco-editor/esm/vs/editor/editor.main.js
import '../basic-languages/monaco.contribution';
import '../language/css/monaco.contribution';
import '../language/html/monaco.contribution';
import '../language/json/monaco.contribution';
import '../language/typescript/monaco.contribution';
export * from './edcore.main';
Here is the demo code
demo code
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic JS Executor</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#editor {
width: 100%;
height: 300px;
border: 1px solid #ccc;
margin-bottom: 10px;
}
button {
display: block;
margin-bottom: 10px;
}
label {
font-weight: bold;
}
textarea {
width: 100%;
height: 150px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<label for="editor">JS Code (Monaco Editor)</label>
<div id="editor"></div>
<button id="execute">Execute</button>
<label for="output">Output</label>
<textarea id="output" readonly></textarea>
<!-- <script src="https://unpkg.com/[email protected]/dist/system.js"></script> -->
<!-- <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script> -->
<script type="importmap">
{
"imports": {
"monaco-editor": "./assets/monaco-editor/esm/vs/editor/editor.main.js",
"monaco-editor/esm/vs/basic-languages/monaco.contribution": "./assets/monaco-editor/esm/vs/basic-languages/monaco.contribution.js",
"@tauri-apps/api/": "./assets/tauri-apps-api/",
"@tauri-apps/api/core": "./assets/tauri-apps-api/core.js",
"@tauri-apps/plugin-http": "./assets/tauri-apps-plugin-http/index.js"
}
}
</script>
<!--
<script type="module-shim">
console.info(`module-shim`)
// Add the .js suffix if missing in imports
importShim.resolve = (id, parentUrl) => {
console.info('importShim.resolve', id, parentUrl);
if (!id.endsWith('.js') && !id.includes(':')) {
id += '.js';
}
return new URL(id, parentUrl).href;
};
importShim.fetch = async (url) => await fetch(url.indexOf('.js') < 0 ? url + ".js" : url);
// Load main.js using importShim
importShim('./main').catch(console.error);
</script> -->
<script type="module" src="./main.js"></script>
</body>
</html>
// main.js
// Import the ESM version of Monaco Editor
import * as monaco from 'monaco-editor';
// Set up Monaco Editor
const editor = monaco.editor.create(document.getElementById('editor'), {
value: `// Write your JS code here...\n`,
language: 'javascript',
theme: 'vs-dark',
});
const executeButton = document.getElementById('execute');
const outputTextArea = document.getElementById('output');
// Function to redirect console methods
const redirectConsoleOutput = () => {
const originalLog = console.log;
const originalInfo = console.info;
const originalWarn = console.warn;
const originalError = console.error;
// Custom handler to redirect output to the textarea
const logToOutput = (method, args) => {
originalLog.apply(console, args); // Also call the original method to keep logging to console
outputTextArea.value += `[${method.toUpperCase()}] ${args.join(' ')}\n`;
};
// Override console methods
console.log = (...args) => logToOutput('log', args);
console.info = (...args) => logToOutput('info', args);
console.warn = (...args) => logToOutput('warn', args);
console.error = (...args) => logToOutput('error', args);
};
// Set up the Execute button click handler
executeButton.addEventListener('click', async () => {
const code = editor.getValue();
outputTextArea.value = ''; // Clear output
// Redirect console output
redirectConsoleOutput();
try {
// Remove any previous <script> tags that were added dynamically
const oldScript = document.getElementById('dynamicScript');
if (oldScript) {
oldScript.remove();
}
// Create a new <script> tag to execute the user code
const script = document.createElement('script');
script.id = 'dynamicScript';
script.type = 'module'; // Ensure the module scope if needed
script.innerHTML = `
import { fetch } from '@tauri-apps/plugin-http';
import { invoke } from '@tauri-apps/api';
async function executeUserCode() {
try {
${code} // Execute the user's code here
} catch (error) {
console.error('Error executing user code:', error);
}
}
executeUserCode();
`;
// Append the <script> to the body
document.body.appendChild(script);
} catch (error) {
outputTextArea.value = `Error: ${error.message}`;
}
});
I have tried to use https://github.com/guybedford/es-module-shims or https://github.com/systemjs/systemjs, but none of them works for me.
Actual (Problematic) Behavior
The url of import module doesn't end with .js, so the static server will not work as expected.
Expected Behavior
The import of esm build should end with .js to make it work for pure browser.
Additional Context
No response
I tried to use Service Worker to intercept the request, but some code like import './inlineProgressWidget.css'; in js still could not be handled via the browser esm loader without bundler.
How about build monaco-editor both esm-bundler and esm-browser just like vue does.
The vue seems use rollup to build both esm-bundler and esm-browser output.
sw.js
self.addEventListener('install', event => {
console.log('Service worker installed');
});
self.addEventListener('activate', event => {
console.log('Service worker activated');
});
self.addEventListener('fetch', event => {
event.respondWith(handleRequest(event)); // Pass handleRequest directly to respondWith
});
async function handleRequest(event) {
try {
let request = event.request.clone();
const url = new URL(request.url);
// Get the extension of the last part of the pathname
const extension = url.pathname.includes('.') ? url.pathname.split('.').pop() : '';
// Check if the URL starts with `/assets` and doesn't end with `/`, `.js`, or `.css`
if (url.pathname.startsWith(`/assets`) && !url.pathname.endsWith('/') && !['js', 'css'].includes(extension)) {
console.info(`Rewrite url: ${url.href} to ${url.href}.js`);
// The request URL is immutable, so we need to create a new Request object with the modified URL
request = new Request(`${url.href}.js`, {
method: request.method,
headers: request.headers,
body: request.body, // Only use body if the method supports it (e.g., POST, PUT)
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
integrity: request.integrity,
keepalive: request.keepalive,
});
}
// Check if the response for the request is already in the cache
const cache = await caches.open('assets-cache');
let response = await cache.match(request.url);
if (!response) {
console.info(`Cache miss for ${request.url}`);
response = await fetch(request);
const responseClone = response.clone(); // Clone the response to put it into the cache
event.waitUntil(cache.put(request.url, responseClone));
}
return response; // Immediately return the response (or the cached response)
} catch (error) {
console.error(`Error in handleRequest: ${error}`);
// Return a default response if an error occurs, to avoid interruption
return new Response('Error fetching resource', { status: 500 });
}
}
I have also noticed that some similar work is doing in the vscode project, see https://github.com/microsoft/vscode/issues/226260.
also it uses wrong css imports:
https://github.com/microsoft/monaco-editor/issues/4526
This is fixed now.
@hediet no it is not fixed. look in editor.worker.js, still wrong imports
Content of editor.worker.js in 0.55.1:
import { isWorkerInitialized } from '../common/initialize.js';
export { initialize } from '../common/initialize.js';
import { start } from './editor.worker.start.js';
self.onmessage = () => {
if (!isWorkerInitialized()) {
start(() => {
return {};
});
}
};