javascriptreactjsgoogle-chromevite

Chrome Extension: React Cannot use import statement outside a module only on Chrome Extension Store


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:

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:

Any help would be greatly appreciated!


Solution

  • 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.