skip to Main Content

I’m developing a Chrome extension using React and Vite. When I load the extension locally from the dist folder as an unpacked extension in Chrome, it runs without any issues.

However, after publishing it to the Chrome Web Store, I receive the following error when trying to use the extension:

Uncaught SyntaxError: Cannot use import statement outside a module

What I’ve tried so far:

  • List item
  • Removed React.StrictMode from the code.
  • Modified tsconfig.json with different module settings:
  • Tried "module": "ESNext"
  • Tried "module": "commonjs"
  • Ensured that package.json includes "type": "module".
  • Built the project using vite build instead of vite dev; no errors were shown during the build process.

Content Script Code

import ReactDOM from 'react-dom/client';
import ContentApp from './ContentApp';
import { ExplanationContainer } from '../features/explanation/ExplanationContainer';
import { ConfigProvider } from 'antd';
import customTheme from '../theme/customTheme.ts';
import '../index.css';
import { StyleProvider } from '@ant-design/cssinjs';
import { SessionProvider } from '../features/auth/SessionContext.tsx';
import { getQuizProgression } from '../features/shared/helpers/getQuizProgression.ts';

const ROOT_ELEMENT_ID = 'crx-root';
const EXPLANATION_ROOT_ID = 'explanation-root';

interface RootInfo {
    root: ReactDOM.Root;
    element: HTMLElement;
}

let contentRoot: RootInfo | null = null;
let explanationRoot: RootInfo | null = null;
let isRendering = false;

const AppProviders: React.FC<{
    children: React.ReactNode;
}> = ({ children }) => {
    return (
        <StyleProvider hashPriority="high">
            <ConfigProvider prefixCls={'ant'} theme={customTheme}>
                <SessionProvider>{children}</SessionProvider>
            </ConfigProvider>
        </StyleProvider>
    );
};

const createOrGetRoot = (id: string): HTMLElement => {
    let element = document.getElementById(id);
    if (!element) {
        element = document.createElement('div');
        element.id = id;
        document.body.appendChild(element);
    }
    return element;
};

const renderComponent = (id: string, Component: React.FC): RootInfo => {
    const element = createOrGetRoot(id);
    const root = ReactDOM.createRoot(element);
    root.render(
        <AppProviders>
            <Component />
        </AppProviders>
    );
    return { root, element };
};

const safeAppendChild = (parent: Element | null, child: HTMLElement) => {
    if (parent && !parent.contains(child)) {
        parent.appendChild(child);
    }
};

const renderContentApp = () => {
    const cardHeader = document.querySelector('.card-header');
    const cardHeaderText = cardHeader?.textContent;
    const keywords = ['Réglages', 'Settings', 'Einstellungen', 'Impostazioni'];
    const shouldRenderProfile = keywords.some((keyword) =>
        cardHeaderText?.includes(keyword)
    );

    const headerElement = document.querySelector('.card-body');
    if (headerElement && shouldRenderProfile) {
        if (contentRoot) {
            safeAppendChild(headerElement, contentRoot.element);
        } else {
            contentRoot = renderComponent(ROOT_ELEMENT_ID, ContentApp);
            safeAppendChild(headerElement, contentRoot.element);
        }
    } else if (contentRoot) {
        contentRoot.root.unmount();
        contentRoot = null;
    }
};

const renderAiExplanation = () => {
    const bodyElement = document.querySelector('.card-body');
    const quizProgressionText = getQuizProgression();
    const shouldRenderExplanation = quizProgressionText !== null;

    if (shouldRenderExplanation && bodyElement) {
        if (explanationRoot == null) {
            explanationRoot = renderComponent(
                EXPLANATION_ROOT_ID,
                ExplanationContainer
            );
            safeAppendChild(bodyElement, explanationRoot.element);
        }
    } else if (explanationRoot) {
        explanationRoot.root.unmount();
        explanationRoot = null;
    }
};

const renderComponents = () => {
    if (isRendering) {
        return;
    }
    isRendering = true;
    renderContentApp();
    renderAiExplanation();
    isRendering = false;
};

const cleanup = () => {
    if (contentRoot) {
        contentRoot.root.unmount();
        contentRoot = null;
    }
    if (explanationRoot) {
        explanationRoot.root.unmount();
        explanationRoot = null;
    }
};

const observeDocumentBody = () => {
    const observer = new MutationObserver(() => {
        renderComponents();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('beforeunload', () => {
        observer.disconnect();
        cleanup();
    });

    return observer;
};

renderComponents();

const observer = observeDocumentBody();

if (import.meta.hot) {
    import.meta.hot.dispose(() => {
        observer.disconnect();
        cleanup();
    });
}

Here is my current manifest:

{
  "manifest_version": 3,
  "version": "0.2.6",
  "name": "Paragliding Copilot AI",
  "description": "Enhance SHV FSVL eLearning experience with AI-generated explanations for questions, providing deeper understanding and insights.",
  "permissions": [
    "tabs",
    "storage"
  ],
  "action": {
    "default_icon": "src/assets/para-bot-no-bg.png",
    "16": "src/assets/para-bot-no-bg-16.png",
    "32": "src/assets/para-bot-no-bg-32.png",
    "48": "src/assets/para-bot-no-bg-48.png",
    "128": "src/assets/para-bot-no-bg-128.png"
  },
  "background": {
    "service_worker": "./src/background/background.ts"
  },
  "content_scripts": [
    {
      "js": [
        "./src/content/content.tsx"
      ],
      "matches": [
        "https://elearning.shv-fsvl.ch/*"
      ]
    }
  ],
  "web_accessible_resources": [
    {
      "resources": [
        "./src/assets/fonts/*"
      ],
      "matches": [
        "*://*/*"
      ]
    }
  ]
}

Additional Information:

Comments:

  • Is there a difference in how modules are handled when an extension is published versus when it’s loaded locally?
  • Could there be an issue with the way Vite bundles the extension for production?
  • Are there specific configurations required for React-based Chrome extensions when publishing?

Any help would be greatly appreciated!

2

Answers


  1. I checked your extension code, it seems to me, the problem is simple: you are under the impression that you uploaded the build files (dist folder), but from what I see, you uploaded the whole source folder (the parent of dist folder), the dead giveaway is the .tsx file on the error script reference link:

    chrome-extension://f…ntent/content.tsx:1
    

    The solution is, you should upload just the dist folder, not the parent of dist folder.

    Login or Signup to reply.
  2. You uploaded the root folder of the project to the web store instead of the dist folder.

    Here’s what you need to do:

    1. Build the project.
    2. Zip the dist folder.
    3. Upload the zip file.

    This is what you uploaded (root of the project):

    This is what you uploaded

    And this is what you should upload (dist folder after pnpm run build):

    Files of dist folder after pnpm run build

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search