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:
- The issue only occurs after publishing the extension to the Chrome Web Store, making it hard to test locally.
- The extension works perfectly when loaded locally as an unpacked extension.
- You can test the website it interacts with here (no login required): https://elearning.shv-fsvl.ch/
- The Chrome extension is available here (may only be available in the Swiss store): Paragliding Copilot AI (https://chromewebstore.google.com/detail/paragliding-copilot-ai/fgifmceoflmldnlmbfjjppoilngpenej)
- I’m looking for suggestions on how to resolve this issue.
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
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 ofdist
folder), the dead giveaway is the.tsx
file on the error script reference link:The solution is, you should upload just the
dist
folder, not the parent ofdist
folder.You uploaded the root folder of the project to the web store instead of the
dist
folder.Here’s what you need to do:
This is what you uploaded (root of the project):
And this is what you should upload (
dist
folder afterpnpm run build
):