I’m trying to create a Shopify app without using the CLI. I’m using Express for the server, I created the react app with npm create [email removed] and ngrok is running to tunnel the app to my local environment.
I have written the server index.js file much like Shopify have in their latest app template. I haven’t touched the vite config file and it is not the same as theirs as I don’t fully understand what they’re doing with it yet (my first time using vite).
The issue I’m having is when loading the JS/JSX file in the HTML document. I get the following error:
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of “text/jsx”. Strict MIME type checking is enforced for module scripts per HTML spec.
I can successfully output the contents of a normal HTML file into the embedded app within Shopify’s admin by changing the file the route is returning. I can also output elements such as headings when I insert them in the index.html file. So it’s retrieving that fine.
If I change the type of the script tag in index.html to “text/javascript”, I then obviously I get the error Uncaught SyntaxError: Cannot use import statement outside a module (at main.jsx:1:1).
I also tried the typical npx create-react-app. This also doesn’t work when accessed through the Shopify app, although it doesn’t return the error in the console. It’s just blank.
The app works fine when I access it directly through localhost:PORT.
This is the server’s index.js:
import dotenv from 'dotenv';
import { readFileSync } from "fs";
import express from "express";
import serveStatic from "serve-static";
import { shopifyApp } from '@shopify/shopify-app-express';
import GDPRWebhookHandlers from "./gdpr.js";
dotenv.config();
const PORT = process.env.PORT ? process.env.PORT : 3500;
const shopify = shopifyApp({
api: {
apiKey: process.env.API_KEY,
apiSecretKey: process.env.API_SECRET_KEY,
scopes: process.env.SCOPES,
hostName: process.env.HOST_NAME,
},
auth: {
path: '/api/auth',
callbackPath: '/api/auth/callback',
},
webhooks: {
path: '/api/webhooks'
},
});
const app = express();
app.get(shopify.config.auth.path, shopify.auth.begin());
app.get(
shopify.config.auth.callbackPath,
shopify.auth.callback(),
shopify.redirectToShopifyOrAppRoot(),
);
app.post(
shopify.config.webhooks.path,
shopify.processWebhooks({ webhookHandlers: GDPRWebhookHandlers })
)
// All endpoints after this point will require an active session
app.use("/api/*", shopify.validateAuthenticatedSession());
app.use(express.json());
app.use(serveStatic(`${process.cwd()}/client/`, { index: false }));
app.use("/*", shopify.ensureInstalledOnShop(), async (_req, res, _next) => {
return res
.status(200)
.set("Content-Type", "text/html")
.send(readFileSync(`${process.cwd()}/client/index.html`));
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
The vite.config.js file:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
The index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
The main.jsx file:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)