Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] esm build does not have .js suffix #4716

Open
2 tasks done
liudonghua123 opened this issue Oct 17, 2024 · 2 comments
Open
2 tasks done

[Bug] esm build does not have .js suffix #4716

liudonghua123 opened this issue Oct 17, 2024 · 2 comments

Comments

@liudonghua123
Copy link

Reproducible in vscode.dev or in VS Code Desktop?

  • Not reproducible in vscode.dev or VS Code Desktop

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.

Image

Expected Behavior

The import of esm build should end with .js to make it work for pure browser.

Additional Context

No response

@liudonghua123
Copy link
Author

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 });
  }
}

Image

@liudonghua123
Copy link
Author

I have also noticed that some similar work is doing in the vscode project, see microsoft/vscode#226260.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant